kscale 0.3.17__tar.gz → 0.3.18__tar.gz
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-0.3.17/kscale.egg-info → kscale-0.3.18}/PKG-INFO +1 -1
- {kscale-0.3.17 → kscale-0.3.18}/kscale/__init__.py +1 -1
- {kscale-0.3.17 → kscale-0.3.18}/kscale/cli.py +6 -0
- kscale-0.3.18/kscale/web/cli/clip.py +132 -0
- kscale-0.3.18/kscale/web/cli/group.py +241 -0
- kscale-0.3.18/kscale/web/cli/permission.py +110 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/cli/user.py +0 -2
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/base.py +1 -1
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/client.py +6 -0
- kscale-0.3.18/kscale/web/clients/clip.py +118 -0
- kscale-0.3.18/kscale/web/clients/group.py +85 -0
- kscale-0.3.18/kscale/web/clients/permission.py +35 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/gen/api.py +100 -3
- {kscale-0.3.17 → kscale-0.3.18/kscale.egg-info}/PKG-INFO +1 -1
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/SOURCES.txt +6 -0
- {kscale-0.3.17 → kscale-0.3.18}/pyproject.toml +0 -1
- {kscale-0.3.17 → kscale-0.3.18}/LICENSE +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/MANIFEST.in +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/README.md +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/artifacts/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/artifacts/plane.obj +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/artifacts/plane.urdf +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/conf.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/py.typed +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/requirements-dev.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/requirements.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/utils/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/utils/api_base.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/utils/checksum.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/utils/cli.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/cli/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/cli/robot.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/cli/robot_class.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/robot.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/robot_class.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/clients/user.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/gen/__init__.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale/web/utils.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/dependency_links.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/entry_points.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/not-zip-safe +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/requires.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/kscale.egg-info/top_level.txt +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/setup.cfg +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/setup.py +0 -0
- {kscale-0.3.17 → kscale-0.3.18}/tests/test_dummy.py +0 -0
@@ -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
|
@@ -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()
|
@@ -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",
|
@@ -90,7 +90,7 @@ class BaseClient:
|
|
90
90
|
logger.info("Use KSCALE_VERBOSE_ERROR=1 to see the full error message")
|
91
91
|
logger.info("If this persists, please create an issue here: https://github.com/kscalelabs/kscale")
|
92
92
|
|
93
|
-
logger.error("Got error %d from the K-Scale API", error_code)
|
93
|
+
logger.error("Got error %d from the K-Scale API %s endpoint %s", error_code, method, url)
|
94
94
|
if isinstance(error_json, Mapping):
|
95
95
|
for key, value in error_json.items():
|
96
96
|
logger.error(" [%s] %s", key, value)
|
@@ -1,6 +1,9 @@
|
|
1
1
|
"""Defines a unified client for the K-Scale WWW API."""
|
2
2
|
|
3
3
|
from kscale.web.clients.base import BaseClient
|
4
|
+
from kscale.web.clients.clip import ClipClient
|
5
|
+
from kscale.web.clients.group import GroupClient
|
6
|
+
from kscale.web.clients.permission import PermissionClient
|
4
7
|
from kscale.web.clients.robot import RobotClient
|
5
8
|
from kscale.web.clients.robot_class import RobotClassClient
|
6
9
|
from kscale.web.clients.user import UserClient
|
@@ -10,6 +13,9 @@ class WWWClient(
|
|
10
13
|
RobotClient,
|
11
14
|
RobotClassClient,
|
12
15
|
UserClient,
|
16
|
+
ClipClient,
|
17
|
+
GroupClient,
|
18
|
+
PermissionClient,
|
13
19
|
BaseClient,
|
14
20
|
):
|
15
21
|
pass
|
@@ -0,0 +1,118 @@
|
|
1
|
+
"""Defines the client for interacting with the K-Scale clip endpoints."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import logging
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import httpx
|
8
|
+
|
9
|
+
from kscale.web.clients.base import BaseClient
|
10
|
+
from kscale.web.gen.api import (
|
11
|
+
Clip,
|
12
|
+
ClipDownloadResponse,
|
13
|
+
ClipUploadResponse,
|
14
|
+
)
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
UPLOAD_TIMEOUT = 300.0
|
19
|
+
DOWNLOAD_TIMEOUT = 60.0
|
20
|
+
|
21
|
+
|
22
|
+
class ClipClient(BaseClient):
|
23
|
+
async def get_clips(self) -> list[Clip]:
|
24
|
+
"""Get all clips for the authenticated user."""
|
25
|
+
data = await self._request("GET", "/clips/", auth=True)
|
26
|
+
return [Clip.model_validate(item) for item in data]
|
27
|
+
|
28
|
+
async def get_clip(self, clip_id: str) -> Clip:
|
29
|
+
"""Get a specific clip by ID."""
|
30
|
+
data = await self._request("GET", f"/clips/{clip_id}", auth=True)
|
31
|
+
return Clip.model_validate(data)
|
32
|
+
|
33
|
+
async def create_clip(self, description: str | None = None) -> Clip:
|
34
|
+
"""Create a new clip."""
|
35
|
+
data = {}
|
36
|
+
if description is not None:
|
37
|
+
data["description"] = description
|
38
|
+
response_data = await self._request("POST", "/clips/", data=data, auth=True)
|
39
|
+
return Clip.model_validate(response_data)
|
40
|
+
|
41
|
+
async def update_clip(self, clip_id: str, new_description: str | None = None) -> Clip:
|
42
|
+
"""Update a clip's metadata."""
|
43
|
+
data = {}
|
44
|
+
if new_description is not None:
|
45
|
+
data["new_description"] = new_description
|
46
|
+
response_data = await self._request("POST", f"/clips/{clip_id}", data=data, auth=True)
|
47
|
+
return Clip.model_validate(response_data)
|
48
|
+
|
49
|
+
async def delete_clip(self, clip_id: str) -> None:
|
50
|
+
"""Delete a clip."""
|
51
|
+
await self._request("DELETE", f"/clips/{clip_id}", auth=True)
|
52
|
+
|
53
|
+
async def upload_clip(self, clip_id: str, file_path: str | Path) -> ClipUploadResponse:
|
54
|
+
"""Upload a file for a clip."""
|
55
|
+
if not (file_path := Path(file_path)).exists():
|
56
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
57
|
+
|
58
|
+
# Determine content type based on file extension
|
59
|
+
ext = file_path.suffix.lower()
|
60
|
+
content_type_map = {
|
61
|
+
".mp4": "video/mp4",
|
62
|
+
".avi": "video/x-msvideo",
|
63
|
+
".mov": "video/quicktime",
|
64
|
+
".mkv": "video/x-matroska",
|
65
|
+
".webm": "video/webm",
|
66
|
+
".json": "application/json",
|
67
|
+
".txt": "text/plain",
|
68
|
+
}
|
69
|
+
content_type = content_type_map.get(ext, "application/octet-stream")
|
70
|
+
|
71
|
+
# Get upload URL
|
72
|
+
data = await self._request(
|
73
|
+
"PUT",
|
74
|
+
f"/clips/{clip_id}/upload",
|
75
|
+
data={"filename": file_path.name, "content_type": content_type},
|
76
|
+
auth=True,
|
77
|
+
)
|
78
|
+
response = ClipUploadResponse.model_validate(data)
|
79
|
+
|
80
|
+
# Upload the file
|
81
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(UPLOAD_TIMEOUT)) as client:
|
82
|
+
async with client.stream(
|
83
|
+
"PUT",
|
84
|
+
response.url,
|
85
|
+
content=file_path.read_bytes(),
|
86
|
+
headers={"Content-Type": response.content_type},
|
87
|
+
) as r:
|
88
|
+
r.raise_for_status()
|
89
|
+
|
90
|
+
return response
|
91
|
+
|
92
|
+
async def download_clip(self, clip_id: str) -> ClipDownloadResponse:
|
93
|
+
"""Get download URL and metadata for a clip."""
|
94
|
+
data = await self._request("GET", f"/clips/{clip_id}/download", auth=True)
|
95
|
+
return ClipDownloadResponse.model_validate(data)
|
96
|
+
|
97
|
+
async def download_clip_to_file(self, clip_id: str, output_path: str | Path) -> Path:
|
98
|
+
"""Download a clip to a local file."""
|
99
|
+
download_response = await self.download_clip(clip_id)
|
100
|
+
output_path = Path(output_path)
|
101
|
+
|
102
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(DOWNLOAD_TIMEOUT)) as client:
|
103
|
+
async with client.stream("GET", download_response.url) as response:
|
104
|
+
response.raise_for_status()
|
105
|
+
with output_path.open("wb") as f:
|
106
|
+
async for chunk in response.aiter_bytes():
|
107
|
+
f.write(chunk)
|
108
|
+
|
109
|
+
# Verify download integrity if hash is provided
|
110
|
+
if download_response.md5_hash:
|
111
|
+
with output_path.open("rb") as f:
|
112
|
+
file_hash = hashlib.md5(f.read()).hexdigest()
|
113
|
+
if file_hash != download_response.md5_hash:
|
114
|
+
raise ValueError(
|
115
|
+
f"Downloaded file hash mismatch: expected {download_response.md5_hash}, got {file_hash}"
|
116
|
+
)
|
117
|
+
|
118
|
+
return output_path
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Defines the client for interacting with the K-Scale group endpoints."""
|
2
|
+
|
3
|
+
from kscale.web.clients.base import BaseClient
|
4
|
+
from kscale.web.gen.api import (
|
5
|
+
GroupMembershipResponse,
|
6
|
+
GroupResponse,
|
7
|
+
GroupShareResponse,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class GroupClient(BaseClient):
|
12
|
+
async def get_groups(self) -> list[GroupResponse]:
|
13
|
+
"""Get all groups for the authenticated user."""
|
14
|
+
data = await self._request("GET", "/groups/", auth=True)
|
15
|
+
return [GroupResponse.model_validate(item) for item in data]
|
16
|
+
|
17
|
+
async def get_group(self, group_id: str) -> GroupResponse:
|
18
|
+
"""Get a specific group by ID."""
|
19
|
+
data = await self._request("GET", f"/groups/{group_id}", auth=True)
|
20
|
+
return GroupResponse.model_validate(data)
|
21
|
+
|
22
|
+
async def create_group(self, name: str, description: str | None = None) -> GroupResponse:
|
23
|
+
"""Create a new group."""
|
24
|
+
data = {"name": name}
|
25
|
+
if description is not None:
|
26
|
+
data["description"] = description
|
27
|
+
response_data = await self._request("POST", "/groups/", data=data, auth=True)
|
28
|
+
return GroupResponse.model_validate(response_data)
|
29
|
+
|
30
|
+
async def update_group(
|
31
|
+
self,
|
32
|
+
group_id: str,
|
33
|
+
name: str | None = None,
|
34
|
+
description: str | None = None,
|
35
|
+
) -> GroupResponse:
|
36
|
+
"""Update a group's metadata."""
|
37
|
+
data = {}
|
38
|
+
if name is not None:
|
39
|
+
data["name"] = name
|
40
|
+
if description is not None:
|
41
|
+
data["description"] = description
|
42
|
+
response_data = await self._request("POST", f"/groups/{group_id}", data=data, auth=True)
|
43
|
+
return GroupResponse.model_validate(response_data)
|
44
|
+
|
45
|
+
async def delete_group(self, group_id: str) -> None:
|
46
|
+
"""Delete a group."""
|
47
|
+
await self._request("DELETE", f"/groups/{group_id}", auth=True)
|
48
|
+
|
49
|
+
# Group membership management
|
50
|
+
async def get_group_memberships(self, group_id: str) -> list[GroupMembershipResponse]:
|
51
|
+
"""Get all memberships for a group."""
|
52
|
+
data = await self._request("GET", f"/groups/{group_id}/memberships", auth=True)
|
53
|
+
return [GroupMembershipResponse.model_validate(item) for item in data]
|
54
|
+
|
55
|
+
async def request_group_membership(self, group_id: str) -> GroupMembershipResponse:
|
56
|
+
"""Request to join a group."""
|
57
|
+
data = await self._request("POST", f"/groups/{group_id}/memberships", auth=True)
|
58
|
+
return GroupMembershipResponse.model_validate(data)
|
59
|
+
|
60
|
+
async def approve_group_membership(self, group_id: str, user_id: str) -> GroupMembershipResponse:
|
61
|
+
"""Approve a membership request."""
|
62
|
+
data = await self._request("POST", f"/groups/{group_id}/memberships/{user_id}/approve", auth=True)
|
63
|
+
return GroupMembershipResponse.model_validate(data)
|
64
|
+
|
65
|
+
async def reject_group_membership(self, group_id: str, user_id: str) -> None:
|
66
|
+
"""Reject a membership request."""
|
67
|
+
await self._request("DELETE", f"/groups/{group_id}/memberships/{user_id}", auth=True)
|
68
|
+
|
69
|
+
# Group sharing management
|
70
|
+
async def get_group_shares(self, group_id: str) -> list[GroupShareResponse]:
|
71
|
+
"""Get all resources shared with a group."""
|
72
|
+
data = await self._request("GET", f"/groups/{group_id}/shares", auth=True)
|
73
|
+
return [GroupShareResponse.model_validate(item) for item in data]
|
74
|
+
|
75
|
+
async def share_resource_with_group(
|
76
|
+
self, group_id: str, resource_type: str, resource_id: str
|
77
|
+
) -> GroupShareResponse:
|
78
|
+
"""Share a resource with a group."""
|
79
|
+
data = {"resource_type": resource_type, "resource_id": resource_id}
|
80
|
+
response_data = await self._request("POST", f"/groups/{group_id}/shares", data=data, auth=True)
|
81
|
+
return GroupShareResponse.model_validate(response_data)
|
82
|
+
|
83
|
+
async def unshare_resource_from_group(self, group_id: str, share_id: str) -> None:
|
84
|
+
"""Remove a resource share from a group."""
|
85
|
+
await self._request("DELETE", f"/groups/{group_id}/shares/{share_id}", auth=True)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""Defines the client for interacting with the K-Scale permission endpoints."""
|
2
|
+
|
3
|
+
from kscale.web.clients.base import BaseClient
|
4
|
+
from kscale.web.gen.api import PermissionResponse, UserPermissionsResponse
|
5
|
+
|
6
|
+
|
7
|
+
class PermissionClient(BaseClient):
|
8
|
+
async def get_all_permissions(self) -> list[PermissionResponse]:
|
9
|
+
"""Get all available permissions."""
|
10
|
+
data = await self._request("GET", "/permissions/list")
|
11
|
+
return [PermissionResponse.model_validate(item) for item in data]
|
12
|
+
|
13
|
+
async def get_user_permissions(self, user_id: str = "me") -> UserPermissionsResponse:
|
14
|
+
"""Get permissions for a specific user."""
|
15
|
+
data = {"user_id": user_id}
|
16
|
+
data = await self._request("GET", "/permissions/user", data=data, auth=True)
|
17
|
+
return UserPermissionsResponse.model_validate(data)
|
18
|
+
|
19
|
+
async def update_user_permissions(self, user_id: str, permissions: list[str]) -> UserPermissionsResponse:
|
20
|
+
"""Update permissions for a user."""
|
21
|
+
data = {"user_id": user_id, "permissions": permissions}
|
22
|
+
response_data = await self._request("PUT", "/permissions/user", data=data, auth=True)
|
23
|
+
return UserPermissionsResponse.model_validate(response_data)
|
24
|
+
|
25
|
+
async def add_user_permission(self, user_id: str, permission: str) -> UserPermissionsResponse:
|
26
|
+
"""Add a single permission to a user."""
|
27
|
+
data = {"user_id": user_id, "permission": permission}
|
28
|
+
response_data = await self._request("POST", "/permissions/user", data=data, auth=True)
|
29
|
+
return UserPermissionsResponse.model_validate(response_data)
|
30
|
+
|
31
|
+
async def remove_user_permission(self, user_id: str, permission: str) -> UserPermissionsResponse:
|
32
|
+
"""Remove a single permission from a user."""
|
33
|
+
params = {"user_id": user_id, "permission": permission}
|
34
|
+
response_data = await self._request("DELETE", "/permissions/user", params=params, auth=True)
|
35
|
+
return UserPermissionsResponse.model_validate(response_data)
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# generated by datamodel-codegen:
|
4
4
|
# filename: openapi.json
|
5
|
-
# timestamp: 2025-
|
5
|
+
# timestamp: 2025-09-03T23:04:34+00:00
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
@@ -24,6 +24,7 @@ class APIKey(BaseModel):
|
|
24
24
|
last_used_at: Optional[datetime] = Field(None, title="Last Used At")
|
25
25
|
expires_at: Optional[datetime] = Field(None, title="Expires At")
|
26
26
|
is_active: Optional[bool] = Field(True, title="Is Active")
|
27
|
+
is_admin: Optional[bool] = Field(False, title="Is Admin")
|
27
28
|
|
28
29
|
|
29
30
|
class APIKeyRequest(BaseModel):
|
@@ -78,6 +79,14 @@ class ActuatorMetadataOutput(BaseModel):
|
|
78
79
|
error_gain: Optional[str] = Field(None, title="Error Gain")
|
79
80
|
|
80
81
|
|
82
|
+
class AddClipRequest(BaseModel):
|
83
|
+
description: Optional[str] = Field(None, title="Description")
|
84
|
+
|
85
|
+
|
86
|
+
class AddPermissionRequest(BaseModel):
|
87
|
+
permission: str = Field(..., title="Permission")
|
88
|
+
|
89
|
+
|
81
90
|
class AddRobotClassRequest(BaseModel):
|
82
91
|
description: Optional[str] = Field(None, title="Description")
|
83
92
|
|
@@ -111,6 +120,31 @@ class AgentUploadResponse(BaseModel):
|
|
111
120
|
content_type: str = Field(..., title="Content Type")
|
112
121
|
|
113
122
|
|
123
|
+
class Clip(BaseModel):
|
124
|
+
id: str = Field(..., title="Id")
|
125
|
+
description: Optional[str] = Field(None, title="Description")
|
126
|
+
user_id: str = Field(..., title="User Id")
|
127
|
+
created_at: datetime = Field(..., title="Created At")
|
128
|
+
num_downloads: Optional[int] = Field(0, title="Num Downloads")
|
129
|
+
file_size: Optional[int] = Field(None, title="File Size")
|
130
|
+
|
131
|
+
|
132
|
+
class ClipDownloadResponse(BaseModel):
|
133
|
+
url: str = Field(..., title="Url")
|
134
|
+
md5_hash: str = Field(..., title="Md5 Hash")
|
135
|
+
|
136
|
+
|
137
|
+
class ClipUploadRequest(BaseModel):
|
138
|
+
filename: str = Field(..., title="Filename")
|
139
|
+
content_type: str = Field(..., title="Content Type")
|
140
|
+
|
141
|
+
|
142
|
+
class ClipUploadResponse(BaseModel):
|
143
|
+
url: str = Field(..., title="Url")
|
144
|
+
filename: str = Field(..., title="Filename")
|
145
|
+
content_type: str = Field(..., title="Content Type")
|
146
|
+
|
147
|
+
|
114
148
|
class CreateAPIKeyRequest(BaseModel):
|
115
149
|
name: str = Field(..., title="Name")
|
116
150
|
permissions: List[str] = Field(..., title="Permissions")
|
@@ -126,6 +160,45 @@ class CreateAgentRequest(BaseModel):
|
|
126
160
|
description: Optional[str] = Field(None, title="Description")
|
127
161
|
|
128
162
|
|
163
|
+
class CreateGroupRequest(BaseModel):
|
164
|
+
name: str = Field(..., title="Name")
|
165
|
+
description: Optional[str] = Field(None, title="Description")
|
166
|
+
|
167
|
+
|
168
|
+
class GroupMembershipResponse(BaseModel):
|
169
|
+
id: str = Field(..., title="Id")
|
170
|
+
group_id: str = Field(..., title="Group Id")
|
171
|
+
user_id: str = Field(..., title="User Id")
|
172
|
+
status: str = Field(..., title="Status")
|
173
|
+
requested_at: str = Field(..., title="Requested At")
|
174
|
+
approved_at: Optional[str] = Field(..., title="Approved At")
|
175
|
+
approved_by: Optional[str] = Field(..., title="Approved By")
|
176
|
+
|
177
|
+
|
178
|
+
class GroupResponse(BaseModel):
|
179
|
+
id: str = Field(..., title="Id")
|
180
|
+
name: str = Field(..., title="Name")
|
181
|
+
description: Optional[str] = Field(..., title="Description")
|
182
|
+
owner_id: str = Field(..., title="Owner Id")
|
183
|
+
created_at: str = Field(..., title="Created At")
|
184
|
+
updated_at: str = Field(..., title="Updated At")
|
185
|
+
is_active: bool = Field(..., title="Is Active")
|
186
|
+
|
187
|
+
|
188
|
+
class GroupShareRequest(BaseModel):
|
189
|
+
resource_type: str = Field(..., title="Resource Type")
|
190
|
+
resource_id: str = Field(..., title="Resource Id")
|
191
|
+
|
192
|
+
|
193
|
+
class GroupShareResponse(BaseModel):
|
194
|
+
id: str = Field(..., title="Id")
|
195
|
+
group_id: str = Field(..., title="Group Id")
|
196
|
+
resource_type: str = Field(..., title="Resource Type")
|
197
|
+
resource_id: str = Field(..., title="Resource Id")
|
198
|
+
shared_by: str = Field(..., title="Shared By")
|
199
|
+
shared_at: str = Field(..., title="Shared At")
|
200
|
+
|
201
|
+
|
129
202
|
class JointMetadataInput(BaseModel):
|
130
203
|
id: Optional[int] = Field(None, title="Id")
|
131
204
|
kp: Optional[Union[float, str]] = Field(None, title="Kp")
|
@@ -161,6 +234,11 @@ class OIDCInfo(BaseModel):
|
|
161
234
|
client_id: str = Field(..., title="Client Id")
|
162
235
|
|
163
236
|
|
237
|
+
class PermissionResponse(BaseModel):
|
238
|
+
permission: str = Field(..., title="Permission")
|
239
|
+
description: str = Field(..., title="Description")
|
240
|
+
|
241
|
+
|
164
242
|
class Robot(BaseModel):
|
165
243
|
id: str = Field(..., title="Id")
|
166
244
|
robot_name: str = Field(..., title="Robot Name")
|
@@ -217,6 +295,15 @@ class UpdateAgentRequest(BaseModel):
|
|
217
295
|
description: Optional[str] = Field(None, title="Description")
|
218
296
|
|
219
297
|
|
298
|
+
class UpdateClipRequest(BaseModel):
|
299
|
+
new_description: Optional[str] = Field(None, title="New Description")
|
300
|
+
|
301
|
+
|
302
|
+
class UpdateGroupRequest(BaseModel):
|
303
|
+
name: Optional[str] = Field(None, title="Name")
|
304
|
+
description: Optional[str] = Field(None, title="Description")
|
305
|
+
|
306
|
+
|
220
307
|
class UpdateRobotClassRequest(BaseModel):
|
221
308
|
new_class_name: Optional[str] = Field(None, title="New Class Name")
|
222
309
|
new_description: Optional[str] = Field(None, title="New Description")
|
@@ -228,11 +315,21 @@ class UpdateRobotRequest(BaseModel):
|
|
228
315
|
new_description: Optional[str] = Field(None, title="New Description")
|
229
316
|
|
230
317
|
|
318
|
+
class UpdateUserPermissionsRequest(BaseModel):
|
319
|
+
permissions: List[str] = Field(..., title="Permissions")
|
320
|
+
|
321
|
+
|
322
|
+
class UserPermissionsResponse(BaseModel):
|
323
|
+
user_id: str = Field(..., title="User Id")
|
324
|
+
display_name: str = Field(..., title="Display Name")
|
325
|
+
email: str = Field(..., title="Email")
|
326
|
+
permissions: List[str] = Field(..., title="Permissions")
|
327
|
+
is_active: bool = Field(..., title="Is Active")
|
328
|
+
|
329
|
+
|
231
330
|
class UserResponse(BaseModel):
|
232
331
|
user_id: str = Field(..., title="User Id")
|
233
332
|
is_admin: bool = Field(..., title="Is Admin")
|
234
|
-
can_upload: bool = Field(..., title="Can Upload")
|
235
|
-
can_test: bool = Field(..., title="Can Test")
|
236
333
|
|
237
334
|
|
238
335
|
class ValidationError(BaseModel):
|
@@ -26,12 +26,18 @@ kscale/utils/cli.py
|
|
26
26
|
kscale/web/__init__.py
|
27
27
|
kscale/web/utils.py
|
28
28
|
kscale/web/cli/__init__.py
|
29
|
+
kscale/web/cli/clip.py
|
30
|
+
kscale/web/cli/group.py
|
31
|
+
kscale/web/cli/permission.py
|
29
32
|
kscale/web/cli/robot.py
|
30
33
|
kscale/web/cli/robot_class.py
|
31
34
|
kscale/web/cli/user.py
|
32
35
|
kscale/web/clients/__init__.py
|
33
36
|
kscale/web/clients/base.py
|
34
37
|
kscale/web/clients/client.py
|
38
|
+
kscale/web/clients/clip.py
|
39
|
+
kscale/web/clients/group.py
|
40
|
+
kscale/web/clients/permission.py
|
35
41
|
kscale/web/clients/robot.py
|
36
42
|
kscale/web/clients/robot_class.py
|
37
43
|
kscale/web/clients/user.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|