primitive 0.1.60__py3-none-any.whl → 0.1.63__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 +1 -1
- primitive/agent/actions.py +12 -1
- primitive/agent/runner.py +2 -1
- primitive/agent/uploader.py +20 -22
- primitive/client.py +13 -10
- primitive/exec/actions.py +26 -1
- primitive/exec/commands.py +1 -1
- primitive/exec/interactive.py +99 -0
- primitive/files/actions.py +125 -17
- primitive/files/commands.py +17 -5
- primitive/files/graphql/mutations.py +27 -6
- primitive/hardware/actions.py +1 -1
- primitive/hardware/graphql/fragments.py +2 -0
- primitive/provisioning/__init__.py +0 -0
- primitive/provisioning/actions.py +62 -0
- primitive/provisioning/graphql/__init__.py +0 -0
- primitive/provisioning/graphql/queries.py +5 -0
- primitive/reservations/graphql/fragments.py +1 -1
- primitive/sim/actions.py +5 -9
- primitive/utils/verible.py +3 -3
- {primitive-0.1.60.dist-info → primitive-0.1.63.dist-info}/METADATA +3 -3
- {primitive-0.1.60.dist-info → primitive-0.1.63.dist-info}/RECORD +25 -20
- {primitive-0.1.60.dist-info → primitive-0.1.63.dist-info}/WHEEL +1 -1
- {primitive-0.1.60.dist-info → primitive-0.1.63.dist-info}/entry_points.txt +0 -0
- {primitive-0.1.60.dist-info → primitive-0.1.63.dist-info}/licenses/LICENSE.txt +0 -0
    
        primitive/__about__.py
    CHANGED
    
    
    
        primitive/agent/actions.py
    CHANGED
    
    | @@ -20,7 +20,7 @@ class Agent(BaseAction): | |
| 20 20 | 
             
                    logger.info(" [*] primitive")
         | 
| 21 21 | 
             
                    logger.info(f" [*] Version: {__version__}")
         | 
| 22 22 |  | 
| 23 | 
            -
                    # Create cache dir if it  | 
| 23 | 
            +
                    # Create cache dir if it doesn't exist
         | 
| 24 24 | 
             
                    cache_dir = get_sources_cache()
         | 
| 25 25 |  | 
| 26 26 | 
             
                    # Create uploader
         | 
| @@ -53,6 +53,11 @@ class Agent(BaseAction): | |
| 53 53 | 
             
                                    logger.debug("Active Reservation:")
         | 
| 54 54 | 
             
                                    logger.debug(f"Node ID: {active_reservation_id}")
         | 
| 55 55 | 
             
                                    logger.debug(f"PK: {active_reservation_pk}")
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                                    logger.debug("Running pre provisioning steps for reservation.")
         | 
| 58 | 
            +
                                    self.primitive.provisioning.add_reservation_authorized_keys(
         | 
| 59 | 
            +
                                        reservation_id=active_reservation_id
         | 
| 60 | 
            +
                                    )
         | 
| 56 61 | 
             
                            else:
         | 
| 57 62 | 
             
                                if (
         | 
| 58 63 | 
             
                                    hardware["activeReservation"] is None
         | 
| @@ -62,6 +67,12 @@ class Agent(BaseAction): | |
| 62 67 | 
             
                                    logger.debug("Previous Reservation is Complete:")
         | 
| 63 68 | 
             
                                    logger.debug(f"Node ID: {active_reservation_id}")
         | 
| 64 69 | 
             
                                    logger.debug(f"PK: {active_reservation_pk}")
         | 
| 70 | 
            +
                                    logger.debug(
         | 
| 71 | 
            +
                                        "Running cleanup provisioning steps for reservation."
         | 
| 72 | 
            +
                                    )
         | 
| 73 | 
            +
                                    self.primitive.provisioning.remove_reservation_authorized_keys(
         | 
| 74 | 
            +
                                        reservation_id=active_reservation_id
         | 
| 75 | 
            +
                                    )
         | 
| 65 76 | 
             
                                    active_reservation_id = None
         | 
| 66 77 | 
             
                                    active_reservation_pk = None
         | 
| 67 78 |  | 
    
        primitive/agent/runner.py
    CHANGED
    
    | @@ -191,7 +191,8 @@ class AgentRunner: | |
| 191 191 | 
             
                        logger.remove(self.logger_handle)
         | 
| 192 192 |  | 
| 193 193 | 
             
                    self.logger_handle = logger.add(
         | 
| 194 | 
            -
                        Path(self.logs_dir / f"{label}_{{time}}.primitive.log"), | 
| 194 | 
            +
                        Path(self.logs_dir / f"{label}_{{time}}.primitive.log"),
         | 
| 195 | 
            +
                        rotation=self.max_log_size,
         | 
| 195 196 | 
             
                    )
         | 
| 196 197 |  | 
| 197 198 | 
             
                def provision(self) -> Optional[Dict]:
         | 
    
        primitive/agent/uploader.py
    CHANGED
    
    | @@ -2,8 +2,10 @@ import typing | |
| 2 2 | 
             
            from typing import Dict
         | 
| 3 3 | 
             
            import shutil
         | 
| 4 4 | 
             
            import os
         | 
| 5 | 
            -
            from loguru import logger
         | 
| 6 5 | 
             
            from pathlib import Path, PurePath
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            from loguru import logger
         | 
| 8 | 
            +
             | 
| 7 9 | 
             
            from ..utils.cache import get_artifacts_cache, get_logs_cache
         | 
| 8 10 |  | 
| 9 11 | 
             
            if typing.TYPE_CHECKING:
         | 
| @@ -17,12 +19,6 @@ class Uploader: | |
| 17 19 | 
             
                ):
         | 
| 18 20 | 
             
                    self.primitive = primitive
         | 
| 19 21 |  | 
| 20 | 
            -
                def upload_file(self, path: Path, prefix: str, job_run_id: str) -> str:
         | 
| 21 | 
            -
                    file_upload_response = self.primitive.files.file_upload(
         | 
| 22 | 
            -
                        path, key_prefix=prefix, job_run_id=job_run_id
         | 
| 23 | 
            -
                    )
         | 
| 24 | 
            -
                    return file_upload_response.json()["data"]["fileUpload"]["id"]
         | 
| 25 | 
            -
                
         | 
| 26 22 | 
             
                def upload_dir(self, cache: Path) -> Dict:
         | 
| 27 23 | 
             
                    file_ids = []
         | 
| 28 24 | 
             
                    job_run_id = cache.name
         | 
| @@ -49,11 +45,12 @@ class Uploader: | |
| 49 45 | 
             
                        )
         | 
| 50 46 |  | 
| 51 47 | 
             
                    for file in files:
         | 
| 52 | 
            -
                         | 
| 53 | 
            -
                            file,
         | 
| 54 | 
            -
                             | 
