kscale 0.0.11__tar.gz → 0.1.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. {kscale-0.0.11/kscale.egg-info → kscale-0.1.0}/PKG-INFO +22 -41
  2. {kscale-0.0.11 → kscale-0.1.0}/README.md +1 -38
  3. {kscale-0.0.11 → kscale-0.1.0}/kscale/__init__.py +2 -2
  4. {kscale-0.0.11 → kscale-0.1.0}/kscale/api.py +3 -6
  5. kscale-0.1.0/kscale/cli.py +32 -0
  6. {kscale-0.0.11 → kscale-0.1.0}/kscale/conf.py +6 -3
  7. kscale-0.1.0/kscale/requirements.txt +26 -0
  8. kscale-0.1.0/kscale/utils/checksum.py +41 -0
  9. kscale-0.1.0/kscale/utils/cli.py +28 -0
  10. kscale-0.1.0/kscale/web/api.py +14 -0
  11. kscale-0.1.0/kscale/web/cli/robot.py +100 -0
  12. kscale-0.1.0/kscale/web/cli/robot_class.py +113 -0
  13. kscale-0.1.0/kscale/web/cli/token.py +33 -0
  14. kscale-0.1.0/kscale/web/cli/user.py +33 -0
  15. kscale-0.1.0/kscale/web/clients/__init__.py +0 -0
  16. kscale-0.1.0/kscale/web/clients/base.py +314 -0
  17. kscale-0.1.0/kscale/web/clients/client.py +11 -0
  18. kscale-0.1.0/kscale/web/clients/robot.py +39 -0
  19. kscale-0.1.0/kscale/web/clients/robot_class.py +114 -0
  20. kscale-0.1.0/kscale/web/clients/user.py +10 -0
  21. kscale-0.1.0/kscale/web/gen/__init__.py +0 -0
  22. kscale-0.1.0/kscale/web/gen/api.py +73 -0
  23. kscale-0.1.0/kscale/web/utils.py +31 -0
  24. {kscale-0.0.11 → kscale-0.1.0/kscale.egg-info}/PKG-INFO +22 -41
  25. {kscale-0.0.11 → kscale-0.1.0}/kscale.egg-info/SOURCES.txt +19 -9
  26. kscale-0.1.0/kscale.egg-info/entry_points.txt +3 -0
  27. {kscale-0.0.11 → kscale-0.1.0}/kscale.egg-info/requires.txt +11 -1
  28. {kscale-0.0.11 → kscale-0.1.0}/pyproject.toml +31 -5
  29. {kscale-0.0.11 → kscale-0.1.0}/setup.py +5 -2
  30. kscale-0.0.11/kscale/requirements.txt +0 -10
  31. kscale-0.0.11/kscale/store/api.py +0 -64
  32. kscale-0.0.11/kscale/store/cli.py +0 -35
  33. kscale-0.0.11/kscale/store/client.py +0 -82
  34. kscale-0.0.11/kscale/store/gen/api.py +0 -397
  35. kscale-0.0.11/kscale/store/pybullet.py +0 -180
  36. kscale-0.0.11/kscale/store/urdf.py +0 -193
  37. kscale-0.0.11/kscale/store/utils.py +0 -33
  38. kscale-0.0.11/kscale.egg-info/entry_points.txt +0 -2
  39. {kscale-0.0.11 → kscale-0.1.0}/LICENSE +0 -0
  40. {kscale-0.0.11 → kscale-0.1.0}/MANIFEST.in +0 -0
  41. {kscale-0.0.11 → kscale-0.1.0}/kscale/artifacts/__init__.py +0 -0
  42. {kscale-0.0.11 → kscale-0.1.0}/kscale/artifacts/plane.obj +0 -0
  43. {kscale-0.0.11 → kscale-0.1.0}/kscale/artifacts/plane.urdf +0 -0
  44. {kscale-0.0.11 → kscale-0.1.0}/kscale/py.typed +0 -0
  45. {kscale-0.0.11 → kscale-0.1.0}/kscale/requirements-dev.txt +0 -0
  46. {kscale-0.0.11/kscale/store → kscale-0.1.0/kscale/utils}/__init__.py +0 -0
  47. {kscale-0.0.11 → kscale-0.1.0}/kscale/utils/api_base.py +0 -0
  48. {kscale-0.0.11/kscale/store/gen → kscale-0.1.0/kscale/web}/__init__.py +0 -0
  49. {kscale-0.0.11/kscale/utils → kscale-0.1.0/kscale/web/cli}/__init__.py +0 -0
  50. {kscale-0.0.11 → kscale-0.1.0}/kscale.egg-info/dependency_links.txt +0 -0
  51. {kscale-0.0.11 → kscale-0.1.0}/kscale.egg-info/not-zip-safe +0 -0
  52. {kscale-0.0.11 → kscale-0.1.0}/kscale.egg-info/top_level.txt +0 -0
  53. {kscale-0.0.11 → kscale-0.1.0}/setup.cfg +0 -0
  54. {kscale-0.0.11 → kscale-0.1.0}/tests/test_dummy.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.0.11
3
+ Version: 0.1.0
4
4
  Summary: The kscale project
