primitive 0.1.57__py3-none-any.whl → 0.1.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.
Files changed (63) hide show
  1. primitive/__about__.py +1 -1
  2. primitive/agent/__init__.py +0 -0
  3. primitive/agent/actions.py +24 -16
  4. primitive/agent/runner.py +12 -9
  5. primitive/auth/actions.py +5 -12
  6. primitive/auth/graphql/__init__.py +0 -0
  7. primitive/auth/graphql/queries.py +13 -0
  8. primitive/cli.py +11 -5
  9. primitive/client.py +4 -0
  10. primitive/daemons/__init__.py +0 -0
  11. primitive/exec/__init__.py +0 -0
  12. primitive/exec/actions.py +50 -0
  13. primitive/exec/commands.py +22 -0
  14. primitive/files/__init__.py +0 -0
  15. primitive/files/actions.py +9 -16
  16. primitive/files/graphql/__init__.py +0 -0
  17. primitive/files/graphql/mutations.py +11 -0
  18. primitive/git/actions.py +11 -11
  19. primitive/git/commands.py +7 -6
  20. primitive/git/graphql/__init__.py +0 -0
  21. primitive/git/graphql/queries.py +7 -0
  22. primitive/graphql/relay.py +32 -0
  23. primitive/graphql/utility_fragments.py +19 -0
  24. primitive/hardware/__init__.py +0 -0
  25. primitive/hardware/actions.py +74 -121
  26. primitive/hardware/commands.py +15 -5
  27. primitive/hardware/graphql/__init__.py +0 -0
  28. primitive/hardware/graphql/fragments.py +22 -0
  29. primitive/hardware/graphql/mutations.py +45 -0
  30. primitive/hardware/graphql/queries.py +31 -0
  31. primitive/jobs/__init__.py +0 -0
  32. primitive/jobs/actions.py +32 -201
  33. primitive/jobs/graphql/__init__.py +0 -0
  34. primitive/jobs/graphql/fragments.py +47 -0
  35. primitive/jobs/graphql/mutations.py +11 -0
  36. primitive/jobs/graphql/queries.py +100 -0
  37. primitive/lint/__init__.py +0 -0
  38. primitive/organizations/__init__.py +0 -0
  39. primitive/organizations/actions.py +4 -49
  40. primitive/organizations/graphql/__init__.py +0 -0
  41. primitive/organizations/graphql/fragments.py +10 -0
  42. primitive/organizations/graphql/mutations.py +0 -0
  43. primitive/organizations/graphql/queries.py +38 -0
  44. primitive/projects/actions.py +5 -47
  45. primitive/projects/graphql/__init__.py +0 -0
  46. primitive/projects/graphql/fragments.py +10 -0
  47. primitive/projects/graphql/mutations.py +0 -0
  48. primitive/projects/graphql/queries.py +36 -0
  49. primitive/reservations/__init__.py +0 -0
  50. primitive/reservations/actions.py +134 -0
  51. primitive/reservations/commands.py +67 -0
  52. primitive/reservations/graphql/__init__.py +0 -0
  53. primitive/reservations/graphql/fragments.py +40 -0
  54. primitive/reservations/graphql/mutations.py +29 -0
  55. primitive/reservations/graphql/queries.py +47 -0
  56. primitive/sim/actions.py +12 -9
  57. primitive/utils/__init__.py +0 -0
  58. {primitive-0.1.57.dist-info → primitive-0.1.59.dist-info}/METADATA +1 -1
  59. primitive-0.1.59.dist-info/RECORD +95 -0
  60. primitive-0.1.57.dist-info/RECORD +0 -53
  61. {primitive-0.1.57.dist-info → primitive-0.1.59.dist-info}/WHEEL +0 -0
  62. {primitive-0.1.57.dist-info → primitive-0.1.59.dist-info}/entry_points.txt +0 -0
  63. {primitive-0.1.57.dist-info → primitive-0.1.59.dist-info}/licenses/LICENSE.txt +0 -0
primitive/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.1.57"
4
+ __version__ = "0.1.59"
File without changes
@@ -1,9 +1,12 @@
1
- import sys
2
1
  import shutil
