kscale 0.0.10__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 +2 -2
- kscale/api.py +3 -6
- kscale/cli.py +32 -0
- kscale/conf.py +6 -3
- kscale/requirements.txt +17 -1
- kscale/utils/checksum.py +41 -0
- kscale/utils/cli.py +28 -0
- kscale/web/api.py +14 -0
- kscale/web/cli/robot.py +100 -0
- kscale/web/cli/robot_class.py +113 -0
- kscale/web/cli/token.py +33 -0
- kscale/web/cli/user.py +33 -0
- kscale/web/clients/__init__.py +0 -0
- kscale/web/clients/base.py +314 -0
- kscale/web/clients/client.py +11 -0
- kscale/web/clients/robot.py +39 -0
- kscale/web/clients/robot_class.py +114 -0
- kscale/web/clients/user.py +10 -0
- kscale/web/gen/__init__.py +0 -0
- kscale/web/gen/api.py +73 -0
- kscale/web/utils.py +31 -0
- {kscale-0.0.10.dist-info → kscale-0.1.0.dist-info}/METADATA +29 -48
- kscale-0.1.0.dist-info/RECORD +36 -0
- {kscale-0.0.10.dist-info → kscale-0.1.0.dist-info}/WHEEL +1 -1
- kscale-0.1.0.dist-info/entry_points.txt +3 -0
- kscale/store/api.py +0 -64
- kscale/store/cli.py +0 -35
- kscale/store/client.py +0 -82
- kscale/store/gen/api.py +0 -301
- kscale/store/pybullet.py +0 -180
- kscale/store/urdf.py +0 -193
- kscale/store/utils.py +0 -33
- kscale-0.0.10.dist-info/RECORD +0 -26
- kscale-0.0.10.dist-info/entry_points.txt +0 -2
- /kscale/{store → web}/__init__.py +0 -0
- /kscale/{store/gen → web/cli}/__init__.py +0 -0
- {kscale-0.0.10.dist-info → kscale-0.1.0.dist-info}/LICENSE +0 -0
- {kscale-0.0.10.dist-info → kscale-0.1.0.dist-info}/top_level.txt +0 -0
kscale/__init__.py
CHANGED
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
|
8
|
-
|
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
|
20
|
-
|
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
|
-
|
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
|
kscale/utils/checksum.py
ADDED
@@ -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()
|
kscale/web/cli/robot.py
ADDED
@@ -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()
|
kscale/web/cli/token.py
ADDED
@@ -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
|