5
5
  Home-page: https://github.com/kscalelabs/kscale
6
6
  Author: Benjamin Bolte
@@ -9,9 +9,19 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: omegaconf
11
11
  Requires-Dist: email_validator
12
+ Requires-Dist: colorlogging
13
+ Requires-Dist: aiohttp
14
+ Requires-Dist: cryptography
12
15
  Requires-Dist: httpx
13
- Requires-Dist: requests
14
16
  Requires-Dist: pydantic
17
+ Requires-Dist: pyjwt
18
+ Requires-Dist: requests
19
+ Requires-Dist: yarl
20
+ Requires-Dist: aiofiles
21
+ Requires-Dist: click
22
+ Requires-Dist: tabulate
23
+ Requires-Dist: async-lru
24
+ Requires-Dist: krec
15
25
  Provides-Extra: dev
16
26
  Requires-Dist: black; extra == "dev"
17
27
  Requires-Dist: darglint; extra == "dev"
@@ -19,12 +29,14 @@ Requires-Dist: mypy; extra == "dev"
19
29
  Requires-Dist: pytest; extra == "dev"
20
30
  Requires-Dist: ruff; extra == "dev"
21
31
  Requires-Dist: datamodel-code-generator; extra == "dev"
22
-
23
- <p align="center">
24
- <picture>
25
- <img alt="K-Scale Open Source Robotics" src="https://media.kscale.dev/kscale-open-source-header.png" style="max-width: 100%;">
26
- </picture>
27
- </p>
32
+ Dynamic: author
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: home-page
36
+ Dynamic: provides-extra
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
28
40
 
29
41
  <div align="center">
30
42
 
@@ -43,41 +55,10 @@ Requires-Dist: datamodel-code-generator; extra == "dev"
43
55
 
44
56
  # K-Scale Command Line Interface
45
57
 
46
- This is a command line tool for interacting with various services provided by K-Scale Labs, such as:
47
-
48
- - [K-Scale Store](https://kscale.store/)
58
+ This is a command line tool for interacting with various services provided by K-Scale Labs. For more information, see the [documentation](https://docs.kscale.dev/pkg/intro).
49
59
 
50
60
  ## Installation
51
61
 
52
62
  ```bash
53
63
  pip install kscale
54
64
  ```
55
-
56
- ## Usage
57
-
58
- ### CLI
59
-
60
- Download a URDF from the K-Scale Store:
61
-
62
- ```bash
63
- kscale urdf download <artifact_id>
64
- ```
65
-
66
- Upload a URDF to the K-Scale Store:
67
-
68
- ```bash
69
- kscale urdf upload <artifact_id> <root_dir>
70
- ```
71
-
72
- ### Python API
73
-
74
- Reference a URDF by ID from the K-Scale Store:
75
-
76
- ```python
77
- from kscale import KScale
78
-
79
- async def main():
80
- kscale = KScale()
81
- urdf_dir_path = await kscale.store.urdf("123456")
82
- print(urdf_dir_path)
83
- ```
@@ -1,9 +1,3 @@
1
- <p align="center">
2
- <picture>
3
- <img alt="K-Scale Open Source Robotics" src="https://media.kscale.dev/kscale-open-source-header.png" style="max-width: 100%;">
4
- </picture>
5
- </p>
6
-
7
1
  <div align="center">
8
2
 
9
3
  [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/kscalelabs/ksim/blob/main/LICENSE)
@@ -21,41 +15,10 @@
21
15
 
22
16
  # K-Scale Command Line Interface
23
17
 
24
- This is a command line tool for interacting with various services provided by K-Scale Labs, such as:
25
-
26
- - [K-Scale Store](https://kscale.store/)
18
+ This is a command line tool for interacting with various services provided by K-Scale Labs. For more information, see the [documentation](https://docs.kscale.dev/pkg/intro).
27
19
 
28
20
  ## Installation
29
21
 
30
22
  ```bash
31
23
  pip install kscale
32
24
  ```
33
-
34
- ## Usage
35
-
36
- ### CLI
37
-
38
- Download a URDF from the K-Scale Store:
39
-
40
- ```bash
41
- kscale urdf download <artifact_id>
42
- ```
43
-
44
- Upload a URDF to the K-Scale Store:
45
-
46
- ```bash
47
- kscale urdf upload <artifact_id> <root_dir>
48
- ```
49
-
50
- ### Python API
51
-
52
- Reference a URDF by ID from the K-Scale Store:
53
-
54
- ```python
55
- from kscale import KScale
56
-
57
- async def main():
58
- kscale = KScale()
59
- urdf_dir_path = await kscale.store.urdf("123456")
60
- print(urdf_dir_path)
61
- ```
@@ -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
@@ -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
@@ -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))
@@ -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)
@@ -0,0 +1,26 @@
1
+ # requirements.txt
2
+
3
+ # Configuration
4
+ omegaconf
5
+ email_validator
6
+ colorlogging
7
+
8
+ # HTTP requests
9
+ aiohttp
10
+ cryptography
11
+ httpx
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)
@@ -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)
@@ -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()
@@ -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