primitive 0.2.57__py3-none-any.whl → 0.2.59__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
primitive/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.57"
4
+ __version__ = "0.2.59"
primitive/agent/runner.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  import os
3
3
  import shutil
4
4
  import typing
5
- from enum import Enum, IntEnum
5
+ from enum import Enum
6
6
  from pathlib import Path, PurePath
7
7
  from typing import Dict, List, TypedDict
8
8
 
@@ -31,12 +31,6 @@ class JobConfig(TypedDict):
31
31
  stores: List[str]
32
32
 
33
33
 
34
- # NOTE This must match FailureLevel subclass in JobSettings model
35
- class FailureLevel(IntEnum):
36
- ERROR = 1
37
- WARNING = 2
38
-
39
-
40
34
  class LogLevel(Enum):
41
35
  INFO = "INFO"
42
36
  ERROR = "ERROR"
primitive/client.py CHANGED
@@ -5,6 +5,8 @@ from loguru import logger
5
5
  from rich.logging import RichHandler
6
6
  from rich.traceback import install
7
7
 
8
+ from primitive.messaging.provider import MessagingProvider
9
+
8
10
  from .agent.actions import Agent
9
11
  from .auth.actions import Auth
10
12
  from .daemons.actions import Daemons
@@ -80,6 +82,8 @@ class Primitive:
80
82
  else:
81
83
  self.host_config = {"username": "", "token": token, "transport": transport}
82
84
 
85
+ self.messaging: MessagingProvider = MessagingProvider(self)
86
+
83
87
  self.auth: Auth = Auth(self)
84
88
  self.organizations: Organizations = Organizations(self)
85
89
  self.projects: Projects = Projects(self)