2
+ import sys
3
3
  from time import sleep
4
- from primitive.utils.actions import BaseAction
4
+
5
5
  from loguru import logger
6
+
6
7
  from primitive.__about__ import __version__
8
+ from primitive.utils.actions import BaseAction
9
+
7
10
  from ..utils.cache import get_sources_cache
8
11
  from .runner import AgentRunner
9
12
  from .uploader import Uploader
@@ -73,12 +76,12 @@ class Agent(BaseAction):
73
76
  sleep(sleep_amount)
74
77
  continue
75
78
 
76
- job_runs_data = self.primitive.jobs.get_job_runs(
79
+ job_runs_result = self.primitive.jobs.get_job_runs(
77
80
  status="pending", first=1, reservation_id=active_reservation_id
78
81
  )
79
82
 
80
83
  pending_job_runs = [
81
- edge["node"] for edge in job_runs_data["jobRuns"]["edges"]
84
+ edge["node"] for edge in job_runs_result.data["jobRuns"]["edges"]
82
85
  ]
83
86
 
84
87
  if not pending_job_runs:
@@ -119,18 +122,23 @@ class Agent(BaseAction):
119
122
  job_run["jobSettings"]["rootDirectory"]
120
123
  )
121
124
 
122
- runner = AgentRunner(
123
- primitive=self.primitive,
124
- source_dir=source_dir,
125
- job_id=job_run["id"],
126
- job_slug=job_run["job"]["slug"],
127
- )
128
-
129
- # Execute job
130
- runner.execute()
131
-
132
- # Clean up
133
- shutil.rmtree(path=downloaded_git_repository_dir)
125
+ try:
126
+ # Initialize Runner
127
+ runner = AgentRunner(
128
+ primitive=self.primitive,
129
+ source_dir=source_dir,
130
+ job_id=job_run["id"],
131
+ job_slug=job_run["job"]["slug"],
132
+ )
133
+ except Exception as e:
134
+ # Log Error
135
+ logger.error(f"Error initializing agent runner: {e}")
136
+ else:
137
+ # Execute job
138
+ runner.execute()
139
+ finally:
140
+ # Clean up
141
+ shutil.rmtree(path=downloaded_git_repository_dir)
134
142
 
135
143
  sleep(5)
136
144
  except KeyboardInterrupt:
primitive/agent/runner.py CHANGED
@@ -1,16 +1,17 @@
1
- import yaml
2
- import sys
3
- import typing
4
1
  import os
5
2
  import threading
6
- from time import sleep
7
- from typing import TypedDict, Iterable, List, Optional, Dict
3
+ import typing
8
4
  from pathlib import Path, PurePath
5
+ from time import sleep
6
+ from typing import Dict, Iterable, List, Optional, TypedDict
7
+
8
+ import yaml
9
9
  from loguru import logger
10
- from .process import Process
11
- from .provision import ProvisionPython
10
+
12
11
  from ..utils.cache import get_artifacts_cache
13
12
  from ..utils.files import find_files_for_extension
13
+ from .process import Process
14
+ from .provision import ProvisionPython
14
15
 
15
16
  try:
16
17
  from yaml import CLoader as Loader
@@ -72,7 +73,8 @@ class AgentRunner:
72
73
  logger.error(
73
74
  f"Found two job descriptions with the same slug: {self.job_slug}"
74
75
  )
75
- sys.exit(1)
76
+ self.conclude(conclusion="failure")
77
+ raise FileExistsError
76
78
 
77
79
  if yaml_file.exists():
78
80
  self.job = yaml.load(open(yaml_file, "r"), Loader=Loader)
@@ -82,7 +84,8 @@ class AgentRunner:
82
84
  logger.error(
83
85
  f"No job description with matching slug '{self.job_slug}' found"
84
86
  )
85
- sys.exit(1)
87
+ self.conclude(conclusion="failure")
88
+ raise FileNotFoundError
86
89
 
87
90
  logger.info(f"Found job description for {self.job_slug}")
88
91
 
primitive/auth/actions.py CHANGED
@@ -1,23 +1,16 @@
1
1
  from gql import gql
