kscale 0.3.6__py3-none-any.whl → 0.3.8__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """Defines the common interface for the K-Scale Python API."""
2
2
 
3
- __version__ = "0.3.6"
3
+ __version__ = "0.3.8"
4
4
 
5
5
  from pathlib import Path
6
6
 
kscale/cli.py CHANGED
@@ -8,7 +8,6 @@ import colorlogging
8
8
  from kscale.utils.cli import recursive_help
9
9
  from kscale.web.cli.robot import cli as robot_cli
10
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
11
  from kscale.web.cli.user import cli as user_cli
13
12
 
14
13
 
@@ -22,7 +21,6 @@ def cli() -> None:
22
21
  logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
23
22
 
24
23
 
25
- cli.add_command(token_cli, "token")
26
24
  cli.add_command(user_cli, "user")
27
25
  cli.add_command(robot_class_cli, "robots")
28
26
  cli.add_command(robot_cli, "robot")
kscale/requirements.txt CHANGED
@@ -21,6 +21,3 @@ tabulate
21
21
 
22
22
  # Async
23
23
  async-lru
24
-
25
- # K-Scale
26
- krec
@@ -1,5 +1,6 @@
1
1
  """Defines the CLI for getting information about robot classes."""
2
2
 
3
+ import itertools
3
4
  import json
4
5
  import logging
5
6
  import math