primitive/graphql/sdk.py CHANGED
@@ -26,7 +26,7 @@ def create_session(
26
26
  if fingerprint:
27
27
  headers["x-primitive-fingerprint"] = fingerprint
28
28
 
29
- transport = AIOHTTPTransport(url=url, headers=headers)
29
+ transport = AIOHTTPTransport(url=url, headers=headers, ssl=True)
30
30
  session = Client(
31
31
  transport=transport,
32
32
  fetch_schema_from_transport=fetch_schema_from_transport,
@@ -4,15 +4,18 @@ import json
4
4
  import platform
5
5
  import subprocess
6
6
  import typing
7
+ from dataclasses import dataclass
7
8
  from shutil import which
8
9
  from typing import Dict, List, Optional
9
10
 
10
11
  import click
12
+ import psutil
11
13
  from aiohttp import client_exceptions
12
14
  from gql import gql
13
15
  from loguru import logger
14
16
 
15
17
  from primitive.graphql.relay import from_base64
18
+ from primitive.messaging.provider import MESSAGE_TYPES
16
19
  from primitive.utils.memory_size import MemorySize
17
20
 
18
21
  from ..utils.auth import guard
@@ -24,6 +27,7 @@ from .graphql.mutations import (
24
27
  register_child_hardware_mutation,
25
28
  register_hardware_mutation,
26
29
  unregister_hardware_mutation,
30
+ hardware_certificate_create_mutation,
27
31
  )
28
32
  from .graphql.queries import (
29
33
  hardware_details,
@@ -31,6 +35,7 @@ from .graphql.queries import (
31
35
  nested_children_hardware_list,
32
36
  )
33
37
 
38
+
34
39
  if typing.TYPE_CHECKING:
35
40
  pass
36
41
 
@@ -40,6 +45,12 @@ from primitive.utils.actions import BaseAction
40
45
  from primitive.utils.shell import does_executable_exist
41
46
 
42
47
 
48
+ @dataclass
49
+ class CertificateCreateResult:
50
+ certificate_id: str
51
+ certificate_pem: str
52
+
53
+
43
54
  class Hardware(BaseAction):
44
55
  def __init__(self, *args, **kwargs) -> None:
45
56
  super().__init__(*args, **kwargs)
@@ -280,6 +291,48 @@ class Hardware(BaseAction):
280
291
  system_info["gpu_config"] = self._get_gpu_config()
281
292
  return system_info
282
293
 
294
+ @guard
295
+ def certificate_create(
296
+ self, hardware_id: str, csr_pem: str
297
+ ) -> CertificateCreateResult:
298
+ mutation = gql(hardware_certificate_create_mutation)
299
+ variables = {
300
+ "input": {
301
+ "hardwareId": hardware_id,
302
+ "csrPem": csr_pem,
303
+ }
304
+ }
305
+
306
+ if not self.primitive.session:
307
+ raise Exception("No active session available for certificate creation")
308
+
309
+ result = self.primitive.session.execute(
310
+ mutation,
311
+ variable_values=variables,
312
+ get_execution_result=True,
313
+ )
314
+
315
+ if result.errors:
316
+ message = " ".join([error.message for error in result.errors])
317
+ raise Exception(message)
318
+
319
+ if not result.data:
320
+ raise Exception(
321
+ "No data received from hardware certificate creation request"
322
+ )
323
+
324
+ hardware_certificate_create = result.data["hardwareCertificateCreate"]
325
+
326
+ if hardware_certificate_create["__typename"] == "OperationInfo":
327
+ messages = hardware_certificate_create["messages"]
328
+ message = " ".join([error["message"] for error in messages])
329
+ raise Exception(message)
330
+
331
+ return CertificateCreateResult(
332
+ certificate_id=hardware_certificate_create["id"],
333
+ certificate_pem=hardware_certificate_create["certificatePem"],
334
+ )
335
+
283
336
  @guard
284
337
  def register(self, organization_id: Optional[str] = None):
285
338
  system_info = self.get_system_info()
@@ -375,6 +428,24 @@ class Hardware(BaseAction):
375
428
 
376
429
  return result
377
430
 
431
+ def check_in(
432
+ self,
433
+ is_healthy: bool = True,
434
+ is_quarantined: bool = False,
435
+ is_available: bool = False,
436
+ is_online: bool = True,
437
+ stopping_agent: Optional[bool] = False,
438
+ ):
439
+ message = {
440
+ "is_healthy": is_healthy,
441
+ "is_quarantined": is_quarantined,
442
+ "is_available": is_available,
443
+ "is_online": is_online,
444
+ }
445
+ self.primitive.messaging.send_message(
446
+ message_type=MESSAGE_TYPES.CHECK_IN, message=message
447
+ )
448
+
378
449
  @guard
379
450
  def check_in_http(
380
451
  self,
@@ -667,3 +738,28 @@ class Hardware(BaseAction):
667
738
 
668
739
  except Exception as exception:
669
740
  logger.exception(f"Error checking in children: {exception}")
741
+
742
+ def push_metrics(self):
743
+ self.primitive.messaging.send_message(
744
+ message_type=MESSAGE_TYPES.METRICS,
745
+ message=self.get_metrics(),
746
+ )
747
+
748
+ def get_metrics(self):
749
+ cpu_percent = psutil.cpu_percent(interval=1, percpu=False)
750
+ virtual_memory = psutil.virtual_memory()
751
+ disk_usage = psutil.disk_usage("/")
752
+
753
+ metrics = {
754
+ "cpu_percent": cpu_percent,
755
+ "memory_percent": virtual_memory.percent,
756
+ "memory_total": virtual_memory.total,
757
+ "memory_available": virtual_memory.available,
758
+ "memory_used": virtual_memory.used,
759
+ "memory_free": virtual_memory.free,
760
+ "disk_percent": disk_usage.percent,
761
+ "disk_total": disk_usage.total,
762
+ "disk_used": disk_usage.used,
763
+ "disk_free": disk_usage.free,
764
+ }
765
+ return metrics
@@ -1,15 +1,18 @@
1
+ from time import sleep
1
2
  from typing import TYPE_CHECKING, Optional
2
3
 
3
4
  import click
4
-
5
- from ..utils.printer import print_result
6
- from .ui import render_hardware_table
5
+ from loguru import logger
6
+ from primitive.utils.printer import print_result
7
+ from primitive.hardware.ui import render_hardware_table
8
+ from primitive.utils.x509 import (
9
+ generate_csr_pem,
10
+ write_certificate_pem,
11
+ )
7
12
 
8
13
  if TYPE_CHECKING:
9
14
  from ..client import Primitive
10
15
 
11
- from loguru import logger
12
-
13
16
 
14
17
  @click.group()
15
18
  @click.pass_context
@@ -33,8 +36,17 @@ def systeminfo_command(context):
33
36
  type=str,
34
37
  help="Organization [slug] to register hardware with",
35
38
  )
39
+ @click.option(
40
+ "--issue-certificate",
41
+ is_flag=True,
42
+ show_default=True,
43
+ default=False,
44
+ help="Issue certificate.",
45
+ )
36
46
  @click.pass_context
37
- def register_command(context, organization: Optional[str] = None):
47
+ def register_command(
48
+ context, organization: Optional[str] = None, issue_certificate: bool = False
49
+ ):
38
50
  """Register Hardware with Primitive"""
39
51
  primitive: Primitive = context.obj.get("PRIMITIVE")
40
52
 
@@ -47,14 +59,30 @@ def register_command(context, organization: Optional[str] = None):
47
59
  logger.info("Registering hardware with the default organization.")
48
60
 
49
61
  result = primitive.hardware.register(organization_id=organization_id)
50
- color = "green" if result else "red"
51
- if result.data.get("registerHardware"):
52
- message = "Hardware registered successfully"
53
- else:
54
- message = (
55
- "There was an error registering this device. Please review the above logs."
62
+ hardware = result.data.get("registerHardware")
63
+
64
+ if not hardware:
65
+ print_result(
66
+ fg="red",
67
+ context=context,
68
+ message="There was an error registering this device. Please review the above logs.",
56
69
  )
57
- print_result(message=message, context=context, fg=color)
70
+ return
71
+
72
+ if issue_certificate:
73
+ certificate = primitive.hardware.certificate_create(
74
+ hardware_id=hardware["id"],
75
+ csr_pem=generate_csr_pem(
76
+ hardware_id=hardware["pk"],
77
+ ),
78
+ )
79
+ write_certificate_pem(certificate.certificate_pem)
80
+
81
+ print_result(
82
+ fg="green",
83
+ context=context,
84
+ message="Hardware registered successfully.",
85
+ )
58
86
 
59
87
 
60
88
  @cli.command("unregister")
@@ -77,12 +105,7 @@ def unregister_command(context):
77
105
  def checkin_command(context):
78
106
  """Checkin Hardware with Primitive"""
79
107
  primitive: Primitive = context.obj.get("PRIMITIVE")
80
- check_in_http_result = primitive.hardware.check_in_http()
81
- if messages := check_in_http_result.data.get("checkIn").get("messages"):
82
- print_result(message=messages, context=context, fg="yellow")
83
- else:
84
- message = "Hardware checked in successfully"
85
- print_result(message=message, context=context, fg="green")
108
+ primitive.hardware.check_in()
86
109
 
87
110
 
88
111
  @cli.command("list")
@@ -125,3 +148,17 @@ def get_command(context, hardware_identifier: str) -> None:
125
148
  return
126
149
  else:
127
150
  render_hardware_table([hardware])
151
+
152
+
153
+ @cli.command("metrics")
154
+ @click.pass_context
155
+ @click.option("--watch", is_flag=True, help="Watch hardware metrics")
156
+ def metrics_command(context, watch: bool = False) -> None:
157
+ """Get Hardware Metrics"""
158
+ primitive: Primitive = context.obj.get("PRIMITIVE")
159
+ if watch:
160
+ while True:
161
+ print_result(message=primitive.hardware.get_metrics(), context=context)
162
+ sleep(1)
163
+ else:
164
+ print_result(message=primitive.hardware.get_metrics(), context=context)
@@ -1,11 +1,29 @@
1
1
  from primitive.graphql.utility_fragments import operation_info_fragment
2
2
 
3
+ hardware_certificate_create_mutation = (
4
+ operation_info_fragment
5
+ + """
6
+ mutation hardwareCertificateCreate($input: HardwareCertificateCreateInput!) {
7
+ hardwareCertificateCreate(input: $input) {
8
+ __typename
9
+ ... on HardwareCertificate {
10
+ id
11
+ certificatePem
12
+ }
13
+ ...OperationInfoFragment
14
+ }
15
+ }
16
+ """
17
+ )
18
+
3
19
  register_hardware_mutation = (
4
20
  operation_info_fragment
5
21
  + """
6
22
  mutation registerHardware($input: RegisterHardwareInput!) {
7
23
  registerHardware(input: $input) {
8
24
  ... on Hardware {
25
+ id
26
+ pk
9
27
  fingerprint
10
28
  }
11
29
  ...OperationInfoFragment
@@ -31,10 +31,6 @@ fragment JobRunFragment on JobRun {
31
31
  jobSettings {
32
32
  containerArgs
33
33
  rootDirectory
34
- parseLogs
35
- parseStderr
36
- failureLevel
37
- repositoryFilename
38
34
  config
39
35
  }
40
36
  gitCommit {
File without changes
@@ -0,0 +1,127 @@
1
+ import enum
2
+ import json
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING
5
+ from uuid import uuid4
6
+ import pika
7
+ from pika import credentials
8
+ from ..utils.x509 import (
9
+ are_certificate_files_present,
10
+ create_ssl_context,
11
+ read_certificate_common_name,
12
+ )
13
+ from loguru import logger
14
+
15
+ from primitive.utils.actions import BaseAction
16
+
17
+ if TYPE_CHECKING:
18
+ import primitive.client
19
+
20
+ EXCHANGE = "hardware"
21
+ ROUTING_KEY = "hardware"
22
+ VIRTUAL_HOST = "primitive"
23
+ CELERY_TASK_NAME = "hardware.tasks.task_receive_hardware_message"
24
+
25
+
26
+ class MESSAGE_TYPES(enum.Enum):
27
+ CHECK_IN = "CHECK_IN"
28
+ METRICS = "METRICS"
29
+
30
+
31
+ class MessagingProvider(BaseAction):
32
+ def __init__(self, primitive: "primitive.client.Primitive") -> None:
33
+ super().__init__(primitive=primitive)
34
+ self.ready = False
35
+
36
+ self.fingerprint = self.primitive.host_config.get("fingerprint", None)
37
+ if not self.fingerprint:
38
+ return
39
+ self.token = self.primitive.host_config.get("token", None)
40
+ if not self.token:
41
+ return
42
+
43
+ rabbitmq_host = "rabbitmq-cluster.primitive.tech"
44
+ RABBITMQ_PORT = 443
45
+
46
+ if primitive.host == "api.dev.primitive.tech":
47
+ rabbitmq_host = "rabbitmq-cluster.dev.primitive.tech"
48
+ elif primitive.host == "api.primitive.tech":
49
+ rabbitmq_host = "rabbitmq-cluster.primitive.tech"
50
+ elif primitive.host == "api.staging.primitive.tech":
51
+ rabbitmq_host = "rabbitmq-cluster.staging.primitive.tech"
52
+ elif primitive.host == "api.test.primitive.tech":
53
+ rabbitmq_host = "rabbitmq-cluster.test.primitive.tech"
54
+ elif primitive.host == "localhost:8000":
55
+ rabbitmq_host = "localhost"
56
+ RABBITMQ_PORT = 5671
57
+
58
+ if not are_certificate_files_present():
59
+ logger.warning(
60
+ "Certificate files not present or incomplete. MessagingProvider not initialized."
61
+ )
62
+ return
63
+
64
+ ssl_context = create_ssl_context()
65
+ ssl_options = pika.SSLOptions(ssl_context)
66
+ self.common_name = read_certificate_common_name()
67
+
68
+ self.parameters = pika.ConnectionParameters(
69
+ host=rabbitmq_host,
70
+ port=RABBITMQ_PORT,
71
+ virtual_host=VIRTUAL_HOST,
72
+ ssl_options=ssl_options,
73
+ credentials=credentials.ExternalCredentials(),
74
+ )
75
+
76
+ self.ready = True
77
+
78
+ def send_message(self, message_type: MESSAGE_TYPES, message: dict[str, any]): # type: ignore
79
+ if not self.ready:
80
+ logger.warning(
81
+ "send_message: cannot send message. MessagingProvider not initialized."
82
+ )
83
+ return
84
+
85
+ body = {
86
+ "timestamp": datetime.now(timezone.utc).isoformat(),
87
+ "message_type": message_type.value,
88
+ "message": message,
89
+ }
90
+
91
+ full_body_for_celery = [
92
+ [],
93
+ body,
94
+ {"callbacks": None, "errbacks": None, "chain": None, "chord": None},
95
+ ]
96
+ with pika.BlockingConnection(parameters=self.parameters) as conn:
97
+ message_uuid = str(uuid4())
98
+ channel = conn.channel()
99
+
100
+ headers = {
101
+ "fingerprint": self.fingerprint,
102
+ "token": self.token,
103
+ "argsrepr": "()",
104
+ "id": message_uuid,
105
+ "ignore_result": False,
106
+ "kwargsrepr": str(body),
107
+ "lang": "py",
108
+ "replaced_task_nesting": 0,
109
+ "retries": 0,
110
+ "root_id": message_uuid,
111
+ "task": CELERY_TASK_NAME,
112
+ }
113
+
114
+ channel.basic_publish(
115
+ exchange=EXCHANGE,
116
+ routing_key=ROUTING_KEY,
117
+ body=json.dumps(full_body_for_celery),
118
+ properties=pika.BasicProperties(
119
+ user_id=self.common_name,
120
+ correlation_id=message_uuid,
121
+ priority=0,
122
+ delivery_mode=2,
123
+ headers=headers,
124
+ content_encoding="utf-8",
125
+ content_type="application/json",
126
+ ),
127
+ )
@@ -67,6 +67,8 @@ class Monitor(BaseAction):
67
67
  # handles cleanup of old reservations
68
68
  # obtains an active JobRun's ID
69
69
  if not RUNNING_IN_CONTAINER:
70
+ self.primitive.hardware.push_metrics()
71
+
70
72
  hardware = self.primitive.hardware.get_own_hardware_details()
71
73
  # fetch the latest hardware and activeReservation details
72
74
  if active_reservation_data := hardware["activeReservation"]:
@@ -1,6 +1,6 @@
1
- import typing
1
+ from typing import TYPE_CHECKING
2
2
 
3
- if typing.TYPE_CHECKING:
3
+ if TYPE_CHECKING:
4
4
  import primitive.client
5
5
 
6
6
 
@@ -4,7 +4,7 @@ import click
4
4
  from loguru import logger
5
5
 
6
6
 
7
- def print_result(message: str, context: click.Context, fg: str = None):
7
+ def print_result(message: str | dict, context: click.Context, fg: str = None):
8
8
  """Print message to stdout or stderr"""
9
9
  if context.obj["DEBUG"]:
10
10
  logger.info(json.dumps(message))
@@ -0,0 +1,140 @@
1
+ import ssl
2
+ from pathlib import Path
3
+ from loguru import logger
4
+
5
+ from cryptography.hazmat.primitives import serialization, hashes
6
+ from cryptography.hazmat.primitives.asymmetric import ec
7
+ from cryptography import x509
8
+ from cryptography.x509.oid import NameOID
9
+
10
+ PrivateKey = ec.EllipticCurvePrivateKey
11
+
12
+ HOME_DIRECTORY = Path.home()
13
+
14
+ PRIVATE_KEY_PATH = Path(HOME_DIRECTORY / ".config" / "primitive" / "private-key.pem")
15
+ CERTIFICATE_PATH = Path(HOME_DIRECTORY / ".config" / "primitive" / "certificate.pem")
16
+
17
+ # NOTE Only used with self-signed server certificate
18
+ # THIS IS FOR LOCAL TESTING
19
+ SELF_SIGNED_SERVER_CA_PATH = Path(
20
+ HOME_DIRECTORY / ".config" / "primitive" / "server-ca.crt.pem"
21
+ )
22
+
23
+
24
+ def are_certificate_files_present() -> bool:
25
+ return PRIVATE_KEY_PATH.exists() and CERTIFICATE_PATH.exists()
26
+
27
+
28
+ def generate_private_key() -> PrivateKey:
29
+ return ec.generate_private_key(
30
+ curve=ec.SECP521R1(),
31
+ )
32
+
33
+
34
+ def read_certificate() -> x509.Certificate:
35
+ cert = x509.load_pem_x509_certificate(CERTIFICATE_PATH.read_bytes())
36
+ return cert
37
+
38
+
39
+ def read_certificate_common_name() -> str:
40
+ cert = read_certificate()
41
+
42
+ names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
43
+
44
+ return str(names[0].value)
45
+
46
+
47
+ def create_ssl_context() -> ssl.SSLContext:
48
+ context = ssl.create_default_context(
49
+ cafile=SELF_SIGNED_SERVER_CA_PATH
50
+ if SELF_SIGNED_SERVER_CA_PATH.exists()
51
+ else None,
52
+ purpose=ssl.Purpose.SERVER_AUTH,
53
+ )
54
+ context.load_cert_chain(
55
+ certfile=CERTIFICATE_PATH,
56
+ keyfile=PRIVATE_KEY_PATH,
57
+ )
58
+
59
+ return context
60
+
61
+
62
+ def read_private_key() -> PrivateKey:
63
+ private_key = serialization.load_pem_private_key(
64
+ data=PRIVATE_KEY_PATH.read_bytes(),
65
+ password=None,
66
+ )
67
+
68
+ if not isinstance(private_key, PrivateKey):
69
+ raise Exception(
70
+ f"Expected private key type {PrivateKey.__name__}, got {type(private_key).__name__}"
71
+ )
72
+
73
+ return private_key
74
+
75
+
76
+ def ensure_private_key() -> PrivateKey:
77
+ private_key_path = PRIVATE_KEY_PATH
78
+
79
+ if private_key_path.exists():
80
+ private_key = read_private_key()
81
+ else:
82
+ logger.info("Generating private key.")
83
+
84
+ private_key = generate_private_key()
85
+
86
+ with private_key_path.open("wb") as f:
87
+ f.write(
88
+ private_key.private_bytes(
89
+ encoding=serialization.Encoding.PEM,
90
+ format=serialization.PrivateFormat.PKCS8,
91
+ encryption_algorithm=serialization.NoEncryption(),
92
+ )
93
+ )
94
+
95
+ return private_key
96
+
97
+
98
+ def write_certificate_pem(certificate_pem: str):
99
+ CERTIFICATE_PATH.write_text(
100
+ data=certificate_pem,
101
+ encoding="utf-8",
102
+ )
103
+
104
+
105
+ def check_certificate():
106
+ try:
107
+ if CERTIFICATE_PATH.exists():
108
+ with CERTIFICATE_PATH.open("rb") as file:
109
+ private_key = read_private_key()
110
+ crt = x509.load_pem_x509_certificate(file.read())
111
+
112
+ # NOTE: Make sure certificate match private key
113
+ return crt.public_key() == private_key.public_key()
114
+ except Exception:
115
+ return False
116
+
117
+ return False
118
+
119
+
120
+ def generate_csr_pem(hardware_id: str) -> str:
121
+ builder = x509.CertificateSigningRequestBuilder()
122
+ builder = builder.subject_name(
123
+ x509.Name(
124
+ [
125
+ x509.NameAttribute(NameOID.COMMON_NAME, hardware_id),
126
+ ]
127
+ )
128
+ )
129
+ builder = builder.add_extension(
130
+ x509.BasicConstraints(ca=False, path_length=None),
131
+ critical=True,
132
+ )
133
+ csr = builder.sign(
134
+ algorithm=hashes.SHA512(),
135
+ private_key=ensure_private_key(),
136
+ )
137
+
138
+ return csr.public_bytes(encoding=serialization.Encoding.PEM).decode(
139
+ encoding="utf-8", errors="strict"
140
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.57
3
+ Version: 0.2.59
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
@@ -22,6 +22,7 @@ Requires-Dist: click
22
22
  Requires-Dist: gql[all]
23
23
  Requires-Dist: loguru
24
24
  Requires-Dist: paramiko[invoke]
25
+ Requires-Dist: pika>=1.3.2
25
26
  Requires-Dist: psutil>=7.0.0
26
27
  Requires-Dist: rich>=13.9.4
27
28
  Requires-Dist: speedtest-cli
@@ -1,11 +1,11 @@
1
- primitive/__about__.py,sha256=cDEWXb9BZdVtt8aKIxF1WZw-mD4ABL_QS4Vz6YUcy3A,130
1
+ primitive/__about__.py,sha256=DQFleay9CHBMqSyleA7yap__uZps7tWqIeLhtV6rCvw,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
3
  primitive/cli.py,sha256=g7EtHI9MATAB0qQu5w-WzbXtxz_8zu8z5E7sETmMkKU,2509
4
- primitive/client.py,sha256=RMF46F89oK82gfZH6Bf0WZrhXPUu01pbieSO_Vcuoc4,3624
4
+ primitive/client.py,sha256=gyZIj61qMtOi_s8y0WG3gDehGT-Ms3BtbDP2J_2lajU,3753
5
5
  primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  primitive/agent/actions.py,sha256=f1sSDdFHOflfGQCwNJ5P51YWUTKs8EK5YXbMAvZ-KMM,8018
7
7
  primitive/agent/commands.py,sha256=o847pK7v7EWQGG67tky6a33qtwoutX6LZrP2FIS_NOk,388
8
- primitive/agent/runner.py,sha256=1ceYsEY1mYTLzvsm3vIT8UgohxyvEQZVrErFci5rcM0,11648
8
+ primitive/agent/runner.py,sha256=vymeuIDiah6J9SHjTbsNQGi74PpnJw1MxWPd3TWjc-w,11512
9
9
  primitive/agent/uploader.py,sha256=DT_Nzt5eOTm_uRcYKm1sjBBaQZzp5iNZ_uN5XktfQ30,3382
10
10
  primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  primitive/auth/actions.py,sha256=9NIEXJ1BNJutJs6AMMSjMN_ziONUAUhY_xHwojYJCLA,942
@@ -36,25 +36,27 @@ primitive/git/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
36
36
  primitive/git/graphql/queries.py,sha256=I1HGlqBb1lHIAWVSsC8tVY9JdsQ8DJVqs4nqSTcL30M,98
37
37
  primitive/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,726
39
- primitive/graphql/sdk.py,sha256=KhVWDZms_eMBgt6ftSJitRALguagy-nmrj4IC2taeXY,1535
39
+ primitive/graphql/sdk.py,sha256=dE4TD8KiTKw3Y0uiw5XrIcuZGqexE47eSlPaPD6jDGo,1545
40
40
  primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
41
41
  primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- primitive/hardware/actions.py,sha256=qBMcCcgSf3XRe-ua1unf5M64J-Y8EelUQKysa8vje0s,26060
42
+ primitive/hardware/actions.py,sha256=qDbwd8W6zMlEfp1IfxTWiyAFqV5iYomDisGVXTRfy8Y,29145
43
43
  primitive/hardware/android.py,sha256=tu7pBPxWFrIwb_mm5CEdFFf1_veNDOKjOCQg13i_Lh4,2758
44
- primitive/hardware/commands.py,sha256=NMliVHBZDl4UAvhmNEjrvN9KWPuqn87-d7eVb0ZqEYA,3752
44
+ primitive/hardware/commands.py,sha256=lwO-cm3doGtuGvR30fib-nALJyNCScdCTXOVVbYVpkY,4604
45
45
  primitive/hardware/ui.py,sha256=12rucuZ2s-w5R4bKyxON5dEbrdDnVf5sbj3K_nbdo44,2473
46
46
  primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  primitive/hardware/graphql/fragments.py,sha256=H315uv-ujOsZEbQR5tmstbRU_9OCa5SErZMwIckSiLc,383
48
- primitive/hardware/graphql/mutations.py,sha256=_4Hkbfik9Ron4T-meulu6T-9FR_BZjyPNwn745MPksU,1484
48
+ primitive/hardware/graphql/mutations.py,sha256=wOKNJN-1x_DGxvdHt2Bm3AUqyW8P1lNPkxLIQThbXUg,1874
49
49
  primitive/hardware/graphql/queries.py,sha256=I86uLuOSjHSph11Y5MVCYko5Js7hoiEZ-cEoPTc4J-k,1392
50
50
  primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  primitive/jobs/actions.py,sha256=Fej-rpdfIiHjs0SEFY2ZcMn64AWHn6kD90RC5wtRM3Q,6531
52
52
  primitive/jobs/commands.py,sha256=MxPCkBEYW_eLNqgCRYeyj7ZcLOFAWfpVZlqDR2Y_S0o,830
53
53
  primitive/jobs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- primitive/jobs/graphql/fragments.py,sha256=DJKsvt59dYbyXiXrFZB_1NMuamRLaeR-oBu-9P7aq0M,684
54
+ primitive/jobs/graphql/fragments.py,sha256=rWZWxZs9ZWjWal0eiY_XPWUS7i7f3fwSM_w8ZvDs2JQ,614
55
55
  primitive/jobs/graphql/mutations.py,sha256=8ASvCmwQh7cMeeiykOdYaYVryG8FRIuVF6v_J8JJZuw,219
56
56
  primitive/jobs/graphql/queries.py,sha256=ZxNmm-WovytbggNuKRnwa0kc26T34_0yhqkoqx-2uj0,1736
57
- primitive/monitor/actions.py,sha256=coGEQkKvkTRO0Zgii4hQOTSk1wa9HkVFDn4OiEJ20r4,9581
57
+ primitive/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
+ primitive/messaging/provider.py,sha256=NCgl1KiV4Ow49Laz2grNGsP7Ij4vUUeorUjkhAKF1H4,4182
59
+ primitive/monitor/actions.py,sha256=vdjfA-3v1j4g-S-gs6mziE3pDf3eR6dtYtgEVRnSfBQ,9641
58
60
  primitive/monitor/commands.py,sha256=VDlEL_Qpm_ysHxug7VpI0cVAZ0ny6AS91Y58D7F1zkU,409
59
61
  primitive/organizations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
62
  primitive/organizations/actions.py,sha256=kVHOhG1oS2sI5p8uldSo5L-RUZsnG36eaulVuKLyZ-M,1863
@@ -82,7 +84,7 @@ primitive/reservations/graphql/fragments.py,sha256=h3edd0Es38SNldhHiRU3hAJNYwrUI
82
84
  primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
83
85
  primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
84
86
  primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
- primitive/utils/actions.py,sha256=HOFrmM3-0A_A3NS84MqrZ6JmQEiiPSoDqEeuu6b_qfQ,196
87
+ primitive/utils/actions.py,sha256=Gm84YgXFSpilpYqjG5nxcQTdRSp3waN7ZorzIzMBLtc,208
86
88
  primitive/utils/auth.py,sha256=uBIZNPF2CpbaPV2UMi6eWVUKghV6WIm-pG3-UM29bNs,1465
87
89
  primitive/utils/cache.py,sha256=FHGmVWYLJFQOazpXXcEwI0YJEZbdkgG39nOLdOv6VNk,1575
88
90
  primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw,2914
@@ -91,12 +93,13 @@ primitive/utils/daemons.py,sha256=mSoSHitiGfS4KYAEK9sKsiv_YcACHKgY3qISnDpUUIE,10
91
93
  primitive/utils/exceptions.py,sha256=KSAR2zqWEjQhqb6OAtaMUvYejKPETdBVRRRIKzy90A8,554
92
94
  primitive/utils/logging.py,sha256=vpwu-hByZC1BgJfUi6iSfAxzCobP_zg9-99EUf80KtY,1132
93
95
  primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
94
- primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
96
+ primitive/utils/printer.py,sha256=Iy9RkTyDZ6CcoTZfiNTPMAAT2rM8U0Reku4RjQf_DqM,377
95
97
  primitive/utils/psutil.py,sha256=xa7ef435UL37jyjmUPbEqCO2ayQMpCs0HCrxVEvLcuM,763
96
98
  primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
97
99
  primitive/utils/text.py,sha256=XiESMnlhjQ534xE2hMNf08WehE1SKaYFRNih0MmnK0k,829
98
- primitive-0.2.57.dist-info/METADATA,sha256=hv8Qi897IB5ogOUPwKVZpoyLjYp66qTeR4_x3Q6wekI,3513
99
- primitive-0.2.57.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
100
- primitive-0.2.57.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
101
- primitive-0.2.57.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
102
- primitive-0.2.57.dist-info/RECORD,,
100
+ primitive/utils/x509.py,sha256=HwHRPqakTHWd40ny-9O_yNknSL1Cxo50O0UCjXHFq04,3796
101
+ primitive-0.2.59.dist-info/METADATA,sha256=TK2BJRsrYXbcffM-S0oA9sBT03PjeiLEeDBwlaH1m6s,3540
102
+ primitive-0.2.59.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
103
+ primitive-0.2.59.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
104
+ primitive-0.2.59.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
105
+ primitive-0.2.59.dist-info/RECORD,,