| 48 | 
            +
                        response = self.primitive.files.upload_file_via_api(
         | 
| 49 | 
            +
                            path=file,
         | 
| 50 | 
            +
                            key_prefix=str(PurePath(file).relative_to(cache.parent).parent),
         | 
| 55 51 | 
             
                            job_run_id=job_run_id,
         | 
| 56 52 | 
             
                        )
         | 
| 53 | 
            +
                        upload_id = response.json()["data"]["fileUpload"]["id"]
         | 
| 57 54 |  | 
| 58 55 | 
             
                        if upload_id:
         | 
| 59 56 | 
             
                            file_ids.append(upload_id)
         | 
| @@ -66,23 +63,24 @@ class Uploader: | |
| 66 63 |  | 
| 67 64 | 
             
                    return {job_run_id: file_ids}
         | 
| 68 65 |  | 
| 69 | 
            -
             | 
| 70 66 | 
             
                def scan(self) -> None:
         | 
| 71 67 | 
             
                    # Scan artifacts directory
         | 
| 72 68 | 
             
                    artifacts_dir = get_artifacts_cache()
         | 
| 73 69 | 
             
                    logs_dir = get_logs_cache()
         | 
| 74 70 |  | 
| 75 | 
            -
                    artifacts = sorted( | 
| 76 | 
            -
                         | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 71 | 
            +
                    artifacts = sorted(
         | 
| 72 | 
            +
                        [
         | 
| 73 | 
            +
                            artifacts_cache
         | 
| 74 | 
            +
                            for artifacts_cache in artifacts_dir.iterdir()
         | 
| 75 | 
            +
                            if artifacts_cache.is_dir()
         | 
| 76 | 
            +
                        ],
         | 
| 77 | 
            +
                        key=lambda p: p.stat().st_ctime,
         | 
| 78 | 
            +
                    )
         | 
| 80 79 |  | 
| 81 | 
            -
                    logs = sorted( | 
| 82 | 
            -
                        logs_cache 
         | 
| 83 | 
            -
                         | 
| 84 | 
            -
             | 
| 85 | 
            -
                    ], key=lambda p: p.stat().st_ctime)
         | 
| 80 | 
            +
                    logs = sorted(
         | 
| 81 | 
            +
                        [logs_cache for logs_cache in logs_dir.iterdir() if logs_cache.is_dir()],
         | 
| 82 | 
            +
                        key=lambda p: p.stat().st_ctime,
         | 
| 83 | 
            +
                    )
         | 
| 86 84 |  | 
| 87 85 | 
             
                    log_files = {
         | 
| 88 86 | 
             
                        job_id: files
         | 
| @@ -97,7 +95,7 @@ class Uploader: | |
| 97 95 | 
             
                    }
         | 
| 98 96 |  | 
| 99 97 | 
             
                    files_by_id = {
         | 
| 100 | 
            -
                        job_id: log_files.get(job_id, []) + artifact_files.get(job_id, []) | 
| 98 | 
            +
                        job_id: log_files.get(job_id, []) + artifact_files.get(job_id, [])
         | 
| 101 99 | 
             
                        for job_id in log_files.keys() | artifact_files.keys()
         | 
| 102 100 | 
             
                    }
         | 
| 103 101 |  | 
    
        primitive/client.py
    CHANGED
    
    | @@ -1,20 +1,22 @@ | |
| 1 1 | 
             
            import sys
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from loguru import logger
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            from .agent.actions import Agent
         | 
| 2 6 | 
             
            from .auth.actions import Auth
         | 
| 3 | 
            -
            from . | 
| 4 | 
            -
            from . | 
| 7 | 
            +
            from .daemons.actions import Daemons
         | 
| 8 | 
            +
            from .exec.actions import Exec
         | 
| 5 9 | 
             
            from .files.actions import Files
         | 
| 6 | 
            -
            from .sim.actions import Sim
         | 
| 7 | 
            -
            from .hardware.actions import Hardware
         | 
| 8 | 
            -
            from .lint.actions import Lint
         | 
| 9 | 
            -
            from .agent.actions import Agent
         | 
| 10 10 | 
             
            from .git.actions import Git
         | 
| 11 | 
            -
            from . | 
| 11 | 
            +
            from .hardware.actions import Hardware
         | 
| 12 12 | 
             
            from .jobs.actions import Jobs
         | 
| 13 | 
            +
            from .lint.actions import Lint
         | 
| 13 14 | 
             
            from .organizations.actions import Organizations
         | 
| 14 | 
            -
            from . | 
| 15 | 
            +
            from .projects.actions import Projects
         | 
| 16 | 
            +
            from .provisioning.actions import Provisioning
         | 
| 15 17 | 
             
            from .reservations.actions import Reservations
         | 
| 16 | 
            -
             | 
| 17 | 
            -
            from  | 
| 18 | 
            +
            from .sim.actions import Sim
         | 
| 19 | 
            +
            from .utils.config import read_config_file
         | 
| 18 20 |  | 
| 19 21 | 
             
            logger.disable("primitive")
         | 
| 20 22 |  | 
| @@ -67,6 +69,7 @@ class Primitive: | |
| 67 69 | 
             
                    self.git: Git = Git(self)
         | 
| 68 70 | 
             
                    self.daemons: Daemons = Daemons(self)
         | 
| 69 71 | 
             
                    self.exec: Exec = Exec(self)
         | 
| 72 | 
            +
                    self.provisioning: Provisioning = Provisioning(self)
         | 
| 70 73 |  | 
| 71 74 | 
             
                def get_host_config(self):
         | 
| 72 75 | 
             
                    self.full_config = read_config_file()
         | 
    
        primitive/exec/actions.py
    CHANGED
    
    | @@ -1,9 +1,13 @@ | |
| 1 1 | 
             
            import typing
         | 
| 2 2 |  | 
| 3 | 
            +
            from primitive.exec.interactive import interactive_shell
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            if typing.TYPE_CHECKING:
         | 
| 4 6 | 
             
                pass
         | 
| 5 7 |  | 
| 6 8 |  | 
| 9 | 
            +
            from paramiko import SSHClient
         | 
| 10 | 
            +
             | 
| 7 11 | 
             
            from primitive.utils.actions import BaseAction
         | 
| 8 12 |  | 
| 9 13 |  | 
| @@ -41,7 +45,28 @@ class Exec(BaseAction): | |
| 41 45 | 
             
                        reservation_id=reservation["id"], desired_status="in_progress"
         | 
| 42 46 | 
             
                    )
         | 
| 43 47 |  | 
| 44 | 
            -
                     | 
| 48 | 
            +
                    ssh_hostname = hardware["hostname"]
         | 
| 49 | 
            +
                    ssh_username = hardware["ssh_username"]
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    ssh_client = SSHClient()
         | 
| 52 | 
            +
                    ssh_client.load_system_host_keys()
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    ssh_client.connect(
         | 
| 55 | 
            +
                        hostname=ssh_hostname,
         | 
| 56 | 
            +
                        username=ssh_username,
         | 
| 57 | 
            +
                    )
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    if command:
         | 
