kscale 0.0.11__py3-none-any.whl → 0.1.0__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.
- 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.11.dist-info → kscale-0.1.0.dist-info}/METADATA +29 -48
- kscale-0.1.0.dist-info/RECORD +36 -0
- {kscale-0.0.11.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 -397
- kscale/store/pybullet.py +0 -180
- kscale/store/urdf.py +0 -193
- kscale/store/utils.py +0 -33
- kscale-0.0.11.dist-info/RECORD +0 -26
- kscale-0.0.11.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.11.dist-info → kscale-0.1.0.dist-info}/LICENSE +0 -0
- {kscale-0.0.11.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
|