kscale 0.0.11__py3-none-any.whl → 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
kscale/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """Defines the common interface for the K-Scale Python API."""
2
2
 
3
- __version__ = "0.0.11"
3
+ __version__ = "0.1.0"
4
4
 
5
5
  from pathlib import Path
6
6
 
7
- from kscale.api import KScale
7
+ from kscale.api import K
8
8
 
9
9
  ROOT_DIR = Path(__file__).parent
kscale/api.py CHANGED
@@ -1,14 +1,11 @@
1
1
  """Defines common functionality for the K-Scale API."""
2
2
 
3
- from kscale.store.api import StoreAPI
4
3
  from kscale.utils.api_base import APIBase
4
+ from kscale.web.api import WebAPI
5
5
 
6
6
 
7
- class KScale(
8
- StoreAPI,
7
+ class K(
8
+ WebAPI,
9
9
  APIBase,
10
10
  ):
11
11
  """Defines a common interface for the K-Scale API."""
12
-
13
- def __init__(self, api_key: str | None = None) -> None:
14
- self.api_key = api_key
kscale/cli.py ADDED
@@ -0,0 +1,32 @@
1
+ """Defines the top-level KOL CLI."""
2
+
3
+ import logging
4
+
5
+ import click
6
+ import colorlogging
7
+
8
+ from kscale.utils.cli import recursive_help
9
+ from kscale.web.cli.robot import cli as robot_cli
10
+ from kscale.web.cli.robot_class import cli as robot_class_cli
11
+ from kscale.web.cli.token import cli as token_cli
12
+ from kscale.web.cli.user import cli as user_cli
13
+
14
+
15
+ @click.group()
16
+ def cli() -> None:
17
+ """Command line interface for interacting with the K-Scale web API."""
18
+ colorlogging.configure()
19
+
20
+ # Suppress aiohttp access logging
21
+ logging.getLogger("httpx").setLevel(logging.WARNING)
22
+ logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
23
+
24
+
25
+ cli.add_command(token_cli, "token")
26
+ cli.add_command(user_cli, "user")
27
+ cli.add_command(robot_class_cli, "robots")
28
+ cli.add_command(robot_cli, "robot")
29
+
30
+ if __name__ == "__main__":
31
+ # python -m kscale.cli
32
+ print(recursive_help(cli))
kscale/conf.py CHANGED
@@ -8,6 +8,9 @@ from pathlib import Path
8
8
 
9
9
  from omegaconf import II, OmegaConf
10
10
 
11
+ # This is the public API endpoint for the K-Scale WWW API.
12
+ DEFAULT_API_ROOT = "https://api.kscale.dev"
13
+
11
14
 
12
15
  def get_path() -> Path:
13
16
  if "KSCALE_CONFIG_DIR" in os.environ:
@@ -16,14 +19,14 @@ def get_path() -> Path:
16
19
 
17
20
 
18
21
  @dataclass
19
- class StoreSettings:
20
- api_key: str | None = field(default=None)
22
+ class WWWSettings:
23
+ api_root: str = field(default=DEFAULT_API_ROOT)
21
24
  cache_dir: str = field(default=II("oc.env:KSCALE_CACHE_DIR,'~/.kscale/cache/'"))
22
25
 
23
26
 
24
27
  @dataclass
25
28
  class Settings:
26
- store: StoreSettings = field(default_factory=StoreSettings)
29
+ www: WWWSettings = field(default_factory=WWWSettings)
27
30
 
28
31
  def save(self) -> None:
29
32
  (dir_path := get_path()).mkdir(parents=True, exist_ok=True)
kscale/requirements.txt CHANGED
@@ -3,8 +3,24 @@
3
3
  # Configuration
4
4
  omegaconf
5
5
  email_validator
6
+ colorlogging
6
7
 
7
8
  # HTTP requests
9
+ aiohttp
10
+ cryptography
8
11
  httpx
9
- requests
10
12
  pydantic
13
+ pyjwt
14
+ requests
15
+ yarl
16
+
17
+ # CLI
18
+ aiofiles
19
+ click
20
+ tabulate
21
+
22
+ # Async
23
+ async-lru
24
+
25
+ # K-Scale
26
+ krec
@@ -0,0 +1,41 @@
1
+ """Utility functions for file checksums."""
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Tuple
6
+
7
+ CHUNK_SIZE = 8192
8
+
9
+
10
+ async def calculate_sha256(file_path: str | Path) -> Tuple[str, int]:
11
+ """Calculate SHA256 checksum and size of a file.
12
+
13
+ Args:
14
+ file_path: Path to the file
15
+
16
+ Returns:
17
+ Tuple of (checksum hex string, file size in bytes)
18
+ """
19
+ sha256_hash = hashlib.sha256()
20
+ file_size = 0
21
+
22
+ with open(file_path, "rb") as f:
23
+ for chunk in iter(lambda: f.read(CHUNK_SIZE), b""):
24
+ sha256_hash.update(chunk)
25
+ file_size += len(chunk)
26
+
27
+ return sha256_hash.hexdigest(), file_size
28
+
29
+
30
+ class FileChecksum:
31
+ """Helper class for handling file checksums."""
32
+
33
+ @staticmethod
34
+ async def calculate(file_path: str | Path) -> Tuple[str, int]:
35
+ """Calculate SHA256 checksum and size of a file."""
36
+ return await calculate_sha256(file_path)
37
+
38
+ @staticmethod
39
+ def update_hash(hash_obj: "hashlib._Hash", chunk: bytes) -> None:
40
+ """Update a hash object with new data."""
41
+ hash_obj.update(chunk)
kscale/utils/cli.py ADDED
@@ -0,0 +1,28 @@
1
+ """Defines utilities for working with asyncio."""
2
+
3
+ import asyncio
4
+ import textwrap
5
+ from functools import wraps
6
+ from typing import Any, Callable, Coroutine, ParamSpec, TypeVar
7
+
8
+ import click
9
+
10
+ T = TypeVar("T")
11
+ P = ParamSpec("P")
12
+
13
+
14
+ def coro(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
15
+ @wraps(f)
16
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
17
+ return asyncio.run(f(*args, **kwargs))
18
+
19
+ return wrapper
20
+
21
+
22
+ def recursive_help(cmd: click.Command, parent: click.Context | None = None, indent: int = 0) -> str:
23
+ ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
24
+ help_text = cmd.get_help(ctx)
25
+ commands = getattr(cmd, "commands", {})
26
+ for sub in commands.values():
27
+ help_text += recursive_help(sub, ctx, indent + 2)
28
+ return textwrap.indent(help_text, " " * indent)
kscale/web/api.py ADDED
@@ -0,0 +1,14 @@
1
+ """Defines a common interface for the K-Scale WWW API."""
2
+
3
+ from kscale.utils.api_base import APIBase
4
+ from kscale.web.clients.client import WWWClient
5
+ from kscale.web.gen.api import ProfileResponse
6
+
7
+
8
+ class WebAPI(APIBase):
9
+ async def www_client(self) -> WWWClient:
10
+ return WWWClient()
11
+
12
+ async def get_profile_info(self) -> ProfileResponse:
13
+ client = await self.www_client()
14
+ return await client.get_profile_info()
@@ -0,0 +1,100 @@
1
+ """Defines the CLI for getting information about robots."""
2
+
3
+ import click
4
+ from tabulate import tabulate
5
+
6
+ from kscale.utils.cli import coro
7
+ from kscale.web.clients.robot import RobotClient
8
+
9
+
10
+ @click.group()
11
+ def cli() -> None:
12
+ """Get information about robots."""
13
+ pass
14
+
15
+
16
+ @cli.command()
17
+ @coro
18
+ async def list() -> None:
19
+ client = RobotClient()
20
+ robots = await client.get_all_robots()
21
+ if robots:
22
+ table_data = [
23
+ [
24
+ click.style(robot.id, fg="blue"),
25
+ click.style(robot.robot_name, fg="green"),
26
+ click.style(robot.class_id, fg="yellow"),
27
+ robot.description or "N/A",
28
+ ]
29
+ for robot in robots
30
+ ]
31
+ click.echo(tabulate(table_data, headers=["ID", "Name", "Class", "Description"], tablefmt="simple"))
32
+ else:
33
+ click.echo(click.style("No robots found", fg="red"))
34
+
35
+
36
+ @cli.command()
37
+ @click.option("-u", "--user-id", type=str, default="me")
38
+ @coro
39
+ async def user(user_id: str = "me") -> None:
40
+ client = RobotClient()
41
+ robots = await client.get_user_robots(user_id)
42
+ if robots:
43
+ table_data = [
44
+ [
45
+ click.style(robot.id, fg="blue"),
46
+ click.style(robot.robot_name, fg="green"),
47
+ click.style(robot.class_id, fg="yellow"),
48
+ robot.description or "N/A",
49
+ ]
50
+ for robot in robots
51
+ ]
52
+ click.echo(tabulate(table_data, headers=["ID", "Name", "Class", "Description"], tablefmt="simple"))
53
+ else:
54
+ click.echo(click.style("No robots found", fg="red"))
55
+
56
+
57
+ @cli.command()
58
+ @click.argument("robot_id")
59
+ @coro
60
+ async def id(robot_id: str) -> None:
61
+ client = RobotClient()
62
+ robot = await client.get_robot_by_id(robot_id)
63
+ click.echo("Robot:")
64
+ click.echo(f" ID: {click.style(robot.id, fg='blue')}")
65
+ click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
66
+ click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
67
+ click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
68
+
69
+
70
+ @cli.command()
71
+ @click.argument("robot_name")
72
+ @coro
73
+ async def name(robot_name: str) -> None:
74
+ client = RobotClient()
75
+ robot = await client.get_robot_by_name(robot_name)
76
+ click.echo("Robot:")
77
+ click.echo(f" ID: {click.style(robot.id, fg='blue')}")
78
+ click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
79
+ click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
80
+ click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
81
+
82
+
83
+ @cli.command()
84
+ @click.argument("class_name")
85
+ @click.argument("name")
86
+ @click.option("-c", "--class-name", type=str, required=True)
87
+ @click.option("-d", "--description", type=str, default=None)
88
+ @coro
89
+ async def add(name: str, class_name: str, description: str | None = None) -> None:
90
+ client = RobotClient()
91
+ robot = await client.add_robot(name, class_name, description)
92
+ click.echo("Robot added:")
93
+ click.echo(f" ID: {click.style(robot.id, fg='blue')}")
94
+ click.echo(f" Name: {click.style(robot.robot_name, fg='green')}")
95
+ click.echo(f" Class: {click.style(robot.class_name, fg='yellow')}")
96
+ click.echo(f" Description: {click.style(robot.description or 'N/A', fg='yellow')}")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ cli()
@@ -0,0 +1,113 @@
1
+ """Defines the CLI for getting information about robot classes."""
2
+
3
+ import logging
4
+
5
+ import click
6
+ from tabulate import tabulate
7
+
8
+ from kscale.utils.cli import coro
9
+ from kscale.web.clients.robot_class import RobotClassClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @click.group()
15
+ def cli() -> None:
16
+ """Get information about robot classes."""
17
+ pass
18
+
19
+
20
+ @cli.command()
21
+ @coro
22
+ async def list() -> None:
23
+ """Lists all robot classes."""
24
+ client = RobotClassClient()
25
+ robot_classes = await client.get_robot_classes()
26
+ if robot_classes:
27
+ # Prepare table data
28
+ table_data = [
29
+ [
30
+ click.style(rc.id, fg="blue"),
31
+ click.style(rc.class_name, fg="green"),
32
+ rc.description or "N/A",
33
+ ]
34
+ for rc in robot_classes
35
+ ]
36
+ click.echo(tabulate(table_data, headers=["ID", "Name", "Description"], tablefmt="simple"))
37
+ else:
38
+ click.echo(click.style("No robot classes found", fg="red"))
39
+
40
+
41
+ @cli.command()
42
+ @click.argument("name")
43
+ @click.option("-d", "--description", type=str, default=None)
44
+ @coro
45
+ async def add(
46
+ name: str,
47
+ description: str | None = None,
48
+ ) -> None:
49
+ """Adds a new robot class."""
50
+ async with RobotClassClient() as client:
51
+ robot_class = await client.create_robot_class(name, description)
52
+ click.echo("Robot class created:")
53
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
54
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
55
+ click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
56
+
57
+
58
+ @cli.command()
59
+ @click.argument("current_name")
60
+ @click.option("-n", "--name", type=str, default=None)
61
+ @click.option("-d", "--description", type=str, default=None)
62
+ @coro
63
+ async def update(current_name: str, name: str | None = None, description: str | None = None) -> None:
64
+ """Updates a robot class."""
65
+ async with RobotClassClient() as client:
66
+ robot_class = await client.update_robot_class(current_name, name, description)
67
+ click.echo("Robot class updated:")
68
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
69
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
70
+ click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
71
+
72
+
73
+ @cli.command()
74
+ @click.argument("name")
75
+ @coro
76
+ async def delete(name: str) -> None:
77
+ """Deletes a robot class."""
78
+ async with RobotClassClient() as client:
79
+ await client.delete_robot_class(name)
80
+ click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
81
+
82
+
83
+ @cli.group()
84
+ def urdf() -> None:
85
+ """Handle the robot class URDF."""
86
+ pass
87
+
88
+
89
+ @urdf.command()
90
+ @click.argument("class_name")
91
+ @click.argument("urdf_file")
92
+ @coro
93
+ async def upload(class_name: str, urdf_file: str) -> None:
94
+ """Uploads a URDF file to a robot class."""
95
+ async with RobotClassClient() as client:
96
+ response = await client.upload_robot_class_urdf(class_name, urdf_file)
97
+ click.echo("URDF uploaded:")
98
+ click.echo(f" Filename: {click.style(response.filename, fg='green')}")
99
+
100
+
101
+ @urdf.command()
102
+ @click.argument("class_name")
103
+ @click.option("--no-cache", is_flag=True, default=False)
104
+ @coro
105
+ async def download(class_name: str, no_cache: bool) -> None:
106
+ """Downloads a URDF file from a robot class."""
107
+ async with RobotClassClient() as client:
108
+ urdf_file = await client.download_robot_class_urdf(class_name, cache=not no_cache)
109
+ click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ cli()
@@ -0,0 +1,33 @@
1
+ """Defines the CLI for interacting with K-Scale's OpenID Connect server."""
2
+
3
+ import logging
4
+
5
+ import click
6
+
7
+ from kscale.utils.cli import coro
8
+ from kscale.web.clients.base import BaseClient
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @click.group()
14
+ def cli() -> None:
15
+ """Retrieve an OICD token from the K-Scale authentication server."""
16
+ pass
17
+
18
+
19
+ @cli.command()
20
+ @coro
21
+ async def get() -> None:
22
+ """Get a bearer token from OpenID Connect."""
23
+ logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
24
+ async with BaseClient() as client:
25
+ try:
26
+ token = await client.get_bearer_token()
27
+ logger.info("Bearer token: %s", token)
28
+ except Exception:
29
+ logger.exception("Error getting bearer token")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ cli()
kscale/web/cli/user.py ADDED
@@ -0,0 +1,33 @@
1
+ """Defines the CLI for getting information about the current user."""
2
+
3
+ import logging
4
+
5
+ import click
6
+
7
+ from kscale.utils.cli import coro
8
+ from kscale.web.clients.user import UserClient
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @click.group()
14
+ def cli() -> None:
15
+ """Get information about the currently-authenticated user."""
16
+ pass
17
+
18
+
19
+ @cli.command()
20
+ @coro
21
+ async def me() -> None:
22
+ client = UserClient()
23
+ profile = await client.get_profile_info()
24
+ logger.info("Email: %s", profile.email)
25
+ logger.info("Email verified: %s", profile.email_verified)
26
+ logger.info("User ID: %s", profile.user.user_id)
27
+ logger.info("Is admin: %s", profile.user.is_admin)
28
+ logger.info("Can upload: %s", profile.user.can_upload)
29
+ logger.info("Can test: %s", profile.user.can_test)
30
+
31
+
32
+ if __name__ == "__main__":
33
+ cli()
File without changes