2
2
 
3
- from ..utils.config import read_config_file, update_config_file
4
- from ..utils.auth import guard
5
-
6
3
  from primitive.utils.actions import BaseAction
7
4
 
5
+ from ..utils.auth import guard
6
+ from ..utils.config import read_config_file, update_config_file
7
+ from .graphql.queries import whoami_query
8
+
8
9
 
9
10
  class Auth(BaseAction):
10
11
  @guard
11
12
  def whoami(self):
12
- query = gql(
13
- """
14
- query whoami {
15
- whoami {
16
- username
17
- }
18
- }
19
- """
20
- )
13
+ query = gql(whoami_query)
21
14
 
22
15
  result = self.primitive.session.execute(query, get_execution_result=True)
23
16
 
File without changes
@@ -0,0 +1,13 @@
1
+ whoami_query = """
2
+ query whoami {
3
+ whoami {
4
+ username
5
+ defaultOrganization {
6
+ id
7
+ pk
8
+ name
9
+ slug
10
+ }
11
+ }
12
+ }
13
+ """
primitive/cli.py CHANGED
@@ -1,18 +1,22 @@
1
1
  import os
2
2
  import sys
3
+
3
4
  import click
5
+
4
6
  from .__about__ import __version__
5
- from .client import Primitive
7
+ from .agent.commands import cli as agent_commands
6
8
  from .auth.commands import config_command, whoami_command
9
+ from .client import Primitive
10
+ from .daemons.commands import cli as daemons_commands
11
+ from .exec.commands import cli as exec_commands
7
12
  from .files.commands import cli as file_commands
8
- from .hardware.commands import cli as hardware_commands
9
- from .lint.commands import cli as lint_commands
10
- from .agent.commands import cli as agent_commands
11
13
  from .git.commands import cli as git_commands
12
- from .daemons.commands import cli as daemons_commands
14
+ from .hardware.commands import cli as hardware_commands
13
15
  from .jobs.commands import cli as jobs_commands
16
+ from .lint.commands import cli as lint_commands
14
17
  from .organizations.commands import cli as organizations_commands
15
18
  from .projects.commands import cli as projects_commands
19
+ from .reservations.commands import cli as reservations_commands
16
20
  from .sim.commands import cli as sim_commands
17
21
 
18
22
 
@@ -69,6 +73,8 @@ cli.add_command(jobs_commands, "jobs")
69
73
  cli.add_command(organizations_commands, "organizations")
70
74
  cli.add_command(projects_commands, "projects")
71
75
  cli.add_command(sim_commands, "sim")
76
+ cli.add_command(reservations_commands, "reservations")
77
+ cli.add_command(exec_commands, "exec")
72
78
 
73
79
  if __name__ == "__main__":
74
80
  cli(obj={})
primitive/client.py CHANGED
@@ -11,6 +11,8 @@ from .git.actions import Git
11
11
  from .daemons.actions import Daemons
12
12
  from .jobs.actions import Jobs
13
13
  from .organizations.actions import Organizations
14
+ from .exec.actions import Exec
15
+ from .reservations.actions import Reservations
14
16
 
15
17
  from loguru import logger
16
18
 
@@ -58,11 +60,13 @@ class Primitive:
58
60
  self.jobs: Jobs = Jobs(self)
59
61
  self.files: Files = Files(self)
60
62
  self.sim: Sim = Sim(self)
63
+ self.reservations: Reservations = Reservations(self)
61
64
  self.hardware: Hardware = Hardware(self)
62
65
  self.lint: Lint = Lint(self)
63
66
  self.agent: Agent = Agent(self)
64
67
  self.git: Git = Git(self)
65
68
  self.daemons: Daemons = Daemons(self)
69
+ self.exec: Exec = Exec(self)
66
70
 
67
71
  def get_host_config(self):
68
72
  self.full_config = read_config_file()
