kscale 0.3.16__py3-none-any.whl → 0.3.18__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.16"
3
+ __version__ = "0.3.18"
4
4
 
5
5
  from pathlib import Path
6
6
 
kscale/cli.py CHANGED
@@ -6,6 +6,9 @@ import click
6
6
  import colorlogging
7
7
 
8
8
  from kscale.utils.cli import recursive_help
9
+ from kscale.web.cli.clip import cli as clip_cli
10
+ from kscale.web.cli.group import cli as group_cli
11
+ from kscale.web.cli.permission import cli as permission_cli
9
12
  from kscale.web.cli.robot import cli as robot_cli
10
13
  from kscale.web.cli.robot_class import cli as robot_class_cli
11
14
  from kscale.web.cli.user import cli as user_cli
@@ -24,6 +27,9 @@ def cli() -> None:
24
27
  cli.add_command(user_cli, "user")
25
28
  cli.add_command(robot_class_cli, "robots")
26
29
  cli.add_command(robot_cli, "robot")
30
+ cli.add_command(clip_cli, "clips")
31
+ cli.add_command(group_cli, "groups")
32
+ cli.add_command(permission_cli, "permissions")
27
33
 
28
34
  if __name__ == "__main__":
29
35
  # python -m kscale.cli
kscale/web/cli/clip.py ADDED
@@ -0,0 +1,132 @@
1
+ """Defines the CLI for managing clips."""
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.clip import ClipClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @click.group()
15
+ def cli() -> None:
16
+ """Manage clips."""
17
+ pass
18
+
19
+
20
+ @cli.command()
21
+ @coro
22
+ async def list() -> None:
23
+ """Lists all clips for the authenticated user."""
24
+ client = ClipClient()
25
+ clips = await client.get_clips()
26
+ if clips:
27
+ # Prepare table data
28
+ table_data = [
29
+ [
30
+ click.style(clip.id, fg="blue"),
31
+ clip.description or "N/A",
32
+ clip.created_at.strftime("%Y-%m-%d %H:%M:%S"),
33
+ "N/A" if clip.num_downloads is None else f"{clip.num_downloads:,}",
34
+ "N/A" if clip.file_size is None else f"{clip.file_size:,} bytes",
35
+ ]
36
+ for clip in clips
37
+ ]
38
+ click.echo(
39
+ tabulate(
40
+ table_data,
41
+ headers=["ID", "Description", "Created", "Downloads", "Size"],
42
+ tablefmt="simple",
43
+ )
44
+ )
45
+ else:
46
+ click.echo(click.style("No clips found", fg="red"))
47
+
48
+
49
+ @cli.command()
50
+ @click.argument("clip_id")
51
+ @coro
52
+ async def get(clip_id: str) -> None:
53
+ """Get information about a specific clip."""
54
+ client = ClipClient()
55
+ clip = await client.get_clip(clip_id)
56
+ click.echo(f"ID: {click.style(clip.id, fg='blue')}")
57
+ click.echo(f"Description: {click.style(clip.description or 'N/A', fg='green')}")
58
+ click.echo(f"User ID: {click.style(clip.user_id, fg='yellow')}")
59
+ click.echo(f"Created: {clip.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
60
+ click.echo(f"Downloads: {clip.num_downloads or 0}")
61
+ if clip.file_size:
62
+ click.echo(f"File Size: {clip.file_size:,} bytes")
63
+
64
+
65
+ @cli.command()
66
+ @click.option("-d", "--description", type=str, default=None, help="Description for the clip")
67
+ @coro
68
+ async def create(description: str | None = None) -> None:
69
+ """Create a new clip."""
70
+ async with ClipClient() as client:
71
+ clip = await client.create_clip(description)
72
+ click.echo("Clip created:")
73
+ click.echo(f" ID: {click.style(clip.id, fg='blue')}")
74
+ click.echo(f" Description: {click.style(clip.description or 'N/A', fg='green')}")
75
+ click.echo(f" Created: {clip.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
76
+
77
+
78
+ @cli.command()
79
+ @click.argument("clip_id")
80
+ @click.option("-d", "--description", type=str, default=None, help="New description for the clip")
81
+ @coro
82
+ async def update(clip_id: str, description: str | None = None) -> None:
83
+ """Update a clip's metadata."""
84
+ async with ClipClient() as client:
85
+ clip = await client.update_clip(clip_id, description)
86
+ click.echo("Clip updated:")
87
+ click.echo(f" ID: {click.style(clip.id, fg='blue')}")
88
+ click.echo(f" Description: {click.style(clip.description or 'N/A', fg='green')}")
89
+
90
+
91
+ @cli.command()
92
+ @click.argument("clip_id")
93
+ @click.confirmation_option(prompt="Are you sure you want to delete this clip?")
94
+ @coro
95
+ async def delete(clip_id: str) -> None:
96
+ """Delete a clip."""
97
+ async with ClipClient() as client:
98
+ await client.delete_clip(clip_id)
99
+ click.echo(f"Clip deleted: {click.style(clip_id, fg='red')}")
100
+
101
+
102
+ @cli.command()
103
+ @click.argument("clip_id")
104
+ @click.argument("file_path", type=click.Path(exists=True))
105
+ @coro
106
+ async def upload(clip_id: str, file_path: str) -> None:
107
+ """Upload a file for a clip."""
108
+ async with ClipClient() as client:
109
+ response = await client.upload_clip(clip_id, file_path)
110
+ click.echo("File uploaded:")
111
+ click.echo(f" Filename: {click.style(response.filename, fg='green')}")
112
+ click.echo(f" Content Type: {response.content_type}")
113
+
114
+
115
+ @cli.command()
116
+ @click.argument("clip_id")
117
+ @click.option("-o", "--output", type=click.Path(), help="Output file path")
118
+ @coro
119
+ async def download(clip_id: str, output: str | None = None) -> None:
120
+ """Download a clip file."""
121
+ async with ClipClient() as client:
122
+ if output:
123
+ output_path = await client.download_clip_to_file(clip_id, output)
124
+ click.echo(f"Clip downloaded to: {click.style(str(output_path), fg='green')}")
125
+ else:
126
+ download_response = await client.download_clip(clip_id)
127
+ click.echo(f"Download URL: {click.style(download_response.url, fg='blue')}")
128
+ click.echo(f"MD5 Hash: {download_response.md5_hash}")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ cli()
@@ -0,0 +1,241 @@
1
+ """Defines the CLI for managing groups."""
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.group import GroupClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @click.group()
15
+ def cli() -> None:
16
+ """Manage groups."""
17
+ pass
18
+
19
+
20
+ @cli.command()
21
+ @coro
22
+ async def list() -> None:
23
+ """Lists all groups for the authenticated user."""
24
+ client = GroupClient()
25
+ groups = await client.get_groups()
26
+ if groups:
27
+ # Prepare table data
28
+ table_data = [
29
+ [
30
+ click.style(group.id, fg="blue"),
31
+ click.style(group.name, fg="green"),
32
+ group.description or "N/A",
33
+ group.created_at,
34
+ "Active" if group.is_active else "Inactive",
35
+ ]
36
+ for group in groups
37
+ ]
38
+ click.echo(
39
+ tabulate(
40
+ table_data,
41
+ headers=["ID", "Name", "Description", "Created", "Status"],
42
+ tablefmt="simple",
43
+ )
44
+ )
45
+ else:
46
+ click.echo(click.style("No groups found", fg="red"))
47
+
48
+
49
+ @cli.command()
50
+ @click.argument("group_id")
51
+ @coro
52
+ async def get(group_id: str) -> None:
53
+ """Get information about a specific group."""
54
+ client = GroupClient()
55
+ group = await client.get_group(group_id)
56
+ click.echo(f"ID: {click.style(group.id, fg='blue')}")
57
+ click.echo(f"Name: {click.style(group.name, fg='green')}")
58
+ click.echo(f"Description: {click.style(group.description or 'N/A', fg='yellow')}")
59
+ click.echo(f"Owner ID: {click.style(group.owner_id, fg='cyan')}")
60
+ click.echo(f"Created: {group.created_at}")
61
+ click.echo(f"Updated: {group.updated_at}")
62
+ click.echo(f"Status: {'Active' if group.is_active else 'Inactive'}")
63
+
64
+
65
+ @cli.command()
66
+ @click.argument("name")
67
+ @click.option("-d", "--description", type=str, default=None, help="Description for the group")
68
+ @coro
69
+ async def create(name: str, description: str | None = None) -> None:
70
+ """Create a new group."""
71
+ async with GroupClient() as client:
72
+ group = await client.create_group(name, description)
73
+ click.echo("Group created:")
74
+ click.echo(f" ID: {click.style(group.id, fg='blue')}")
75
+ click.echo(f" Name: {click.style(group.name, fg='green')}")
76
+ click.echo(f" Description: {click.style(group.description or 'N/A', fg='yellow')}")
77
+
78
+
79
+ @cli.command()
80
+ @click.argument("group_id")
81
+ @click.option("-n", "--name", type=str, default=None, help="New name for the group")
82
+ @click.option("-d", "--description", type=str, default=None, help="New description for the group")
83
+ @coro
84
+ async def update(group_id: str, name: str | None = None, description: str | None = None) -> None:
85
+ """Update a group's metadata."""
86
+ async with GroupClient() as client:
87
+ group = await client.update_group(group_id, name, description)
88
+ click.echo("Group updated:")
89
+ click.echo(f" ID: {click.style(group.id, fg='blue')}")
90
+ click.echo(f" Name: {click.style(group.name, fg='green')}")
91
+ click.echo(f" Description: {click.style(group.description or 'N/A', fg='yellow')}")
92
+
93
+
94
+ @cli.command()
95
+ @click.argument("group_id")
96
+ @click.confirmation_option(prompt="Are you sure you want to delete this group?")
97
+ @coro
98
+ async def delete(group_id: str) -> None:
99
+ """Delete a group."""
100
+ async with GroupClient() as client:
101
+ await client.delete_group(group_id)
102
+ click.echo(f"Group deleted: {click.style(group_id, fg='red')}")
103
+
104
+
105
+ @cli.group()
106
+ def membership() -> None:
107
+ """Manage group memberships."""
108
+ pass
109
+
110
+
111
+ @membership.command("list")
112
+ @click.argument("group_id")
113
+ @coro
114
+ async def list_memberships(group_id: str) -> None:
115
+ """List all memberships for a group."""
116
+ client = GroupClient()
117
+ memberships = await client.get_group_memberships(group_id)
118
+ if memberships:
119
+ table_data = [
120
+ [
121
+ click.style(membership.id, fg="blue"),
122
+ click.style(membership.user_id, fg="green"),
123
+ membership.status,
124
+ membership.requested_at,
125
+ membership.approved_at or "N/A",
126
+ membership.approved_by or "N/A",
127
+ ]
128
+ for membership in memberships
129
+ ]
130
+ click.echo(
131
+ tabulate(
132
+ table_data,
133
+ headers=["ID", "User ID", "Status", "Requested", "Approved", "Approved By"],
134
+ tablefmt="simple",
135
+ )
136
+ )
137
+ else:
138
+ click.echo(click.style("No memberships found", fg="red"))
139
+
140
+
141
+ @membership.command("request")
142
+ @click.argument("group_id")
143
+ @coro
144
+ async def request_membership(group_id: str) -> None:
145
+ """Request to join a group."""
146
+ async with GroupClient() as client:
147
+ membership = await client.request_group_membership(group_id)
148
+ click.echo("Membership request sent:")
149
+ click.echo(f" ID: {click.style(membership.id, fg='blue')}")
150
+ click.echo(f" Status: {membership.status}")
151
+
152
+
153
+ @membership.command("approve")
154
+ @click.argument("group_id")
155
+ @click.argument("user_id")
156
+ @coro
157
+ async def approve_membership(group_id: str, user_id: str) -> None:
158
+ """Approve a membership request."""
159
+ async with GroupClient() as client:
160
+ membership = await client.approve_group_membership(group_id, user_id)
161
+ click.echo("Membership approved:")
162
+ click.echo(f" ID: {click.style(membership.id, fg='blue')}")
163
+ click.echo(f" Status: {membership.status}")
164
+
165
+
166
+ @membership.command("reject")
167
+ @click.argument("group_id")
168
+ @click.argument("user_id")
169
+ @click.confirmation_option(prompt="Are you sure you want to reject this membership?")
170
+ @coro
171
+ async def reject_membership(group_id: str, user_id: str) -> None:
172
+ """Reject a membership request."""
173
+ async with GroupClient() as client:
174
+ await client.reject_group_membership(group_id, user_id)
175
+ click.echo(f"Membership rejected for user: {click.style(user_id, fg='red')}")
176
+
177
+
178
+ @cli.group()
179
+ def share() -> None:
180
+ """Manage group resource sharing."""
181
+ pass
182
+
183
+
184
+ @share.command("list")
185
+ @click.argument("group_id")
186
+ @coro
187
+ async def list_shares(group_id: str) -> None:
188
+ """List all resources shared with a group."""
189
+ client = GroupClient()
190
+ shares = await client.get_group_shares(group_id)
191
+ if shares:
192
+ table_data = [
193
+ [
194
+ click.style(share.id, fg="blue"),
195
+ share.resource_type,
196
+ click.style(share.resource_id, fg="green"),
197
+ click.style(share.shared_by, fg="yellow"),
198
+ share.shared_at,
199
+ ]
200
+ for share in shares
201
+ ]
202
+ click.echo(
203
+ tabulate(
204
+ table_data,
205
+ headers=["ID", "Type", "Resource ID", "Shared By", "Shared At"],
206
+ tablefmt="simple",
207
+ )
208
+ )
209
+ else:
210
+ click.echo(click.style("No shared resources found", fg="red"))
211
+
212
+
213
+ @share.command("add")
214
+ @click.argument("group_id")
215
+ @click.argument("resource_type")
216
+ @click.argument("resource_id")
217
+ @coro
218
+ async def add_share(group_id: str, resource_type: str, resource_id: str) -> None:
219
+ """Share a resource with a group."""
220
+ async with GroupClient() as client:
221
+ share = await client.share_resource_with_group(group_id, resource_type, resource_id)
222
+ click.echo("Resource shared:")
223
+ click.echo(f" ID: {click.style(share.id, fg='blue')}")
224
+ click.echo(f" Type: {share.resource_type}")
225
+ click.echo(f" Resource ID: {click.style(share.resource_id, fg='green')}")
226
+
227
+
228
+ @share.command("remove")
229
+ @click.argument("group_id")
230
+ @click.argument("share_id")
231
+ @click.confirmation_option(prompt="Are you sure you want to remove this share?")
232
+ @coro
233
+ async def remove_share(group_id: str, share_id: str) -> None:
234
+ """Remove a resource share from a group."""
235
+ async with GroupClient() as client:
236
+ await client.unshare_resource_from_group(group_id, share_id)
237
+ click.echo(f"Share removed: {click.style(share_id, fg='red')}")
238
+
239
+
240
+ if __name__ == "__main__":
241
+ cli()
@@ -0,0 +1,110 @@
1
+ """Defines the CLI for managing permissions."""
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.permission import PermissionClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @click.group()
15
+ def cli() -> None:
16
+ """Manage user permissions."""
17
+ pass
18
+
19
+
20
+ @cli.command("list-all")
21
+ @coro
22
+ async def list_all_permissions() -> None:
23
+ """List all available permissions."""
24
+ client = PermissionClient()
25
+ permissions = await client.get_all_permissions()
26
+ if permissions:
27
+ table_data = [
28
+ [
29
+ click.style(perm.permission, fg="blue"),
30
+ perm.description,
31
+ ]
32
+ for perm in permissions
33
+ ]
34
+ click.echo(
35
+ tabulate(
36
+ table_data,
37
+ headers=["Permission", "Description"],
38
+ tablefmt="simple",
39
+ )
40
+ )
41
+ else:
42
+ click.echo(click.style("No permissions found", fg="red"))
43
+
44
+
45
+ @cli.command()
46
+ @click.option("-u", "--user-id", type=str, default="me", help="User ID (default: me)")
47
+ @coro
48
+ async def list(user_id: str = "me") -> None:
49
+ """List permissions for a user."""
50
+ client = PermissionClient()
51
+ user_perms = await client.get_user_permissions(user_id)
52
+
53
+ click.echo(f"User: {click.style(user_perms.display_name, fg='green')} ({user_perms.email})")
54
+ click.echo(f"User ID: {click.style(user_perms.user_id, fg='blue')}")
55
+ click.echo(f"Status: {'Active' if user_perms.is_active else 'Inactive'}")
56
+ click.echo("\nPermissions:")
57
+
58
+ if user_perms.permissions:
59
+ for perm in user_perms.permissions:
60
+ click.echo(f" • {click.style(perm, fg='yellow')}")
61
+ else:
62
+ click.echo(click.style(" No permissions assigned", fg="red"))
63
+
64
+
65
+ @cli.command()
66
+ @click.argument("user_id")
67
+ @click.argument("permissions", nargs=-1, required=True)
68
+ @coro
69
+ async def set(user_id: str, permissions: tuple[str, ...]) -> None:
70
+ """Set permissions for a user (replaces all existing permissions)."""
71
+ async with PermissionClient() as client:
72
+ user_perms = await client.update_user_permissions(user_id, list(permissions))
73
+
74
+ click.echo("Permissions updated:")
75
+ click.echo(f" User: {click.style(user_perms.display_name, fg='green')}")
76
+ click.echo(f" Permissions: {', '.join([click.style(p, fg='yellow') for p in user_perms.permissions])}")
77
+
78
+
79
+ @cli.command()
80
+ @click.argument("user_id")
81
+ @click.argument("permission")
82
+ @coro
83
+ async def add(user_id: str, permission: str) -> None:
84
+ """Add a permission to a user."""
85
+ async with PermissionClient() as client:
86
+ user_perms = await client.add_user_permission(user_id, permission)
87
+
88
+ click.echo("Permission added:")
89
+ click.echo(f" User: {click.style(user_perms.display_name, fg='green')}")
90
+ click.echo(f" Added: {click.style(permission, fg='yellow')}")
91
+ click.echo(f" All permissions: {', '.join([click.style(p, fg='yellow') for p in user_perms.permissions])}")
92
+
93
+
94
+ @cli.command()
95
+ @click.argument("user_id")
96
+ @click.argument("permission")
97
+ @coro
98
+ async def remove(user_id: str, permission: str) -> None:
99
+ """Remove a permission from a user."""
100
+ async with PermissionClient() as client:
101
+ user_perms = await client.remove_user_permission(user_id, permission)
102
+
103
+ click.echo("Permission removed:")
104
+ click.echo(f" User: {click.style(user_perms.display_name, fg='green')}")
105
+ click.echo(f" Removed: {click.style(permission, fg='red')}")
106
+ click.echo(f" Remaining permissions: {', '.join([click.style(p, fg='yellow') for p in user_perms.permissions])}")
107
+
108
+
109
+ if __name__ == "__main__":
110
+ cli()
@@ -192,7 +192,7 @@ async def run_pybullet(
192
192
  ) -> None:
193
193
  """Shows the URDF file for a robot class in PyBullet."""
194
194
  try:
195
- import pybullet as p
195
+ import pybullet as p # noqa: PLC0415
196
196
  except ImportError:
197
197
  click.echo(click.style("PyBullet is not installed; install it with `pip install pybullet`", fg="red"))
198
198
  return
@@ -453,14 +453,14 @@ async def run_mujoco(class_name: str, scene: str, no_cache: bool) -> None:
453
453
  launches the Mujoco viewer using the provided MJCF file.
454
454
  """
455
455
  try:
456
- from mujoco_scenes.errors import TemplateNotFoundError
457
- from mujoco_scenes.mjcf import list_scenes, load_mjmodel
456
+ from mujoco_scenes.errors import TemplateNotFoundError # noqa: PLC0415
457
+ from mujoco_scenes.mjcf import list_scenes, load_mjmodel # noqa: PLC0415
458
458
  except ImportError:
459
459
  click.echo(click.style("Mujoco Scenes is required; install with `pip install mujoco-scenes`", fg="red"))
460
460
  return
461
461
 
462
462
  try:
463
- import mujoco.viewer
463
+ import mujoco.viewer # noqa: PLC0415
464
464
  except ImportError:
465
465
  click.echo(click.style("Mujoco is required; install with `pip install mujoco`", fg="red"))
466
466
  return
kscale/web/cli/user.py CHANGED
@@ -30,8 +30,6 @@ async def me() -> None:
30
30
  ["Email verified", profile.email_verified],
31
31
  ["User ID", profile.user.user_id],
32
32
  ["Is admin", profile.user.is_admin],
33
- ["Can upload", profile.user.can_upload],
34
- ["Can test", profile.user.can_test],
35
33
  ],
36
34
  headers=["Key", "Value"],
37
35
  tablefmt="simple",
@@ -39,14 +37,5 @@ async def me() -> None:
39
37
  )
40
38
 
41
39
 
42
- @cli.command()
43
- @coro
44
- async def key() -> None:
45
- """Get an API key for the currently-authenticated user."""
46
- client = UserClient()
47
- api_key = await client.get_api_key()
48
- click.echo(f"API key: {click.style(api_key, fg='green')}")
49
-
50
-
51
40
  if __name__ == "__main__":
52
41
  cli()