@@ -80,7 +81,23 @@ async def update(current_name: str, name: str | None = None, description: str |
80
81
  click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
81
82
 
82
83
 
83
- @cli.command()
84
+ @cli.command("delete")
85
+ @click.argument("name")
86
+ @coro
87
+ async def delete_robot_class(name: str) -> None:
88
+ """Deletes a robot class."""
89
+ async with RobotClassClient() as client:
90
+ await client.delete_robot_class(name)
91
+ click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
92
+
93
+
94
+ @cli.group()
95
+ def metadata() -> None:
96
+ """Handle the robot class metadata."""
97
+ pass
98
+
99
+
100
+ @metadata.command("update")
84
101
  @click.argument("name")
85
102
  @click.argument("json_path", type=click.Path(exists=True))
86
103
  @coro
@@ -96,7 +113,7 @@ async def update_metadata(name: str, json_path: str) -> None:
96
113
  click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
97
114
 
98
115
 
99
- @cli.command()
116
+ @metadata.command("get")
100
117
  @click.argument("name")
101
118
  @click.option("--json-path", type=click.Path(exists=False))
102
119
  @coro
@@ -115,27 +132,17 @@ async def get_metadata(name: str, json_path: str | None = None) -> None:
115
132
  json.dump(metadata.model_dump(), f)
116
133
 
117
134
 
118
- @cli.command()
119
- @click.argument("name")
120
- @coro
121
- async def delete(name: str) -> None:
122
- """Deletes a robot class."""
123
- async with RobotClassClient() as client:
124
- await client.delete_robot_class(name)
125
- click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
126
-
127
-
128
135
  @cli.group()
129
136
  def urdf() -> None:
130
137
  """Handle the robot class URDF."""
131
138
  pass
132
139
 
133
140
 
134
- @urdf.command()
141
+ @urdf.command("upload")
135
142
  @click.argument("class_name")
136
143
  @click.argument("urdf_file")
137
144
  @coro
138
- async def upload(class_name: str, urdf_file: str) -> None:
145
+ async def upload_urdf(class_name: str, urdf_file: str) -> None:
139
146
  """Uploads a URDF file to a robot class."""
140
147
  async with RobotClassClient() as client:
141
148
  response = await client.upload_robot_class_urdf(class_name, urdf_file)
@@ -143,18 +150,18 @@ async def upload(class_name: str, urdf_file: str) -> None:
143
150
  click.echo(f" Filename: {click.style(response.filename, fg='green')}")
144
151
 
145
152
 
146
- @urdf.command()
153
+ @urdf.command("download")
147
154
  @click.argument("class_name")
148
155
  @click.option("--cache", is_flag=True, default=False)
149
156
  @coro
150
- async def download(class_name: str, cache: bool) -> None:
157
+ async def download_urdf(class_name: str, cache: bool) -> None:
151
158
  """Downloads a URDF file from a robot class."""
152
159
  async with RobotClassClient() as client:
153
160
  urdf_file = await client.download_and_extract_urdf(class_name, cache=cache)
154
161
  click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
155
162
 
156
163
 
157
- @urdf.command()
164
+ @urdf.command("pybullet")
158
165
  @click.argument("class_name")
159
166
  @click.option("--no-cache", is_flag=True, default=False)
160
167
  @click.option("--hide-gui", is_flag=True, default=False)
@@ -168,7 +175,7 @@ async def download(class_name: str, cache: bool) -> None:
168
175
  @click.option("--start-height", type=float, default=0.0)
169
176
  @click.option("--cycle-duration", type=float, default=2.0)
170
177
  @coro
171
- async def pybullet(
178
+ async def run_pybullet(
172
179
  class_name: str,
173
180
  no_cache: bool,
174
181
  hide_gui: bool,
@@ -432,11 +439,11 @@ async def pybullet(
432
439
  last_time = cur_time
433
440
 
434
441
 
435
- @urdf.command()
442
+ @urdf.command("mujoco")
436
443
  @click.argument("class_name")
437
444
  @click.option("--no-cache", is_flag=True, default=False)
438
445
  @coro
439
- async def mujoco(class_name: str, no_cache: bool) -> None:
446
+ async def run_mujoco(class_name: str, no_cache: bool) -> None:
440
447
  """Shows the URDF file for a robot class in Mujoco.
441
448
 
442
449
  This command downloads and extracts the robot class URDF folder,
@@ -444,11 +451,11 @@ async def mujoco(class_name: str, no_cache: bool) -> None:
444
451
  launches the Mujoco viewer using the provided MJCF file.
445
452
  """
446
453
  try:
447
- from mujoco.viewer import launch_from_path
454
+ import mujoco.viewer
448
455
  except ImportError:
449
456
  click.echo(
450
457
  click.style(
451
- "Mujoco and mujoco-python-viewer are required; install with `pip install mujoco mujoco-python-viewer`",
458
+ "Mujoco and mujoco-python-viewer are required; install with `pip install mujoco`",
452
459
  fg="red",
453
460
  )
454
461
  )
@@ -458,13 +465,20 @@ async def mujoco(class_name: str, no_cache: bool) -> None:
458
465
  extracted_folder = await client.download_and_extract_urdf(class_name, cache=not no_cache)
459
466
 
460
467
  try:
461
- mjcf_file = next(extracted_folder.glob("*.mjcf"))
468
+ mjcf_file = next(
469
+ itertools.chain(
470
+ extracted_folder.glob("*.scene.mjcf"),
471
+ extracted_folder.glob("*.mjcf"),
472
+ extracted_folder.glob("*.xml"),
473
+ )
474
+ )
462
475
  except StopIteration:
463
476
  click.echo(click.style(f"No MJCF file found in {extracted_folder}", fg="red"))
464
477
  return
465
478
 
466
- click.echo(f"Launching Mujoco viewer with: {click.style(str(mjcf_file.resolve()), fg='green')}")
467
- launch_from_path(str(mjcf_file.resolve()))
479
+ mjcf_path_str = str(mjcf_file.resolve())
480
+ click.echo(f"Launching Mujoco viewer with: {click.style(mjcf_path_str, fg='green')}")
481
+ mujoco.viewer.launch_from_path(mjcf_path_str)
468
482
 
469
483
 
470
484
  if __name__ == "__main__":
kscale/web/cli/user.py CHANGED
@@ -45,8 +45,7 @@ async def key() -> None:
45
45
  """Get an API key for the currently-authenticated user."""
46
46
  client = UserClient()
47
47
  api_key = await client.get_api_key()
48
- click.echo("API key:")
49
- click.echo(click.style(api_key, fg="green"))
48
+ click.echo(f"API key: {click.style(api_key, fg='green')}")
50
49
 
51
50
 
52
51
  if __name__ == "__main__":
@@ -5,6 +5,7 @@ import json
5
5
  import logging
6
6
  import os
7
7
  import secrets
8
+ import sys
8
9
  import time
9
10
  import webbrowser
10
11
  from types import TracebackType
@@ -31,6 +32,10 @@ OAUTH_PORT = 16821
31
32
  HEADER_NAME = "x-kscale-api-key"
32
33
 
33
34
 
35
+ def verbose_error() -> bool:
36
+ return os.environ.get("KSCALE_VERBOSE_ERROR", "0") == "1"
37
+
38
+
34
39
  class OAuthCallback:
35
40
  def __init__(self) -> None:
36
41
  self.token_type: str | None = None
@@ -375,8 +380,14 @@ class BaseClient:
375
380
  response = await client.request(method, url, **kwargs)
376
381
 
377
382
  if response.is_error:
378
- logger.error("Error response from K-Scale: %s", response.text)
379
- response.raise_for_status()
383
+ logger.error("Got %d error K-Scale: %s", response.status_code, response.text)
384
+ if verbose_error():
385
+ response.raise_for_status()
386
+ else:
387
+ logger.error("Use KSCALE_VERBOSE_ERROR=1 to see the full error message")
388
+ logger.error("If this persists, please create an issue here: https://github.com/kscalelabs/kscale")
389
+ sys.exit(1)
390
+
380
391
  return response.json()
381
392
 
382
393
  async def close(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: The kscale project
5
5
  Home-page: https://github.com/kscalelabs/kscale
6
6
  Author: Benjamin Bolte
@@ -21,7 +21,6 @@ Requires-Dist: click
21
21
  Requires-Dist: colorlogging
22
22
  Requires-Dist: tabulate
23
23
  Requires-Dist: async-lru
24
- Requires-Dist: krec
25
24
  Provides-Extra: dev
26
25
  Requires-Dist: black; extra == "dev"
27
26
  Requires-Dist: darglint; extra == "dev"
@@ -1,9 +1,9 @@
1
- kscale/__init__.py,sha256=ny2947fOKliAIcJrudjMn6Ocfzk9Hb-VXUNFPikAPGo,200
2
- kscale/cli.py,sha256=PMHLKR5UwdbbReVmqHXpJ-K9-mGHv_0I7KQkwxmFcUA,881
1
+ kscale/__init__.py,sha256=XbIxlxq1doXA2L5v8II07s5g2CoqvIRp8LT5AfdFJDY,200
2
+ kscale/cli.py,sha256=JvaPtmWvF7s0D4I3K98eZAItf3oOi2ULsn5aPGxDcu4,795
3
3
  kscale/conf.py,sha256=dm35XSnzJp93St-ixVtYN4Nvqvb5upPGBrWkSI6Yb-4,1743
4
4
  kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  kscale/requirements-dev.txt,sha256=WI7-ea4IRJakmqVMN8QKhOsDGrghwtvk03aIsFaNSIw,130
6
- kscale/requirements.txt,sha256=_BGbnKTQaXKx0bNEG0wguod9swsiCb2mF6rLm7sFJ2Q,214
6
+ kscale/requirements.txt,sha256=tj8wBkIhl6ijk5rFqtPCdkpdOS7XSIcUvsz_Eq1axcw,198
7
7
  kscale/artifacts/__init__.py,sha256=RK8wdybtCJPgdLLJ8R8-YMi1Ph5ojqAKVJZowHONtgo,232
8
8
  kscale/artifacts/plane.obj,sha256=x59-IIrWpLjhotChiqT2Ul6U8s0RcHkaEeUZb4KXL1c,348
9
9
  kscale/artifacts/plane.urdf,sha256=LCiTk14AyTHjkZ1jvsb0hNaEaJUxDb8Z1JjsgpXu3YM,819
@@ -15,20 +15,19 @@ kscale/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  kscale/web/utils.py,sha256=Mme-FAQ0_zbjjOQeX8wyq8F4kL4i9fH7ytri16U6qOA,1046
16
16
  kscale/web/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  kscale/web/cli/robot.py,sha256=rI-A4_0uvJPeA71Apl4Z3mV5fIfWkgmzT9JRmJYxz3A,3307
18
- kscale/web/cli/robot_class.py,sha256=Ymyrc3UqsO7PzP_wJFXEWW37knMCDZTYP-8ziXVa-AQ,18710
19
- kscale/web/cli/token.py,sha256=1rFC8MYKtqbNsQa2KIqwW1tqpaMtFaxuNsallwejXTU,787
20
- kscale/web/cli/user.py,sha256=aaJJCL1P5lfhK6ZC9OwOHXKA-I3MWqVZ_k7TYnx33CY,1303
18
+ kscale/web/cli/robot_class.py,sha256=Vg-KWROdmz3yqBEG-RS5xiUviKwEz8DZ3q2estK66ZQ,19082
19
+ kscale/web/cli/user.py,sha256=9IGsJBPyhjsmT04mZ2RGOs35ePfqB2RltYP0Ty5zF5o,1290
21
20
  kscale/web/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- kscale/web/clients/base.py,sha256=A70BF9ZnHEBds0RKrowbeUNsX_WwsgV349csUlM4iMc,15174
21
+ kscale/web/clients/base.py,sha256=ThQakuvGh7CquiGQ4wiCyCWAgqmKInsJcANo910X9Uw,15585
23
22
  kscale/web/clients/client.py,sha256=rzW2s8T7bKVuybOSQ65-ghl02rcXBoOxnx_nUDwgEPw,362
24
23
  kscale/web/clients/robot.py,sha256=PI8HHkU-4Re9I5rLpp6dGbekRE-rBNVfXZxR_mO2MqE,1485
25
24
  kscale/web/clients/robot_class.py,sha256=G8Nk6V7LGJE9Wpg9tyyCkIfz1fRTsxXQRgHtleiUVqo,6834
26
25
  kscale/web/clients/user.py,sha256=jsa1_s6qXRM-AGBbHlPhd1NierUtynjY9tVAPNr6_Os,568
27
26
  kscale/web/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
27
  kscale/web/gen/api.py,sha256=VWlt8TaMZaAhCPPIWhx1d4yE0BRfpVz0-_Cps8hjgAI,4662
29
- kscale-0.3.6.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
30
- kscale-0.3.6.dist-info/METADATA,sha256=IHjwsym9rqmZnPltfYE3nGywHbDFR-HCOVfh_YrG_Bg,2340
31
- kscale-0.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
32
- kscale-0.3.6.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
33
- kscale-0.3.6.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
34
- kscale-0.3.6.dist-info/RECORD,,
28
+ kscale-0.3.8.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
29
+ kscale-0.3.8.dist-info/METADATA,sha256=b2ThYIovV2vTrR_uU44Hl4VHWKZEJkNJa56_1VqyvrE,2320
30
+ kscale-0.3.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
31
+ kscale-0.3.8.dist-info/entry_points.txt,sha256=N_0pCpPnwGDYVzOeuaSOrbJkS5L3lS9d8CxpJF1f8UI,62
32
+ kscale-0.3.8.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
33
+ kscale-0.3.8.dist-info/RECORD,,
kscale/web/cli/token.py DELETED
@@ -1,33 +0,0 @@
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()
File without changes