File without changes
File without changes
@@ -0,0 +1,50 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ pass
5
+
6
+
7
+ from primitive.utils.actions import BaseAction
8
+
9
+
10
+ class Exec(BaseAction):
11
+ def __init__(self, *args, **kwargs) -> None:
12
+ super().__init__(*args, **kwargs)
13
+
14
+ def execute_command(self, hardware_identifier: str, command: str) -> None:
15
+ hardware = self.primitive.hardware.get_hardware_from_slug_or_id(
16
+ hardware_identifier=hardware_identifier
17
+ )
18
+
19
+ # since we found hardware, we need to check that the user:
20
+ # - has a valid reservation on it
21
+ # - OR if the device is free we can reserve it
22
+
23
+ # if we create a reservation on behalf of the user, we need to release it after
24
+ created_reservation_on_behalf_of_user = False
25
+
26
+ if active_reservation := hardware["activeReservation"]:
27
+ active_reservation_id = active_reservation["id"]
28
+ reservation_result = self.primitive.reservations.get_reservation(
29
+ reservation_id=active_reservation_id
30
+ )
31
+ reservation = reservation_result.data["reservation"]
32
+ else:
33
+ reservation_result = self.primitive.reservations.create_reservation(
34
+ requested_hardware_ids=[hardware["id"]],
35
+ reason="Executing command from Primitive CLI",
36
+ )
37
+ reservation = reservation_result.data["reservationCreate"]
38
+ created_reservation_on_behalf_of_user = True
39
+
40
+ reservation = self.primitive.reservations.wait_for_reservation_status(
41
+ reservation_id=reservation["id"], desired_status="in_progress"
42
+ )
43
+
44
+ print(f"Executing command: {command} on {hardware['name']}")
45
+
46
+ if created_reservation_on_behalf_of_user:
47
+ print("Cleaning up reservation.")
48
+ self.primitive.reservations.release_reservation(
49
+ reservation_or_hardware_identifier=reservation["id"]
50
+ )
@@ -0,0 +1,22 @@
1
+ import typing
2
+
3
+ import click
4
+
5
+ if typing.TYPE_CHECKING:
6
+ from ..client import Primitive
7
+
8
+
9
+ @click.command("exec")
10
+ @click.pass_context
11
+ @click.argument(
12
+ "hardware_identifier",
13
+ type=str,
14
+ required=True,
15
+ )
16
+ @click.argument("command", nargs=-1, required=True)
17
+ def cli(context, hardware_identifier: str, command: str) -> None:
18
+ """Exec"""
19
+ primitive: Primitive = context.obj.get("PRIMITIVE")
20
+ primitive.exec.execute_command(
21
+ hardware_identifier=hardware_identifier, command=command
22
+ )
File without changes
@@ -1,10 +1,13 @@
1
1
  from pathlib import Path
2
+
2
3
  from gql import gql
3
- from primitive.graphql.sdk import create_requests_session
4
- from ..utils.auth import guard
5
4
 
5
+ from primitive.graphql.sdk import create_requests_session
6
6
  from primitive.utils.actions import BaseAction
7
7
 
8
+ from ..utils.auth import guard
9
+ from .graphql.mutations import create_trace_mutation
10
+
8
11
 
9
12
  class Files(BaseAction):
10
13
  @guard
@@ -17,19 +20,7 @@ class Files(BaseAction):
17
20
  is_vector: bool,
18
21
  size: int,
19
22
  ):
20
- mutation = gql(
21
- """
22
- mutation createTrace($input: TraceCreateInput!) {
23
- traceCreate(input: $input) {
24
- ... on Trace {
25
- id
26
- signalId
27
- signalName
28
- }
29
- }
30
- }
31
- """
32
- )
23
+ mutation = gql(create_trace_mutation)
33
24
  input = {
34
25
  "fileId": file_id,
35
26
  "signalId": signal_id,
@@ -39,7 +30,9 @@ class Files(BaseAction):
39
30
  "size": size,
40
31
  }
41
32
  variables = {"input": input}
42
- result = self.primitive.session.execute(mutation, variable_values=variables)
33
+ result = self.primitive.session.execute(
34
+ mutation, variable_values=variables, get_execution_result=True
35
+ )
43
36
  return result
44
37
 
45
38
  @guard
