primitive 0.1.1__py3-none-any.whl → 0.1.4__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 ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@steins.studio>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.1.4"
primitive/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@steins.studio>
2
+ #
3
+ # SPDX-License-Identifier: MIT
File without changes
@@ -0,0 +1,43 @@
1
+ from gql import gql
2
+
3
+ from ..utils.config import read_config_file, update_config_file
4
+
5
+
6
+ from primitive.utils.actions import BaseAction
7
+
8
+
9
+ class Auth(BaseAction):
10
+ def whoami(self):
11
+ query = gql(
12
+ """
13
+ query whoami {
14
+ whoami {
15
+ username
16
+ }
17
+ }
18
+ """
19
+ )
20
+
21
+ result = self.primitive.session.execute(query, get_execution_result=True)
22
+
23
+ return result
24
+
25
+ def setup_config(
26
+ self,
27
+ username: str,
28
+ token: str,
29
+ host: str = "api.primitive.tech",
30
+ transport: str = "https",
31
+ ):
32
+ full_config = read_config_file()
33
+ new_host_config = {
34
+ "username": username,
35
+ "token": token,
36
+ "transport": transport,
37
+ }
38
+
39
+ if existing_host_config := full_config.get(host, None):
40
+ full_config[host] = {**existing_host_config, **new_host_config}
41
+ else:
42
+ full_config[host] = new_host_config
43
+ update_config_file(new_config=full_config)
@@ -0,0 +1,77 @@
1
+ import os
2
+ import webbrowser
3
+ import click
4
+ from .actions import Auth
5
+ from ..utils.config import PRIMITIVE_CREDENTIALS_FILEPATH
6
+ from ..utils.printer import print_result
7
+
8
+ import typing
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from ..client import Primitive
12
+
13
+
14
+ @click.group()
15
+ @click.pass_context
16
+ def cli(context):
17
+ """Authentication"""
18
+ pass
19
+
20
+
21
+ @cli.command("whoami")
22
+ @click.pass_context
23
+ def whoami_command(context):
24
+ """Whoami"""
25
+ primitive: Primitive = context.obj.get("PRIMITIVE")
26
+ result = primitive.auth.whoami()
27
+ if context.obj["DEBUG"] or context.obj["JSON"]:
28
+ message = result.data
29
+ else:
30
+ message = f"Logged in as {result.data['whoami']['username']}"
31
+ print_result(message=message, context=context)
32
+
33
+
34
+ @cli.command("config")
35
+ @click.pass_context
36
+ @click.option(
37
+ "--username",
38
+ prompt=True,
39
+ default=lambda: os.environ.get("PRIMITIVE_USER", ""),
40
+ show_default="current user",
41
+ help="Username for Primitive API",
42
+ )
43
+ @click.option(
44
+ "--transport",
45
+ required=False,
46
+ default="https",
47
+ show_default="https",
48
+ help="Transport protocol for Primitive API",
49
+ )
50
+ def config_command(context, username: str, transport: str):
51
+ """Configure the CLI"""
52
+ token = os.environ.get("PRIMITIVE_TOKEN", "")
53
+ if not token and context.obj.get("YES"):
54
+ raise click.ClickException(
55
+ "PRIMITIVE_TOKEN environment variable is required for non-interactive mode"
56
+ )
57
+ host = context.obj.get("HOST", "api.primitive.tech")
58
+ while not token:
59
+ account_settings_url = (
60
+ f"{transport}://{host.replace('api', 'app')}/account/tokens"
61
+ )
62
+ if "localhost" in host:
63
+ account_settings_url = "http://localhost:3000/account/tokens"
64
+ click.secho(
65
+ f"You can find or create a Primitive API token at {account_settings_url}",
66
+ fg="yellow",
67
+ )
68
+
69
+ webbrowser.open_new_tab(account_settings_url)
70
+ token = click.prompt(
71
+ "Please enter your Primitive API token", hide_input=True, type=str
72
+ )
73
+
74
+ auth = Auth(primitive=None)
75
+ auth.setup_config(username=username, token=token, host=host, transport=transport)
76
+ message = f"Config created at '{PRIMITIVE_CREDENTIALS_FILEPATH}' for user '{username}' on host '{host}'" # noqa
77
+ print_result(message=message, context=context, fg="green")
primitive/cli.py ADDED
@@ -0,0 +1,60 @@
1
+ import os
2
+ import sys
3
+ import click
4
+ from .__about__ import __version__
5
+ from .client import Primitive
6
+ from .auth.commands import config_command, whoami_command
7
+ 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
+
11
+
12
+ @click.group()
13
+ @click.option(
14
+ "--host",
15
+ required=False,
16
+ default=lambda: os.environ.get("PRIMITIVE_HOST", "api.primitive.tech"),
17
+ show_default="api.primitive.tech",
18
+ help="Environment of Primitive API",
19
+ )
20
+ @click.option(
21
+ "--yes", is_flag=True, show_default=True, default=False, help="Skip interactions."
22
+ )
23
+ @click.option(
24
+ "--debug", is_flag=True, show_default=True, default=False, help="Enable debug mode."
25
+ )
26
+ @click.option(
27
+ "--json",
28
+ is_flag=True,
29
+ show_default=True,
30
+ default=False,
31
+ help="Turn all outputs into JSON.",
32
+ )
33
+ @click.option(
34
+ "-v",
35
+ "--verbose",
36
+ count=True,
37
+ help="Verbosity of output levels.",
38
+ )
39
+ @click.version_option(__version__)
40
+ @click.pass_context
41
+ def cli(context, host, yes, debug, json, verbose):
42
+ """primitive - a CLI tool for https://primitive.design"""
43
+ context.ensure_object(dict)
44
+ context.obj["YES"] = yes
45
+ context.obj["DEBUG"] = debug
46
+ context.obj["JSON"] = json
47
+ context.obj["VERBOSE"] = verbose
48
+ context.obj["HOST"] = host
49
+ if "config" not in sys.argv:
50
+ context.obj["PRIMITIVE"] = Primitive(host=host, DEBUG=debug, JSON=json)
51
+
52
+
53
+ cli.add_command(config_command, "config")
54
+ cli.add_command(whoami_command, "whoami")
55
+ cli.add_command(file_commands, "files")
56
+ cli.add_command(hardware_commands, "hardware")
57
+ cli.add_command(lint_commands, "lint")
58
+
59
+ if __name__ == "__main__":
60
+ cli(obj={})
primitive/client.py ADDED
@@ -0,0 +1,66 @@
1
+ import sys
2
+ from .auth.actions import Auth
3
+ from .projects.actions import Projects
4
+ from .graphql.sdk import create_session
5
+ from .utils.config import read_config_file
6
+ from .files.actions import Files
7
+ from .simulations.actions import Simulations
8
+ from .hardware.actions import Hardware
9
+ from .lint.actions import Lint
10
+
11
+ from loguru import logger
12
+
13
+ logger.disable("primitive")
14
+
15
+
16
+ class Primitive:
17
+ def __init__(
18
+ self,
19
+ host: str = "api.primitive.tech",
20
+ DEBUG: bool = False,
21
+ JSON: bool = False,
22
+ token: str = None,
23
+ transport: str = None,
24
+ ) -> None:
25
+ self.host = host
26
+ self.DEBUG = DEBUG
27
+ self.JSON = JSON
28
+
29
+ if self.DEBUG:
30
+ logger.enable("primitive")
31
+ logger.remove()
32
+ logger.add(
33
+ sink=sys.stderr,
34
+ serialize=self.JSON,
35
+ catch=True,
36
+ backtrace=True,
37
+ diagnose=True,
38
+ )
39
+
40
+ host = self.host
41
+ fingerprint = None
42
+ if not token and not transport:
43
+ self.get_host_config()
44
+ token = self.host_config.get("token")
45
+ transport = self.host_config.get("transport")
46
+ fingerprint = self.host_config.get("fingerprint")
47
+ else:
48
+ self.host_config = {"username": "", "token": token, "transport": transport}
49
+
50
+ self.session = create_session(
51
+ host=self.host, token=token, transport=transport, fingerprint=fingerprint
52
+ )
53
+
54
+ self.auth: Auth = Auth(self)
55
+ self.projects: Projects = Projects(self)
56
+ self.files: Files = Files(self)
57
+ self.simulations: Simulations = Simulations(self)
58
+ self.hardware: Hardware = Hardware(self)
59
+ self.lint: Lint = Lint(self)
60
+
61
+ def get_host_config(self):
62
+ self.full_config = read_config_file()
63
+ self.host_config = self.full_config.get(self.host)
64
+
65
+ if not self.host_config:
66
+ raise KeyError(f"Host {self.host} not found in config file.")
@@ -0,0 +1,76 @@
1
+ from pathlib import Path
2
+ from gql import gql
3
+ from primitive.graphql.sdk import create_requests_session
4
+
5
+
6
+ from primitive.utils.actions import BaseAction
7
+
8
+
9
+ class Files(BaseAction):
10
+ def trace_create(
11
+ self,
12
+ file_id: str,
13
+ signal_id: str,
14
+ signal_name: str,
15
+ module_name: str,
16
+ is_vector: bool,
17
+ size: int,
18
+ ):
19
+ mutation = gql(
20
+ """
21
+ mutation createTrace($input: TraceCreateInput!) {
22
+ traceCreate(input: $input) {
23
+ ... on Trace {
24
+ id
25
+ signalId
26
+ signalName
27
+ }
28
+ }
29
+ }
30
+ """
31
+ )
32
+ input = {
33
+ "fileId": file_id,
34
+ "signalId": signal_id,
35
+ "signalName": signal_name,
36
+ "moduleName": module_name,
37
+ "isVector": is_vector,
38
+ "size": size,
39
+ }
40
+ variables = {"input": input}
41
+ result = self.primitive.session.execute(mutation, variable_values=variables)
42
+ return result
43
+
44
+ def file_upload(self, path: Path, is_public: bool = False, key_prefix: str = ""):
45
+ file_path = str(path.resolve())
46
+ if path.exists() is False:
47
+ raise FileNotFoundError(f"File not found at {file_path}")
48
+
49
+ if is_public:
50
+ operations = (
51
+ """{ "query": "mutation fileUpload($input: FileUploadInput!) { fileUpload(input: $input) { ... on File { id } } }", "variables": { "input": { "fileObject": null, "isPublic": true, "filePath": \""""
52
+ + file_path
53
+ + """\", "keyPrefix": \""""
54
+ + key_prefix
55
+ + """\" } } }"""
56
+ ) # noqa
57
+
58
+ else:
59
+ operations = (
60
+ """{ "query": "mutation fileUpload($input: FileUploadInput!) { fileUpload(input: $input) { ... on File { id } } }", "variables": { "input": { "fileObject": null, "isPublic": false, "filePath": \""""
61
+ + file_path
62
+ + """\", "keyPrefix": \""""
63
+ + key_prefix
64
+ + """\" } } }"""
65
+ ) # noqa
66
+ body = {
67
+ "operations": ("", operations),
68
+ "map": ("", '{"fileObject": ["variables.input.fileObject"]}'),
69
+ "fileObject": (path.name, open(path, "rb")),
70
+ }
71
+
72
+ session = create_requests_session(self.primitive.host_config)
73
+ transport = self.primitive.host_config.get("transport")
74
+ url = f"{transport}://{self.primitive.host}/"
75
+ response = session.post(url, files=body)
76
+ return response
@@ -0,0 +1,28 @@
1
+ import click
2
+ from ..utils.printer import print_result
3
+ from pathlib import Path
4
+ import typing
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from ..client import Primitive
8
+
9
+
10
+ @click.group("files")
11
+ @click.pass_context
12
+ def cli(context):
13
+ """Files"""
14
+ pass
15
+
16
+
17
+ @cli.command("upload")
18
+ @click.pass_context
19
+ @click.argument("path", type=click.Path(exists=True))
20
+ @click.option("--public", "-p", help="Is this a Public file", is_flag=True)
21
+ @click.option("--key-prefix", "-k", help="Key Prefix", default="")
22
+ def file_upload_command(context, path, public, key_prefix):
23
+ """File Upload"""
24
+ primitive: Primitive = context.obj.get("PRIMITIVE")
25
+ path = Path(path)
26
+ result = primitive.files.file_upload(path, is_public=public, key_prefix=key_prefix)
27
+ message = result.json()
28
+ print_result(message=message, context=context)
File without changes
@@ -0,0 +1,53 @@
1
+ from typing import Dict
2
+ from gql import Client
3
+ from gql.transport.aiohttp import AIOHTTPTransport
4
+ import requests
5
+ from primitive.__about__ import __version__
6
+
7
+
8
+ def create_session(
9
+ token: str,
10
+ host: str = "api.primitive.tech",
11
+ transport: str = "https",
12
+ fingerprint: str = None,
13
+ fetch_schema_from_transport: bool = False,
14
+ ):
15
+ url = f"{transport}://{host}/"
16
+ headers = {
17
+ "Content-Type": "application/json",
18
+ "Accept": "application/json",
19
+ "x-primitive-agent": f"primitive@{__version__}",
20
+ }
21
+ if token:
22
+ headers["Authorization"] = f"Token {token}"
23
+
24
+ if fingerprint:
25
+ headers["x-primitive-fingerprint"] = fingerprint
26
+
27
+ transport = AIOHTTPTransport(url=url, headers=headers)
28
+ session = Client(
29
+ transport=transport,
30
+ fetch_schema_from_transport=fetch_schema_from_transport,
31
+ )
32
+ return session
33
+
34
+
35
+ def create_requests_session(
36
+ host_config: Dict,
37
+ ):
38
+ token = host_config.get("token")
39
+
40
+ headers = {
41
+ # "Content-Type": "multipart/form-data", # DO NOT ADD THIS MIME TYPE IT BREAKS
42
+ "Accept": "application/json",
43
+ "x-primitive-agent": f"primitive@{__version__}",
44
+ }
45
+ if token:
46
+ headers["Authorization"] = f"Token {token}"
47
+
48
+ if fingerprint := host_config.get("fingerprint", None):
49
+ headers["x-primitive-fingerprint"] = fingerprint
50
+
51
+ session = requests.Session()
52
+ session.headers.update(headers)
53
+ return session