| 60 | 
            +
                        formatted_command = " ".join(command)
         | 
| 61 | 
            +
                        stdin, stdout, stderr = ssh_client.exec_command(formatted_command)
         | 
| 62 | 
            +
                        print(stdout.read())
         | 
| 63 | 
            +
                        ssh_client.close()
         | 
| 64 | 
            +
                    else:
         | 
| 65 | 
            +
                        channel = ssh_client.get_transport().open_session()
         | 
| 66 | 
            +
                        channel.get_pty()
         | 
| 67 | 
            +
                        channel.invoke_shell()
         | 
| 68 | 
            +
                        interactive_shell(channel)
         | 
| 69 | 
            +
                        ssh_client.close()
         | 
| 45 70 |  | 
| 46 71 | 
             
                    if created_reservation_on_behalf_of_user:
         | 
| 47 72 | 
             
                        print("Cleaning up reservation.")
         | 
    
        primitive/exec/commands.py
    CHANGED
    
    | @@ -13,7 +13,7 @@ if typing.TYPE_CHECKING: | |
| 13 13 | 
             
                type=str,
         | 
| 14 14 | 
             
                required=True,
         | 
| 15 15 | 
             
            )
         | 
| 16 | 
            -
            @click.argument("command", nargs=-1, required= | 
| 16 | 
            +
            @click.argument("command", nargs=-1, required=False)
         | 
| 17 17 | 
             
            def cli(context, hardware_identifier: str, command: str) -> None:
         | 
| 18 18 | 
             
                """Exec"""
         | 
| 19 19 | 
             
                primitive: Primitive = context.obj.get("PRIMITIVE")
         | 
| @@ -0,0 +1,99 @@ | |
| 1 | 
            +
            # https://github.com/paramiko/paramiko/blob/main/demos/interactive.py
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import socket
         | 
| 4 | 
            +
            import sys
         | 
| 5 | 
            +
             | 
| 6 | 
            +
             | 
| 7 | 
            +
            def _to_unicode(s):
         | 
| 8 | 
            +
                """
         | 
| 9 | 
            +
                decode a string as ascii or utf8 if possible (as required by the sftp
         | 
| 10 | 
            +
                protocol).  if neither works, just return a byte string because the server
         | 
| 11 | 
            +
                probably doesn't know the filename's encoding.
         | 
| 12 | 
            +
                """
         | 
| 13 | 
            +
                try:
         | 
| 14 | 
            +
                    return s.encode("ascii")
         | 
| 15 | 
            +
                except (UnicodeError, AttributeError):
         | 
| 16 | 
            +
                    try:
         | 
| 17 | 
            +
                        return s.decode("utf-8")
         | 
| 18 | 
            +
                    except UnicodeError:
         | 
| 19 | 
            +
                        return s
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
            # windows does not have termios...
         | 
| 23 | 
            +
            try:
         | 
| 24 | 
            +
                import termios
         | 
| 25 | 
            +
                import tty
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                has_termios = True
         | 
| 28 | 
            +
            except ImportError:
         | 
| 29 | 
            +
                has_termios = False
         | 
| 30 | 
            +
             | 
| 31 | 
            +
             | 
| 32 | 
            +
            def interactive_shell(chan):
         | 
| 33 | 
            +
                if has_termios:
         | 
| 34 | 
            +
                    posix_shell(chan)
         | 
| 35 | 
            +
                else:
         | 
| 36 | 
            +
                    windows_shell(chan)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
             | 
| 39 | 
            +
            def posix_shell(chan):
         | 
| 40 | 
            +
                import select
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                oldtty = termios.tcgetattr(sys.stdin)
         | 
| 43 | 
            +
                try:
         | 
| 44 | 
            +
                    tty.setraw(sys.stdin.fileno())
         | 
| 45 | 
            +
                    tty.setcbreak(sys.stdin.fileno())
         | 
| 46 | 
            +
                    chan.settimeout(0.0)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    while True:
         | 
| 49 | 
            +
                        r, w, e = select.select([chan, sys.stdin], [], [])
         | 
| 50 | 
            +
                        if chan in r:
         | 
| 51 | 
            +
                            try:
         | 
| 52 | 
            +
                                x = _to_unicode(chan.recv(1024))
         | 
| 53 | 
            +
                                if len(x) == 0:
         | 
| 54 | 
            +
                                    sys.stdout.write("\r\n*** EOF\r\n")
         | 
| 55 | 
            +
                                    break
         | 
| 56 | 
            +
                                sys.stdout.write(x)
         | 
| 57 | 
            +
                                sys.stdout.flush()
         | 
| 58 | 
            +
                            except socket.timeout:
         | 
| 59 | 
            +
                                pass
         | 
| 60 | 
            +
                        if sys.stdin in r:
         | 
| 61 | 
            +
                            x = sys.stdin.read(1)
         | 
| 62 | 
            +
                            if len(x) == 0:
         | 
| 63 | 
            +
                                break
         | 
| 64 | 
            +
                            chan.send(x)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                finally:
         | 
| 67 | 
            +
                    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
             | 
| 70 | 
            +
            # thanks to Mike Looijmans for this code
         | 
| 71 | 
            +
            def windows_shell(chan):
         | 
| 72 | 
            +
                import threading
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                sys.stdout.write(
         | 
| 75 | 
            +
                    "Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n"
         | 
| 76 | 
            +
                )
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def writeall(sock):
         | 
| 79 | 
            +
                    while True:
         | 
| 80 | 
            +
                        data = sock.recv(256)
         | 
| 81 | 
            +
                        if not data:
         | 
| 82 | 
            +
                            sys.stdout.write("\r\n*** EOF ***\r\n\r\n")
         | 
| 83 | 
            +
                            sys.stdout.flush()
         | 
| 84 | 
            +
                            break
         | 
| 85 | 
            +
                        sys.stdout.write(data)
         | 
| 86 | 
            +
                        sys.stdout.flush()
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                writer = threading.Thread(target=writeall, args=(chan,))
         | 
| 89 | 
            +
                writer.start()
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                try:
         | 
| 92 | 
            +
                    while True:
         | 
| 93 | 
            +
                        d = sys.stdin.read(1)
         | 
| 94 | 
            +
                        if not d:
         | 
| 95 | 
            +
                            break
         | 
| 96 | 
            +
                        chan.send(d)
         | 
| 97 | 
            +
                except EOFError:
         | 
| 98 | 
            +
                    # user hit ^Z or F6
         | 
| 99 | 
            +
                    pass
         | 
    
        primitive/files/actions.py
    CHANGED
    
    | @@ -1,34 +1,85 @@ | |
| 1 | 
            +
            import hashlib
         | 
| 2 | 
            +
            import sys
         | 
| 3 | 
            +
            import threading
         | 
| 1 4 | 
             
            from pathlib import Path
         | 
| 5 | 
            +
            from typing import Dict, Optional
         | 
| 2 6 |  | 
| 7 | 
            +
            import requests
         | 
| 3 8 | 
             
            from gql import gql
         | 
| 9 | 
            +
            from loguru import logger
         | 
| 4 10 |  | 
| 5 11 | 
             
            from primitive.graphql.sdk import create_requests_session
         | 
| 6 12 | 
             
            from primitive.utils.actions import BaseAction
         | 
| 7 13 |  | 
| 8 14 | 
             
            from ..utils.auth import guard
         | 
| 9 | 
            -
            from .graphql.mutations import  | 
| 15 | 
            +
            from .graphql.mutations import (
         | 
| 16 | 
            +
                file_update_mutation,
         | 
| 17 | 
            +
                pending_file_create_mutation,
         | 
| 18 | 
            +
            )
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
            # this class can be used in multithreaded S3 client uploader
         | 
| 22 | 
            +
            # this requires getting an S3 access token to this machine however
         | 
| 23 | 
            +
            # we are using presigned urls instead at this time Oct 29th, 2024
         | 
| 24 | 
            +
            class ProgressPercentage(object):
         | 
| 25 | 
            +
                def __init__(self, filepath: Path) -> None:
         | 
| 26 | 
            +
                    self._filename = filepath.name
         | 
| 27 | 
            +
                    self._size = float(filepath.stat().st_size)
         | 
| 28 | 
            +
                    self._seen_so_far = 0
         | 
| 29 | 
            +
                    self._lock = threading.Lock()
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def __call__(self, bytes_amount):
         | 
| 32 | 
            +
                    # To simplify, assume this is hooked up to a single filename
         | 
| 33 | 
            +
                    with self._lock:
         | 
| 34 | 
            +
                        self._seen_so_far += bytes_amount
         | 
| 35 | 
            +
                        percentage = (self._seen_so_far / self._size) * 100
         | 
| 36 | 
            +
                        sys.stdout.write(
         | 
| 37 | 
            +
                            "\r%s  %s / %s  (%.2f%%)"
         | 
| 38 | 
            +
                            % (self._filename, self._seen_so_far, self._size, percentage)
         | 
| 39 | 
            +
                        )
         | 
| 40 | 
            +
                        sys.stdout.flush()
         | 
| 10 41 |  | 
| 11 42 |  | 
| 12 43 | 
             
            class Files(BaseAction):
         | 
| 13 | 
            -
                 | 
| 14 | 
            -
                def trace_create(
         | 
| 44 | 
            +
                def _pending_file_create(
         | 
| 15 45 | 
             
                    self,
         | 
| 16 | 
            -
                     | 
| 17 | 
            -
                     | 
| 18 | 
            -
                     | 
| 19 | 
            -
                     | 
| 20 | 
            -
                     | 
| 21 | 
            -
                     | 
| 46 | 
            +
                    file_name: str,
         | 
| 47 | 
            +
                    file_size: int,
         | 
| 48 | 
            +
                    file_checksum: str,
         | 
| 49 | 
            +
                    file_path: str,
         | 
| 50 | 
            +
                    key_prefix: str,
         | 
| 51 | 
            +
                    is_public: bool = False,
         | 
| 22 52 | 
             
                ):
         | 
| 23 | 
            -
                    mutation = gql( | 
| 53 | 
            +
                    mutation = gql(pending_file_create_mutation)
         | 
| 24 54 | 
             
                    input = {
         | 
| 25 | 
            -
                        " | 
| 26 | 
            -
                        " | 
| 27 | 
            -
                        " | 
| 28 | 
            -
                        " | 
| 29 | 
            -
                        " | 
| 30 | 
            -
                        " | 
| 55 | 
            +
                        "filePath": file_path,
         | 
| 56 | 
            +
                        "fileName": file_name,
         | 
| 57 | 
            +
                        "fileSize": file_size,
         | 
| 58 | 
            +
                        "fileChecksum": file_checksum,
         | 
| 59 | 
            +
                        "keyPrefix": key_prefix,
         | 
| 60 | 
            +
                        "isPublic": is_public,
         | 
| 61 | 
            +
                    }
         | 
| 62 | 
            +
                    variables = {"input": input}
         | 
| 63 | 
            +
                    result = self.primitive.session.execute(
         | 
| 64 | 
            +
                        mutation, variable_values=variables, get_execution_result=True
         | 
| 65 | 
            +
                    )
         | 
| 66 | 
            +
                    return result.data.get("pendingFileCreate")
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def _update_file_status(
         | 
| 69 | 
            +
                    self,
         | 
| 70 | 
            +
                    file_id: str,
         | 
| 71 | 
            +
                    is_uploading: Optional[bool] = None,
         | 
| 72 | 
            +
                    is_complete: Optional[bool] = None,
         | 
| 73 | 
            +
                ):
         | 
| 74 | 
            +
                    mutation = gql(file_update_mutation)
         | 
| 75 | 
            +
                    input: Dict[str, str | bool] = {
         | 
| 76 | 
            +
                        "id": file_id,
         | 
| 31 77 | 
             
                    }
         | 
| 78 | 
            +
                    if is_uploading is not None:
         | 
| 79 | 
            +
                        input["isUploading"] = is_uploading
         | 
| 80 | 
            +
                    if is_complete is not None:
         | 
| 81 | 
            +
                        input["isComplete"] = is_complete
         | 
| 82 | 
            +
             | 
| 32 83 | 
             
                    variables = {"input": input}
         | 
| 33 84 | 
             
                    result = self.primitive.session.execute(
         | 
| 34 85 | 
             
                        mutation, variable_values=variables, get_execution_result=True
         | 
| @@ -36,13 +87,70 @@ class Files(BaseAction): | |
| 36 87 | 
             
                    return result
         | 
| 37 88 |  | 
| 38 89 | 
             
                @guard
         | 
| 39 | 
            -
                def  | 
| 90 | 
            +
                def upload_file_direct(
         | 
| 91 | 
            +
                    self,
         | 
| 92 | 
            +
                    path: Path,
         | 
| 93 | 
            +
                    is_public: False,
         | 
| 94 | 
            +
                    key_prefix: str = "",
         | 
| 95 | 
            +
                    file_id: Optional[str] = None,
         | 
| 96 | 
            +
                ):
         | 
| 97 | 
            +
                    logger.enable("primitive")
         | 
| 98 | 
            +
                    if path.exists() is False:
         | 
| 99 | 
            +
                        raise Exception(f"File {path} does not exist.")
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    file_size = path.stat().st_size
         | 
| 102 | 
            +
                    if file_size == 0:
         | 
| 103 | 
            +
                        raise Exception(f"{path} is empty.")
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    file_checksum = hashlib.md5(path.read_bytes()).hexdigest()
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    if not file_id:
         | 
| 108 | 
            +
                        pending_file_create = self._pending_file_create(
         | 
| 109 | 
            +
                            file_name=path.name,
         | 
| 110 | 
            +
                            file_size=path.stat().st_size,
         | 
| 111 | 
            +
                            file_checksum=file_checksum,
         | 
| 112 | 
            +
                            file_path=str(path),
         | 
| 113 | 
            +
                            key_prefix=key_prefix,
         | 
| 114 | 
            +
                            is_public=is_public,
         | 
| 115 | 
            +
                        )
         | 
| 116 | 
            +
                    file_id = pending_file_create.get("id")
         | 
| 117 | 
            +
                    presigned_url = pending_file_create.get("presignedUrlForUpload")
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    if not file_id:
         | 
| 120 | 
            +
                        raise Exception("No file_id found or provided.")
         | 
| 121 | 
            +
                    if not presigned_url:
         | 
| 122 | 
            +
                        raise Exception("No presigned_url returned.")
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    self._update_file_status(file_id, is_uploading=True)
         | 
| 125 | 
            +
                    with open(path, "rb") as object_file:
         | 
| 126 | 
            +
                        object_text = object_file.read()
         | 
| 127 | 
            +
                        response = requests.put(presigned_url, data=object_text)
         | 
| 128 | 
            +
                        if response.ok:
         | 
| 129 | 
            +
                            logger.info(f"File {path} uploaded successfully.")
         | 
| 130 | 
            +
                            update_file_status_result = self._update_file_status(
         | 
| 131 | 
            +
                                file_id, is_uploading=False, is_complete=True
         | 
| 132 | 
            +
                            )
         | 
| 133 | 
            +
                        else:
         | 
| 134 | 
            +
                            message = f"Failed to upload file {path}. {response.status_code}: {response.text}"
         | 
| 135 | 
            +
                            logger.error(message)
         | 
| 136 | 
            +
                            raise Exception(message)
         | 
| 137 | 
            +
                    file_pk = update_file_status_result.data.get("fileUpdate").get("pk")
         | 
| 138 | 
            +
                    file_access_url = f"{self.primitive.host_config.get("transport")}://{self.primitive.host}/files/{file_pk}/presigned-url/"
         | 
| 139 | 
            +
                    logger.info(f"Available at: {file_access_url}")
         | 
| 140 | 
            +
                    return update_file_status_result
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                @guard
         | 
| 143 | 
            +
                def upload_file_via_api(
         | 
| 40 144 | 
             
                    self,
         | 
| 41 145 | 
             
                    path: Path,
         | 
| 42 146 | 
             
                    is_public: bool = False,
         | 
| 43 147 | 
             
                    key_prefix: str = "",
         | 
| 44 148 | 
             
                    job_run_id: str = "",
         | 
| 45 149 | 
             
                ):
         | 
| 150 | 
            +
                    """
         | 
| 151 | 
            +
                    This method uploads a file via the Primitive API.
         | 
| 152 | 
            +
                    This does NOT upload the file straight to S3
         | 
| 153 | 
            +
                    """
         | 
| 46 154 | 
             
                    file_path = str(path.resolve())
         | 
| 47 155 | 
             
                    if path.exists() is False:
         | 
| 48 156 | 
             
                        raise FileNotFoundError(f"File not found at {file_path}")
         | 
    
        primitive/files/commands.py
    CHANGED
    
    | @@ -1,7 +1,10 @@ | |
| 1 | 
            +
            import json
         | 
| 2 | 
            +
            import typing
         | 
| 3 | 
            +
            from pathlib import Path
         | 
| 4 | 
            +
             | 
| 1 5 | 
             
            import click
         | 
| 6 | 
            +
             | 
| 2 7 | 
             
            from ..utils.printer import print_result
         | 
| 3 | 
            -
            from pathlib import Path
         | 
| 4 | 
            -
            import typing
         | 
| 5 8 |  | 
| 6 9 | 
             
            if typing.TYPE_CHECKING:
         | 
| 7 10 | 
             
                from ..client import Primitive
         | 
| @@ -19,10 +22,19 @@ def cli(context): | |
| 19 22 | 
             
            @click.argument("path", type=click.Path(exists=True))
         | 
| 20 23 | 
             
            @click.option("--public", "-p", help="Is this a Public file", is_flag=True)
         | 
| 21 24 | 
             
            @click.option("--key-prefix", "-k", help="Key Prefix", default="")
         | 
| 22 | 
            -
             | 
| 25 | 
            +
            @click.option("--direct", "-k", help="direct", is_flag=True)
         | 
| 26 | 
            +
            def file_upload_command(context, path, public, key_prefix, direct):
         | 
| 23 27 | 
             
                """File Upload"""
         | 
| 24 28 | 
             
                primitive: Primitive = context.obj.get("PRIMITIVE")
         | 
| 25 29 | 
             
                path = Path(path)
         | 
| 26 | 
            -
                 | 
| 27 | 
            -
             | 
| 30 | 
            +
                if direct:
         | 
| 31 | 
            +
                    result = primitive.files.upload_file_direct(
         | 
| 32 | 
            +
                        path, is_public=public, key_prefix=key_prefix
         | 
| 33 | 
            +
                    )
         | 
| 34 | 
            +
                else:
         | 
| 35 | 
            +
                    result = primitive.files.upload_file_via_api(
         | 
| 36 | 
            +
                        path, is_public=public, key_prefix=key_prefix
         | 
| 37 | 
            +
                    )
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                message = json.dumps(result.data)
         | 
| 28 40 | 
             
                print_result(message=message, context=context)
         | 
| @@ -1,11 +1,32 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 1 | 
            +
            from primitive.graphql.utility_fragments import operation_info_fragment
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            file_update_mutation = (
         | 
| 4 | 
            +
                operation_info_fragment
         | 
| 5 | 
            +
                + """
         | 
| 6 | 
            +
            mutation fileUpdate($input: FileInputPartial!) {
         | 
| 7 | 
            +
                fileUpdate(input: $input) {
         | 
| 8 | 
            +
                    ... on File {
         | 
| 5 9 | 
             
                        id
         | 
| 6 | 
            -
                         | 
| 7 | 
            -
                        signalName
         | 
| 10 | 
            +
                        pk
         | 
| 8 11 | 
             
                    }
         | 
| 12 | 
            +
                    ...OperationInfoFragment
         | 
| 9 13 | 
             
                }
         | 
| 10 14 | 
             
            }
         | 
| 11 15 | 
             
            """
         | 
| 16 | 
            +
            )
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            pending_file_create_mutation = (
         | 
| 19 | 
            +
                operation_info_fragment
         | 
| 20 | 
            +
                + """
         | 
| 21 | 
            +
            mutation pendingFileCreate($input: PendingFileCreateInput!) {
         | 
| 22 | 
            +
                pendingFileCreate(input: $input) {
         | 
| 23 | 
            +
                    ... on File {
         | 
| 24 | 
            +
                        id
         | 
| 25 | 
            +
                        pk
         | 
| 26 | 
            +
                        presignedUrlForUpload
         | 
| 27 | 
            +
                    }
         | 
| 28 | 
            +
                    ...OperationInfoFragment
         | 
| 29 | 
            +
                }
         | 
| 30 | 
            +
            }
         | 
| 31 | 
            +
            """
         | 
| 32 | 
            +
            )
         | 
    
        primitive/hardware/actions.py
    CHANGED
    
    
| 
            File without changes
         | 
| @@ -0,0 +1,62 @@ | |
| 1 | 
            +
            from pathlib import Path
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            from gql import gql
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            from primitive.utils.actions import BaseAction
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            from ..utils.auth import guard
         | 
| 8 | 
            +
            from .graphql.queries import authorized_keys_query
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            HOME_DIRECTORY = Path.home()
         | 
| 11 | 
            +
            AUTHORIZED_KEYS_FILEPATH = Path(HOME_DIRECTORY / ".ssh" / "authorized_keys")
         | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
            class Provisioning(BaseAction):
         | 
| 15 | 
            +
                @guard
         | 
| 16 | 
            +
                def get_authorized_keys(self, reservation_id: str) -> str:
         | 
| 17 | 
            +
                    variables = {
         | 
| 18 | 
            +
                        "reservationId": reservation_id,
         | 
| 19 | 
            +
                    }
         | 
| 20 | 
            +
                    query = gql(authorized_keys_query)
         | 
| 21 | 
            +
                    result = self.primitive.session.execute(
         | 
| 22 | 
            +
                        query, variable_values=variables, get_execution_result=True
         | 
| 23 | 
            +
                    )
         | 
| 24 | 
            +
                    return result.data["authorizedKeys"]
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def add_reservation_authorized_keys(self, reservation_id: str) -> None:
         | 
| 27 | 
            +
                    AUTHORIZED_KEYS_BACKUP_FILEPATH = Path(
         | 
| 28 | 
            +
                        HOME_DIRECTORY / ".ssh" / f"authorized_keys.bak-{reservation_id}"
         | 
| 29 | 
            +
                    )
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    if AUTHORIZED_KEYS_FILEPATH.exists():
         | 
| 32 | 
            +
                        AUTHORIZED_KEYS_BACKUP_FILEPATH.write_text(
         | 
| 33 | 
            +
                            AUTHORIZED_KEYS_FILEPATH.read_text()
         | 
| 34 | 
            +
                        )
         | 
| 35 | 
            +
                    else:
         | 
| 36 | 
            +
                        AUTHORIZED_KEYS_FILEPATH.touch()
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    authorized_keys = self.get_authorized_keys(reservation_id=reservation_id)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    AUTHORIZED_KEYS_FILEPATH.write_text(
         | 
| 41 | 
            +
                        AUTHORIZED_KEYS_FILEPATH.read_text()
         | 
| 42 | 
            +
                        + f"\n## START PRIMITIVE SSH PUBLIC KEYS FOR RESERVATION ID {reservation_id}\n"
         | 
| 43 | 
            +
                        + authorized_keys
         | 
| 44 | 
            +
                        + f"\n## END PRIMITIVE SSH PUBLIC KEYS FOR RESERVATION ID {reservation_id}\n"
         | 
| 45 | 
            +
                    )
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # self.primitive.sshd.reload()
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def remove_reservation_authorized_keys(self, reservation_id: str) -> None:
         | 
| 50 | 
            +
                    AUTHORIZED_KEYS_BACKUP_FILEPATH = Path(
         | 
| 51 | 
            +
                        HOME_DIRECTORY / ".ssh" / f"authorized_keys.bak-{reservation_id}"
         | 
| 52 | 
            +
                    )
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    if AUTHORIZED_KEYS_BACKUP_FILEPATH.exists():
         | 
| 55 | 
            +
                        AUTHORIZED_KEYS_FILEPATH.write_text(
         | 
| 56 | 
            +
                            AUTHORIZED_KEYS_BACKUP_FILEPATH.read_text()
         | 
| 57 | 
            +
                        )
         | 
| 58 | 
            +
                        AUTHORIZED_KEYS_BACKUP_FILEPATH.unlink()
         | 
| 59 | 
            +
                    else:
         | 
| 60 | 
            +
                        AUTHORIZED_KEYS_FILEPATH.unlink()
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    # self.primitive.sshd.reload()
         | 
| 
            File without changes
         | 
    
        primitive/sim/actions.py
    CHANGED
    
    | @@ -46,10 +46,6 @@ class Sim(BaseAction): | |
| 46 46 |  | 
| 47 47 | 
             
                    return True, message
         | 
| 48 48 |  | 
| 49 | 
            -
                def upload_file(self, path: Path, prefix: str) -> str:
         | 
| 50 | 
            -
                    file_upload_response = self.primitive.files.file_upload(path, key_prefix=prefix)
         | 
| 51 | 
            -
                    return file_upload_response.json()["data"]["fileUpload"]["id"]
         | 
| 52 | 
            -
             | 
| 53 49 | 
             
                def collect_artifacts(self, source: Path, job_run_id: str) -> None:
         | 
| 54 50 | 
             
                    # Parse VCD artifacts using rust binding
         | 
| 55 51 | 
             
                    # TODO: eventually make this smarter, only parsing VCDs for failed tests
         | 
| @@ -72,12 +68,12 @@ class Sim(BaseAction): | |
| 72 68 | 
             
                    )
         | 
| 73 69 | 
             
                    for file_path in files:
         | 
| 74 70 | 
             
                        try:
         | 
| 75 | 
            -
                             | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
                                    prefix=f"{job_run_id}/{str(PurePath(file_path).relative_to(Path(source)).parent)}",
         | 
| 79 | 
            -
                                )
         | 
| 71 | 
            +
                            key_prefix = f"{job_run_id}/{str(PurePath(file_path).relative_to(Path(source)).parent)}"
         | 
| 72 | 
            +
                            file_upload_response = self.primitive.files.upload_file_via_api(
         | 
| 73 | 
            +
                                file_path, key_prefix=key_prefix
         | 
| 80 74 | 
             
                            )
         | 
| 75 | 
            +
                            file_id = file_upload_response.json()["data"]["fileUpload"]["id"]
         | 
| 76 | 
            +
                            file_ids.append(file_id)
         | 
| 81 77 | 
             
                        except FileNotFoundError:
         | 
| 82 78 | 
             
                            logger.warning(f"{file_path} not found...")
         | 
| 83 79 |  | 
    
        primitive/utils/verible.py
    CHANGED
    
    | @@ -1,10 +1,10 @@ | |
| 1 1 | 
             
            import tarfile
         | 
| 2 | 
            -
            import requests
         | 
| 3 | 
            -
            from .shell import add_path_to_shell
         | 
| 4 | 
            -
            from .cache import get_deps_cache
         | 
| 5 2 |  | 
| 3 | 
            +
            import requests
         | 
| 6 4 | 
             
            from loguru import logger
         | 
| 7 5 |  | 
| 6 | 
            +
            from .cache import get_deps_cache
         | 
| 7 | 
            +
            from .shell import add_path_to_shell
         | 
| 8 8 |  | 
| 9 9 | 
             
            VERIBLE_MAC_OS_LINK = "https://github.com/chipsalliance/verible/releases/download/v0.0-3752-g8b64887e/verible-v0.0-3752-g8b64887e-macOS.tar.gz"
         | 
| 10 10 | 
             
            VERIBLE_WINDOWS_64_OS_LINK = "https://github.com/chipsalliance/verible/releases/download/v0.0-3752-g8b64887e/verible-v0.0-3752-g8b64887e-win64.zip"
         | 
| @@ -1,12 +1,11 @@ | |
| 1 1 | 
             
            Metadata-Version: 2.3
         | 
| 2 2 | 
             
            Name: primitive
         | 
| 3 | 
            -
            Version: 0.1. | 
| 3 | 
            +
            Version: 0.1.63
         | 
| 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
         | 
| 7 7 | 
             
            Author-email: Dylan Stein <dylan@primitive.tech>, Chase Zimmerman <chase@primitive.tech>
         | 
| 8 | 
            -
            License | 
| 9 | 
            -
            License-File: LICENSE.txt
         | 
| 8 | 
            +
            License: MIT
         | 
| 10 9 | 
             
            Classifier: Development Status :: 4 - Beta
         | 
| 11 10 | 
             
            Classifier: Programming Language :: Python
         | 
| 12 11 | 
             
            Classifier: Programming Language :: Python :: 3
         | 
| @@ -21,6 +20,7 @@ Requires-Python: >=3.11 | |
| 21 20 | 
             
            Requires-Dist: click
         | 
| 22 21 | 
             
            Requires-Dist: gql[all]
         | 
| 23 22 | 
             
            Requires-Dist: loguru
         | 
| 23 | 
            +
            Requires-Dist: paramiko[all]
         | 
| 24 24 | 
             
            Requires-Dist: primitive-pal==0.1.4
         | 
| 25 25 | 
             
            Requires-Dist: pyyaml
         | 
| 26 26 | 
             
            Description-Content-Type: text/markdown
         | 
| @@ -1,14 +1,14 @@ | |
| 1 | 
            -
            primitive/__about__.py,sha256= | 
| 1 | 
            +
            primitive/__about__.py,sha256=uiVaG3uarbeydunpM548F3cr2wnnZhtxTkRYjlZVUoU,130
         | 
| 2 2 | 
             
            primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
         | 
| 3 3 | 
             
            primitive/cli.py,sha256=CGmWiqqCLMHtHGOUPuf3tVO6VvChBZ1VdSwCCglnBgA,2582
         | 
| 4 | 
            -
            primitive/client.py,sha256= | 
| 4 | 
            +
            primitive/client.py,sha256=P7cCDperMu3pxlmRAP-H2owM-cj1kRlZWrqujxnWa4o,2473
         | 
| 5 5 | 
             
            primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 6 | 
            -
            primitive/agent/actions.py,sha256= | 
| 6 | 
            +
            primitive/agent/actions.py,sha256=Hosy2o2FntfBtcNqqHuMFq9dm99EVfySy0v2JGeufvc,6474
         | 
| 7 7 | 
             
            primitive/agent/commands.py,sha256=-dVDilELfkGfbZB7qfEPs77Dm1oT62qJj4tsIk4KoxI,254
         | 
| 8 8 | 
             
            primitive/agent/process.py,sha256=LVI-RB4a0YEuXUTYMXKL5Xi9euNwUI2nxj00mv8EFOg,2253
         | 
| 9 9 | 
             
            primitive/agent/provision.py,sha256=rmwnro1K5F8mwtd45XAq7RVQmpDWnbBCQ8X_qgWhm3M,1546
         | 
| 10 | 
            -
            primitive/agent/runner.py,sha256= | 
| 11 | 
            -
            primitive/agent/uploader.py,sha256= | 
| 10 | 
            +
            primitive/agent/runner.py,sha256=xt0Ty-dAc8VatNIZwBZUJbC3mHXLpywpRVVyr_RvVYw,7142
         | 
| 11 | 
            +
            primitive/agent/uploader.py,sha256=W-aXUgKZvcm9LbTXq8su_cgBl_mFrmcFfkkU9t8W04Q,3002
         | 
| 12 12 | 
             
            primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 13 13 | 
             
            primitive/auth/actions.py,sha256=MPsG9LcKcOPwA7gZ9Ewk0PZJhTQvIrGfODdz4GxSzgA,999
         | 
| 14 14 | 
             
            primitive/auth/commands.py,sha256=JahUq0E2e7Xa-FX1WEUv7TgM6ieDvNH4VwRRtxAW7HE,2340
         | 
| @@ -20,13 +20,14 @@ primitive/daemons/commands.py,sha256=-Muh-6ib4uAVtPn_67AcMrDwuCwYlCnRQozCi2Xurmk | |
| 20 20 | 
             
            primitive/daemons/launch_agents.py,sha256=qovt32gwpjGDd82z_SY5EGCUjaUyNA49pZFajZsw3eE,4796
         | 
| 21 21 | 
             
            primitive/daemons/launch_service.py,sha256=FPB9qKEjhllRfEpct0ng2L9lpIaGJbQwn1JdFT8uBA8,5600
         | 
| 22 22 | 
             
            primitive/exec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 23 | 
            -
            primitive/exec/actions.py,sha256= | 
| 24 | 
            -
            primitive/exec/commands.py,sha256= | 
| 23 | 
            +
            primitive/exec/actions.py,sha256=hgUYi_G69SPbMRpVZv8EjeqIy41apXZIIw9lcg37zAY,2633
         | 
| 24 | 
            +
            primitive/exec/commands.py,sha256=66LO2kkJC-ynNZQpUCXv4Ol15QoacdSZAHblePDcmLo,510
         | 
| 25 | 
            +
            primitive/exec/interactive.py,sha256=TscY6s2ZysijidKPheq6y-fCErUVLS0zcdTW8XyFWGI,2435
         | 
| 25 26 | 
             
            primitive/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 26 | 
            -
            primitive/files/actions.py,sha256= | 
| 27 | 
            -
            primitive/files/commands.py,sha256= | 
| 27 | 
            +
            primitive/files/actions.py,sha256=q33aP7UvCFfhhJ8iOnvI57jOpobBUaJspRN4_3vJApU,6675
         | 
| 28 | 
            +
            primitive/files/commands.py,sha256=x1fxixMrZFvYZGeQb3u5ElsbmWXMmYGq0f_zZArGp8Q,1084
         | 
| 28 29 | 
             
            primitive/files/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 29 | 
            -
            primitive/files/graphql/mutations.py,sha256= | 
| 30 | 
            +
            primitive/files/graphql/mutations.py,sha256=SWxq6rwVWhouiuC72--Avpg9vybURFxmxiwkMY6dX7E,642
         | 
| 30 31 | 
             
            primitive/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 31 32 | 
             
            primitive/git/actions.py,sha256=0KHeHViZZqIhF6-Eqvhs0g_UmglqyWrOQKElQCm6jVw,1506
         | 
| 32 33 | 
             
            primitive/git/commands.py,sha256=sCeSjkRgSEjCEsB5seXgB_h6xfk0KpvMvzMKoRfUbRA,1177
         | 
| @@ -37,10 +38,10 @@ primitive/graphql/relay.py,sha256=bmij2AjdpURQ6GGVCxwWhauF-r_SxuAU2oJ4sDbLxpI,72 | |
| 37 38 | 
             
            primitive/graphql/sdk.py,sha256=BhCGmDtc4sNnH8CxbQSJyFwOZ-ZSqMtjsxMB3JRBhPw,1456
         | 
| 38 39 | 
             
            primitive/graphql/utility_fragments.py,sha256=uIjwILC4QtWNyO5vu77VjQf_p0jvP3A9q_6zRq91zqs,303
         | 
| 39 40 | 
             
            primitive/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 40 | 
            -
            primitive/hardware/actions.py,sha256= | 
| 41 | 
            +
            primitive/hardware/actions.py,sha256=jtthNgRyeRD8txt4WqEZskPtsDWU2Yg2gJZLSrEx1io,18603
         | 
| 41 42 | 
             
            primitive/hardware/commands.py,sha256=_HaWOdRQSkhnA1xZZHZWgadSQ9Gijxtnzg2vc_IDSMA,1854
         | 
| 42 43 | 
             
            primitive/hardware/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 43 | 
            -
            primitive/hardware/graphql/fragments.py,sha256= | 
| 44 | 
            +
            primitive/hardware/graphql/fragments.py,sha256=PuqhW42fKUvRqOli5W7nOs2RfJ8FruSQC1gKBp3psBQ,271
         | 
| 44 45 | 
             
            primitive/hardware/graphql/mutations.py,sha256=Zd6HxnIgTJ9mJQAfKJkdeDfstcPAal6Bj38pnKb_RuI,904
         | 
| 45 46 | 
             
            primitive/hardware/graphql/queries.py,sha256=dhihQwr4O7zxDNRjeNWhkAXaSDOBsK-uqIczEGy1XLI,430
         | 
| 46 47 | 
             
            primitive/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| @@ -67,15 +68,19 @@ primitive/projects/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ | |
| 67 68 | 
             
            primitive/projects/graphql/fragments.py,sha256=02F5nyI8i-ML_bV5FFHUgFWM5bBBfjmz_tkP-4QOXjU,127
         | 
| 68 69 | 
             
            primitive/projects/graphql/mutations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 69 70 | 
             
            primitive/projects/graphql/queries.py,sha256=nFaVf6YOHA2L_FTgIUdRK-80hYTmv1a1X5ac7QPMp1k,646
         | 
| 71 | 
            +
            primitive/provisioning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 72 | 
            +
            primitive/provisioning/actions.py,sha256=IYZYAbtomtZtlkqDaBxx4e7PFKGkRNqek_tABH6q_zY,2116
         | 
| 73 | 
            +
            primitive/provisioning/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 74 | 
            +
            primitive/provisioning/graphql/queries.py,sha256=cBtuKa6shoatYZfKSnQoPJP6B8g8y3QhFqJ_pkvMcG0,134
         | 
| 70 75 | 
             
            primitive/reservations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 71 76 | 
             
            primitive/reservations/actions.py,sha256=XjjF0UJAgKryuSJqakLMAWshZIbuM-DkTmdU95cANs4,4434
         | 
| 72 77 | 
             
            primitive/reservations/commands.py,sha256=OwWWE9DrvtrVBcBki0bKTOqCzCQk090c0rPIAt89JLY,2243
         | 
| 73 78 | 
             
            primitive/reservations/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 74 | 
            -
            primitive/reservations/graphql/fragments.py,sha256= | 
| 79 | 
            +
            primitive/reservations/graphql/fragments.py,sha256=_TQfJeHky-Hh3WCHWobQ6-A1lpSvU-YkS0V9cqj2nOU,476
         | 
| 75 80 | 
             
            primitive/reservations/graphql/mutations.py,sha256=IqzwQL7OclN7RpIcidrTQo9cGYofY7wqoBOdnY0pwN8,651
         | 
| 76 81 | 
             
            primitive/reservations/graphql/queries.py,sha256=x31wTRelskX2fc0fx2qrY7XT1q74nvzLv_Xef3o9weg,746
         | 
| 77 82 | 
             
            primitive/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 78 | 
            -
            primitive/sim/actions.py,sha256= | 
| 83 | 
            +
            primitive/sim/actions.py,sha256=oR77UmCp6PxDEuKvoNejeHOG6E5r6uHax3G9OZYoofM,4810
         | 
| 79 84 | 
             
            primitive/sim/commands.py,sha256=8PaOfL1MO6qxTn7mNVRnBU1X2wa3gk_mlbAhBW6MnI0,591
         | 
| 80 85 | 
             
            primitive/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         | 
| 81 86 | 
             
            primitive/utils/actions.py,sha256=HOFrmM3-0A_A3NS84MqrZ6JmQEiiPSoDqEeuu6b_qfQ,196
         | 
| @@ -87,9 +92,9 @@ primitive/utils/git.py,sha256=1qNOu8X-33CavmrD580BmrFhD_WVO9PGWHUUboXJR_g,663 | |
| 87 92 | 
             
            primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
         | 
| 88 93 | 
             
            primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
         | 
| 89 94 | 
             
            primitive/utils/shell.py,sha256=j7E1YwgNWw57dFHVfEbqRNVcPHX0xDefX2vFSNgeI_8,1648
         | 
| 90 | 
            -
            primitive/utils/verible.py,sha256= | 
| 91 | 
            -
            primitive-0.1. | 
| 92 | 
            -
            primitive-0.1. | 
| 93 | 
            -
            primitive-0.1. | 
| 94 | 
            -
            primitive-0.1. | 
| 95 | 
            -
            primitive-0.1. | 
| 95 | 
            +
            primitive/utils/verible.py,sha256=Zb5NUISvcaIgEvgCDBWr-GCoceMa79Tcwvr5Wl9lfnA,2252
         | 
| 96 | 
            +
            primitive-0.1.63.dist-info/METADATA,sha256=o-IZgpjA7lYJYn-sxuRBDA4EXxHA_c57G1U8Q3dcZzE,3774
         | 
| 97 | 
            +
            primitive-0.1.63.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
         | 
| 98 | 
            +
            primitive-0.1.63.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
         | 
| 99 | 
            +
            primitive-0.1.63.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
         | 
| 100 | 
            +
            primitive-0.1.63.dist-info/RECORD,,
         | 
| 
            File without changes
         | 
| 
            File without changes
         |