File without changes
@@ -0,0 +1,11 @@
1
+ create_trace_mutation = """
2
+ mutation createTrace($input: TraceCreateInput!) {
3
+ traceCreate(input: $input) {
4
+ ... on Trace {
5
+ id
6
+ signalId
7
+ signalName
8
+ }
9
+ }
10
+ }
11
+ """
primitive/git/actions.py CHANGED
@@ -1,26 +1,26 @@
1
+ import os
1
2
  from pathlib import Path
3
+
4
+ from gql import gql
5
+ from loguru import logger
6
+
2
7
  from primitive.utils.actions import BaseAction
8
+
3
9
  from ..utils.auth import guard
4
- from loguru import logger
5
- import os
10
+ from .graphql.queries import github_app_token_query
6
11
 
7
12
 
8
13
  class Git(BaseAction):
9
14
  @guard
10
15
  def get_github_access_token(self) -> str:
11
- query = """
12
- query githubAppToken{
13
- githubAppToken {
14
- token
15
- }
16
- }
17
- """
18
-
16
+ query = gql(github_app_token_query)
19
17
  filters = {}
20
18
  variables = {
21
19
  "filters": filters,
22
20
  }
23
- result = self.primitive.session.execute(query, variable_values=variables)
21
+ result = self.primitive.session.execute(
22
+ query, variable_values=variables, get_execution_result=True
23
+ )
24
24
  return result
25
25
 
26
26
  def download_git_repository_at_ref(
primitive/git/commands.py CHANGED
@@ -1,8 +1,10 @@
1
- import click
2
- from pathlib import Path
1
+ import os
3
2
  import typing
3
+ from pathlib import Path
4
+
5
+ import click
6
+
4
7
  from ..utils.printer import print_result
5
- import os
6
8
 
7
9
  if typing.TYPE_CHECKING:
8
10
  from ..client import Primitive
@@ -39,11 +41,10 @@ def download_ref_command(
39
41
  destination: Path = Path.cwd(),
40
42
  ):
41
43
  primitive: Primitive = context.obj.get("PRIMITIVE")
42
- result, message = primitive.git.download_git_repository_at_ref(
44
+ path = primitive.git.download_git_repository_at_ref(
43
45
  git_repo_full_name=git_repo_full_name,
44
46
  git_ref=git_ref,
45
47
  github_access_token=github_access_token,
46
48
  destination=destination,
47
49
  )
48
- fg = "red" if not result else "green"
49
- print_result(message, context=context, fg=fg)
50
+ print_result(message=path, context=context)
File without changes
@@ -0,0 +1,7 @@
1
+ github_app_token_query = """
2
+ query githubAppToken{
3
+ githubAppToken {
4
+ token
5
+ }
6
+ }
7
+ """
@@ -0,0 +1,32 @@
1
+ import base64
2
+ from typing import Tuple
3
+
4
+
5
+ def from_base64(value: str) -> Tuple[str, str]:
6
+ """
7
+ FROM:
8
+ https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/relay/utils.py#L16C1-L40C1
9
+
10
+ Parse the base64 encoded relay value.
11
+
12
+ Args:
13
+ value:
14
+ The value to be parsed
15
+
16
+ Returns:
17
+ A tuple of (TypeName, NodeID).
18
+
19
+ Raises:
20
+ ValueError:
21
+ If the value is not in the expected format
22
+
23
+ """
24
+ try:
25
+ res = base64.b64decode(value.encode()).decode().split(":", 1)
26
+ except Exception as e:
27
+ raise ValueError(str(e)) from e
28
+
29
+ if len(res) != 2:
30
+ raise ValueError(f"{res} expected to contain only 2 items")
31
+
32
+ return res[0], res[1]
@@ -0,0 +1,19 @@
1
+ operation_info_fragment = """
2
+ fragment OperationInfoFragment on OperationInfo {
3
+ messages {
4
+ kind
5
+ message
6
+ field
7
+ code
8
+ }
9
+ }
10
+ """
11
+
12
+ page_info_fragment = """
13
+ fragment PageInfoFragment on PageInfo {
14
+ hasNextPage
15
+ hasPreviousPage
16
+ startCursor
17
+ endCursor
18
+ }
19
+ """
File without changes