kscale 0.0.2__py3-none-any.whl → 0.0.5__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- kscale/__init__.py +5 -1
- kscale/api.py +14 -0
- kscale/conf.py +1 -1
- kscale/store/__init__.py +0 -1
- kscale/store/api.py +20 -0
- kscale/store/cli.py +9 -4
- kscale/store/client.py +79 -0
- kscale/store/gen/api.py +86 -52
- kscale/store/pybullet.py +9 -10
- kscale/store/urdf.py +108 -106
- kscale/store/utils.py +19 -0
- kscale/utils/__init__.py +0 -0
- kscale/utils/api_base.py +6 -0
- {kscale-0.0.2.dist-info → kscale-0.0.5.dist-info}/METADATA +34 -4
- kscale-0.0.5.dist-info/RECORD +21 -0
- {kscale-0.0.2.dist-info → kscale-0.0.5.dist-info}/WHEEL +1 -1
- kscale-0.0.5.dist-info/entry_points.txt +2 -0
- kscale-0.0.2.dist-info/RECORD +0 -15
- kscale-0.0.2.dist-info/entry_points.txt +0 -2
- {kscale-0.0.2.dist-info → kscale-0.0.5.dist-info}/LICENSE +0 -0
- {kscale-0.0.2.dist-info → kscale-0.0.5.dist-info}/top_level.txt +0 -0
kscale/__init__.py
CHANGED
kscale/api.py
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
"""Defines common functionality for the K-Scale API."""
|
2
|
+
|
3
|
+
from kscale.store.api import StoreAPI
|
4
|
+
from kscale.utils.api_base import APIBase
|
5
|
+
|
6
|
+
|
7
|
+
class KScale(
|
8
|
+
StoreAPI,
|
9
|
+
APIBase,
|
10
|
+
):
|
11
|
+
"""Defines a common interface for the K-Scale API."""
|
12
|
+
|
13
|
+
def __init__(self, api_key: str | None = None) -> None:
|
14
|
+
self.api_key = api_key
|
kscale/conf.py
CHANGED
@@ -17,7 +17,7 @@ def get_path() -> Path:
|
|
17
17
|
|
18
18
|
@dataclass
|
19
19
|
class StoreSettings:
|
20
|
-
api_key: str = field(default=
|
20
|
+
api_key: str | None = field(default=None)
|
21
21
|
cache_dir: str = field(default=II("oc.env:KSCALE_CACHE_DIR,'~/.kscale/cache/'"))
|
22
22
|
|
23
23
|
|
kscale/store/__init__.py
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.0.1"
|
kscale/store/api.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
"""Defines a common interface for the K-Scale Store API."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from kscale.store.urdf import download_urdf
|
6
|
+
from kscale.utils.api_base import APIBase
|
7
|
+
|
8
|
+
|
9
|
+
class StoreAPI(APIBase):
|
10
|
+
def __init__(
|
11
|
+
self,
|
12
|
+
*,
|
13
|
+
api_key: str | None = None,
|
14
|
+
) -> None:
|
15
|
+
super().__init__()
|
16
|
+
|
17
|
+
self.api_key = api_key
|
18
|
+
|
19
|
+
async def urdf(self, artifact_id: str) -> Path:
|
20
|
+
return await download_urdf(artifact_id)
|
kscale/store/cli.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
"""Defines the top-level KOL CLI."""
|
2
2
|
|
3
3
|
import argparse
|
4
|
+
import asyncio
|
4
5
|
from typing import Sequence
|
5
6
|
|
6
7
|
from kscale.store import pybullet, urdf
|
7
8
|
|
8
9
|
|
9
|
-
def main(args: Sequence[str] | None = None) -> None:
|
10
|
+
async def main(args: Sequence[str] | None = None) -> None:
|
10
11
|
parser = argparse.ArgumentParser(description="K-Scale OnShape Library", add_help=False)
|
11
12
|
parser.add_argument(
|
12
13
|
"subcommand",
|
@@ -20,11 +21,15 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
20
21
|
|
21
22
|
match parsed_args.subcommand:
|
22
23
|
case "urdf":
|
23
|
-
urdf.main(remaining_args)
|
24
|
+
await urdf.main(remaining_args)
|
24
25
|
case "pybullet":
|
25
|
-
pybullet.main(remaining_args)
|
26
|
+
await pybullet.main(remaining_args)
|
27
|
+
|
28
|
+
|
29
|
+
def sync_main(args: Sequence[str] | None = None) -> None:
|
30
|
+
asyncio.run(main(args))
|
26
31
|
|
27
32
|
|
28
33
|
if __name__ == "__main__":
|
29
34
|
# python3 -m kscale.store.cli
|
30
|
-
|
35
|
+
sync_main()
|
kscale/store/client.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
"""Defines a typed client for the K-Scale Store API."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import Any, Dict, Type
|
6
|
+
from urllib.parse import urljoin
|
7
|
+
|
8
|
+
import httpx
|
9
|
+
from pydantic import BaseModel
|
10
|
+
|
11
|
+
from kscale.store.gen.api import (
|
12
|
+
NewListingRequest,
|
13
|
+
NewListingResponse,
|
14
|
+
SingleArtifactResponse,
|
15
|
+
UploadArtifactResponse,
|
16
|
+
)
|
17
|
+
from kscale.store.utils import API_ROOT, get_api_key
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class KScaleStoreClient:
|
23
|
+
def __init__(self, base_url: str = API_ROOT) -> None:
|
24
|
+
self.base_url = base_url
|
25
|
+
self.client = httpx.AsyncClient(
|
26
|
+
base_url=self.base_url,
|
27
|
+
headers={"Authorization": f"Bearer {get_api_key()}"},
|
28
|
+
)
|
29
|
+
|
30
|
+
async def _request(
|
31
|
+
self,
|
32
|
+
method: str,
|
33
|
+
endpoint: str,
|
34
|
+
*,
|
35
|
+
params: Dict[str, Any] | None = None,
|
36
|
+
data: BaseModel | None = None,
|
37
|
+
files: Dict[str, Any] | None = None,
|
38
|
+
) -> Dict[str, Any]:
|
39
|
+
url = urljoin(self.base_url, endpoint)
|
40
|
+
kwargs: Dict[str, Any] = {"params": params}
|
41
|
+
|
42
|
+
if data:
|
43
|
+
kwargs["json"] = data.dict(exclude_unset=True)
|
44
|
+
if files:
|
45
|
+
kwargs["files"] = files
|
46
|
+
|
47
|
+
response = await self.client.request(method, url, **kwargs)
|
48
|
+
if response.is_error:
|
49
|
+
logger.error(f"Error response from K-Scale Store: {response.text}")
|
50
|
+
response.raise_for_status()
|
51
|
+
return response.json()
|
52
|
+
|
53
|
+
async def get_artifact_info(self, artifact_id: str) -> SingleArtifactResponse:
|
54
|
+
data = await self._request("GET", f"/artifacts/info/{artifact_id}")
|
55
|
+
return SingleArtifactResponse(**data)
|
56
|
+
|
57
|
+
async def upload_artifact(self, listing_id: str, file_path: str) -> UploadArtifactResponse:
|
58
|
+
with open(file_path, "rb") as f:
|
59
|
+
files = {"files": (f.name, f, "application/gzip")}
|
60
|
+
data = await self._request("POST", f"/artifacts/upload/{listing_id}", files=files)
|
61
|
+
return UploadArtifactResponse(**data)
|
62
|
+
|
63
|
+
async def create_listing(self, request: NewListingRequest) -> NewListingResponse:
|
64
|
+
data = await self._request("POST", "/listings", data=request)
|
65
|
+
return NewListingResponse(**data)
|
66
|
+
|
67
|
+
async def close(self) -> None:
|
68
|
+
await self.client.aclose()
|
69
|
+
|
70
|
+
async def __aenter__(self) -> "KScaleStoreClient":
|
71
|
+
return self
|
72
|
+
|
73
|
+
async def __aexit__(
|
74
|
+
self,
|
75
|
+
exc_type: Type[BaseException] | None,
|
76
|
+
exc_val: BaseException | None,
|
77
|
+
exc_tb: TracebackType | None,
|
78
|
+
) -> None:
|
79
|
+
await self.close()
|
kscale/store/gen/api.py
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
# generated by datamodel-codegen:
|
4
4
|
# filename: openapi.json
|
5
|
-
# timestamp: 2024-
|
5
|
+
# timestamp: 2024-09-04T04:33:58+00:00
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
9
|
from enum import Enum
|
10
|
-
from typing import List, Optional, Union
|
10
|
+
from typing import Dict, List, Optional, Union
|
11
11
|
|
12
12
|
from pydantic import BaseModel, EmailStr, Field
|
13
13
|
|
@@ -21,8 +21,9 @@ class AuthResponse(BaseModel):
|
|
21
21
|
api_key: str = Field(..., title="Api Key")
|
22
22
|
|
23
23
|
|
24
|
-
class
|
25
|
-
|
24
|
+
class BodyPullOnshapeDocumentOnshapePullListingIdGet(BaseModel):
|
25
|
+
suffix_to_joint_effort: Optional[Dict[str, float]] = Field(None, title="Suffix To Joint Effort")
|
26
|
+
suffix_to_joint_velocity: Optional[Dict[str, float]] = Field(None, title="Suffix To Joint Velocity")
|
26
27
|
|
27
28
|
|
28
29
|
class BodyUploadArtifactsUploadListingIdPost(BaseModel):
|
@@ -51,7 +52,13 @@ class GetListingResponse(BaseModel):
|
|
51
52
|
description: Optional[str] = Field(..., title="Description")
|
52
53
|
child_ids: List[str] = Field(..., title="Child Ids")
|
53
54
|
tags: List[str] = Field(..., title="Tags")
|
55
|
+
onshape_url: Optional[str] = Field(..., title="Onshape Url")
|
54
56
|
can_edit: bool = Field(..., title="Can Edit")
|
57
|
+
created_at: int = Field(..., title="Created At")
|
58
|
+
views: int = Field(..., title="Views")
|
59
|
+
score: int = Field(..., title="Score")
|
60
|
+
user_vote: Optional[bool] = Field(..., title="User Vote")
|
61
|
+
creator_id: str = Field(..., title="Creator Id")
|
55
62
|
|
56
63
|
|
57
64
|
class GetTokenResponse(BaseModel):
|
@@ -82,42 +89,6 @@ class KeysResponseItem(BaseModel):
|
|
82
89
|
permissions: Optional[List[Permission]] = Field(..., title="Permissions")
|
83
90
|
|
84
91
|
|
85
|
-
class ArtifactType(Enum):
|
86
|
-
image = "image"
|
87
|
-
|
88
|
-
|
89
|
-
class ArtifactType1(Enum):
|
90
|
-
urdf = "urdf"
|
91
|
-
mjcf = "mjcf"
|
92
|
-
|
93
|
-
|
94
|
-
class ArtifactType2(Enum):
|
95
|
-
stl = "stl"
|
96
|
-
obj = "obj"
|
97
|
-
dae = "dae"
|
98
|
-
ply = "ply"
|
99
|
-
|
100
|
-
|
101
|
-
class ArtifactType3(Enum):
|
102
|
-
tgz = "tgz"
|
103
|
-
zip = "zip"
|
104
|
-
|
105
|
-
|
106
|
-
class ListArtifactsItem(BaseModel):
|
107
|
-
artifact_id: str = Field(..., title="Artifact Id")
|
108
|
-
listing_id: str = Field(..., title="Listing Id")
|
109
|
-
name: str = Field(..., title="Name")
|
110
|
-
artifact_type: Union[ArtifactType, ArtifactType1, ArtifactType2, ArtifactType3] = Field(..., title="Artifact Type")
|
111
|
-
description: Optional[str] = Field(..., title="Description")
|
112
|
-
timestamp: int = Field(..., title="Timestamp")
|
113
|
-
urls: ArtifactUrls
|
114
|
-
is_new: Optional[bool] = Field(None, title="Is New")
|
115
|
-
|
116
|
-
|
117
|
-
class ListArtifactsResponse(BaseModel):
|
118
|
-
artifacts: List[ListArtifactsItem] = Field(..., title="Artifacts")
|
119
|
-
|
120
|
-
|
121
92
|
class ListKeysResponse(BaseModel):
|
122
93
|
keys: List[KeysResponseItem] = Field(..., title="Keys")
|
123
94
|
|
@@ -130,9 +101,16 @@ class ListListingsResponse(BaseModel):
|
|
130
101
|
class Listing(BaseModel):
|
131
102
|
id: str = Field(..., title="Id")
|
132
103
|
user_id: str = Field(..., title="User Id")
|
104
|
+
created_at: int = Field(..., title="Created At")
|
105
|
+
updated_at: int = Field(..., title="Updated At")
|
133
106
|
name: str = Field(..., title="Name")
|
134
107
|
child_ids: List[str] = Field(..., title="Child Ids")
|
135
|
-
description: Optional[str] = Field(
|
108
|
+
description: Optional[str] = Field(None, title="Description")
|
109
|
+
onshape_url: Optional[str] = Field(None, title="Onshape Url")
|
110
|
+
views: Optional[int] = Field(0, title="Views")
|
111
|
+
upvotes: Optional[int] = Field(0, title="Upvotes")
|
112
|
+
downvotes: Optional[int] = Field(0, title="Downvotes")
|
113
|
+
score: Optional[int] = Field(0, title="Score")
|
136
114
|
|
137
115
|
|
138
116
|
class ListingInfoResponse(BaseModel):
|
@@ -141,6 +119,11 @@ class ListingInfoResponse(BaseModel):
|
|
141
119
|
description: Optional[str] = Field(..., title="Description")
|
142
120
|
child_ids: List[str] = Field(..., title="Child Ids")
|
143
121
|
image_url: Optional[str] = Field(..., title="Image Url")
|
122
|
+
onshape_url: Optional[str] = Field(..., title="Onshape Url")
|
123
|
+
created_at: int = Field(..., title="Created At")
|
124
|
+
views: int = Field(..., title="Views")
|
125
|
+
score: int = Field(..., title="Score")
|
126
|
+
user_vote: Optional[bool] = Field(..., title="User Vote")
|
144
127
|
|
145
128
|
|
146
129
|
class LoginRequest(BaseModel):
|
@@ -163,6 +146,10 @@ class MyUserInfoResponse(BaseModel):
|
|
163
146
|
github_id: Optional[str] = Field(..., title="Github Id")
|
164
147
|
google_id: Optional[str] = Field(..., title="Google Id")
|
165
148
|
permissions: Optional[List[Permission1]] = Field(..., title="Permissions")
|
149
|
+
first_name: Optional[str] = Field(..., title="First Name")
|
150
|
+
last_name: Optional[str] = Field(..., title="Last Name")
|
151
|
+
name: Optional[str] = Field(..., title="Name")
|
152
|
+
bio: Optional[str] = Field(..., title="Bio")
|
166
153
|
|
167
154
|
|
168
155
|
class NewKeyRequest(BaseModel):
|
@@ -200,6 +187,48 @@ class PublicUsersInfoResponse(BaseModel):
|
|
200
187
|
users: List[PublicUserInfoResponseItem] = Field(..., title="Users")
|
201
188
|
|
202
189
|
|
190
|
+
class SetRequest(BaseModel):
|
191
|
+
onshape_url: Optional[str] = Field(..., title="Onshape Url")
|
192
|
+
|
193
|
+
|
194
|
+
class ArtifactType(Enum):
|
195
|
+
image = "image"
|
196
|
+
|
197
|
+
|
198
|
+
class ArtifactType1(Enum):
|
199
|
+
urdf = "urdf"
|
200
|
+
mjcf = "mjcf"
|
201
|
+
|
202
|
+
|
203
|
+
class ArtifactType2(Enum):
|
204
|
+
stl = "stl"
|
205
|
+
obj = "obj"
|
206
|
+
dae = "dae"
|
207
|
+
ply = "ply"
|
208
|
+
|
209
|
+
|
210
|
+
class ArtifactType3(Enum):
|
211
|
+
tgz = "tgz"
|
212
|
+
zip = "zip"
|
213
|
+
|
214
|
+
|
215
|
+
class SingleArtifactResponse(BaseModel):
|
216
|
+
artifact_id: str = Field(..., title="Artifact Id")
|
217
|
+
listing_id: str = Field(..., title="Listing Id")
|
218
|
+
name: str = Field(..., title="Name")
|
219
|
+
artifact_type: Union[ArtifactType, ArtifactType1, ArtifactType2, ArtifactType3] = Field(..., title="Artifact Type")
|
220
|
+
description: Optional[str] = Field(..., title="Description")
|
221
|
+
timestamp: int = Field(..., title="Timestamp")
|
222
|
+
urls: ArtifactUrls
|
223
|
+
is_new: Optional[bool] = Field(None, title="Is New")
|
224
|
+
|
225
|
+
|
226
|
+
class SortOption(Enum):
|
227
|
+
newest = "newest"
|
228
|
+
most_viewed = "most_viewed"
|
229
|
+
most_upvoted = "most_upvoted"
|
230
|
+
|
231
|
+
|
203
232
|
class UpdateArtifactRequest(BaseModel):
|
204
233
|
name: Optional[str] = Field(None, title="Name")
|
205
234
|
description: Optional[str] = Field(None, title="Description")
|
@@ -212,18 +241,19 @@ class UpdateListingRequest(BaseModel):
|
|
212
241
|
tags: Optional[List[str]] = Field(None, title="Tags")
|
213
242
|
|
214
243
|
|
215
|
-
class
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
244
|
+
class UpdateUserRequest(BaseModel):
|
245
|
+
email: Optional[str] = Field(None, title="Email")
|
246
|
+
password: Optional[str] = Field(None, title="Password")
|
247
|
+
github_id: Optional[str] = Field(None, title="Github Id")
|
248
|
+
google_id: Optional[str] = Field(None, title="Google Id")
|
249
|
+
first_name: Optional[str] = Field(None, title="First Name")
|
250
|
+
last_name: Optional[str] = Field(None, title="Last Name")
|
251
|
+
name: Optional[str] = Field(None, title="Name")
|
252
|
+
bio: Optional[str] = Field(None, title="Bio")
|
222
253
|
|
223
254
|
|
224
|
-
class
|
225
|
-
|
226
|
-
listing_id: str = Field(..., title="Listing Id")
|
255
|
+
class UploadArtifactResponse(BaseModel):
|
256
|
+
artifacts: List[SingleArtifactResponse] = Field(..., title="Artifacts")
|
227
257
|
|
228
258
|
|
229
259
|
class UserInfoResponseItem(BaseModel):
|
@@ -235,7 +265,7 @@ class UserPublic(BaseModel):
|
|
235
265
|
id: str = Field(..., title="Id")
|
236
266
|
email: str = Field(..., title="Email")
|
237
267
|
permissions: Optional[List[Permission1]] = Field(None, title="Permissions")
|
238
|
-
created_at:
|
268
|
+
created_at: int = Field(..., title="Created At")
|
239
269
|
updated_at: Optional[int] = Field(None, title="Updated At")
|
240
270
|
first_name: Optional[str] = Field(None, title="First Name")
|
241
271
|
last_name: Optional[str] = Field(None, title="Last Name")
|
@@ -265,3 +295,7 @@ class GetBatchListingsResponse(BaseModel):
|
|
265
295
|
|
266
296
|
class HTTPValidationError(BaseModel):
|
267
297
|
detail: Optional[List[ValidationError]] = Field(None, title="Detail")
|
298
|
+
|
299
|
+
|
300
|
+
class ListArtifactsResponse(BaseModel):
|
301
|
+
artifacts: List[SingleArtifactResponse] = Field(..., title="Artifacts")
|
kscale/store/pybullet.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Simple script to interact with a URDF in PyBullet."""
|
2
2
|
|
3
3
|
import argparse
|
4
|
+
import asyncio
|
4
5
|
import itertools
|
5
6
|
import logging
|
6
7
|
import math
|
@@ -8,12 +9,14 @@ import time
|
|
8
9
|
from pathlib import Path
|
9
10
|
from typing import Sequence
|
10
11
|
|
12
|
+
from kscale.store.urdf import download_urdf
|
13
|
+
|
11
14
|
logger = logging.getLogger(__name__)
|
12
15
|
|
13
16
|
|
14
|
-
def main(args: Sequence[str] | None = None) -> None:
|
17
|
+
async def main(args: Sequence[str] | None = None) -> None:
|
15
18
|
parser = argparse.ArgumentParser(description="Show a URDF in PyBullet")
|
16
|
-
parser.add_argument("
|
19
|
+
parser.add_argument("listing_id", help="Listing ID for the URDF")
|
17
20
|
parser.add_argument("--dt", type=float, default=0.01, help="Time step")
|
18
21
|
parser.add_argument("-n", "--hide-gui", action="store_true", help="Hide the GUI")
|
19
22
|
parser.add_argument("--no-merge", action="store_true", help="Do not merge fixed links")
|
@@ -23,6 +26,9 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
23
26
|
parser.add_argument("--show-collision", action="store_true", help="Show collision meshes")
|
24
27
|
parsed_args = parser.parse_args(args)
|
25
28
|
|
29
|
+
# Gets the URDF path.
|
30
|
+
urdf_path = await download_urdf(parsed_args.listing_id)
|
31
|
+
|
26
32
|
try:
|
27
33
|
import pybullet as p # type: ignore[import-not-found]
|
28
34
|
except ImportError:
|
@@ -46,13 +52,6 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
46
52
|
# Loads the floor plane.
|
47
53
|
floor = p.loadURDF(str((Path(__file__).parent / "bullet" / "plane.urdf").resolve()))
|
48
54
|
|
49
|
-
urdf_path = Path("robot" if parsed_args.urdf is None else parsed_args.urdf)
|
50
|
-
if urdf_path.is_dir():
|
51
|
-
try:
|
52
|
-
urdf_path = next(urdf_path.glob("*.urdf"))
|
53
|
-
except StopIteration:
|
54
|
-
raise FileNotFoundError(f"No URDF files found in {urdf_path}")
|
55
|
-
|
56
55
|
# Load the robot URDF.
|
57
56
|
start_position = [0.0, 0.0, 1.0]
|
58
57
|
start_orientation = p.getQuaternionFromEuler([0.0, 0.0, 0.0])
|
@@ -175,4 +174,4 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
175
174
|
|
176
175
|
if __name__ == "__main__":
|
177
176
|
# python -m kscale.store.pybullet
|
178
|
-
main()
|
177
|
+
asyncio.run(main())
|
kscale/store/urdf.py
CHANGED
@@ -3,167 +3,169 @@
|
|
3
3
|
import argparse
|
4
4
|
import asyncio
|
5
5
|
import logging
|
6
|
-
import
|
6
|
+
import shutil
|
7
7
|
import sys
|
8
8
|
import tarfile
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Literal, Sequence
|
10
|
+
from typing import Literal, Sequence, get_args
|
11
11
|
|
12
12
|
import httpx
|
13
13
|
import requests
|
14
14
|
|
15
15
|
from kscale.conf import Settings
|
16
|
-
from kscale.store.
|
16
|
+
from kscale.store.client import KScaleStoreClient
|
17
|
+
from kscale.store.gen.api import SingleArtifactResponse
|
18
|
+
from kscale.store.utils import get_api_key
|
17
19
|
|
18
20
|
# Set up logging
|
19
21
|
logging.basicConfig(level=logging.INFO)
|
20
22
|
logger = logging.getLogger(__name__)
|
21
23
|
|
22
24
|
|
23
|
-
def get_api_key() -> str:
|
24
|
-
api_key = Settings.load().store.api_key
|
25
|
-
if not api_key:
|
26
|
-
raise ValueError(
|
27
|
-
"API key not found! Get one here and set it as the `KSCALE_API_KEY` environment variable or in your"
|
28
|
-
"config file: https://kscale.store/keys"
|
29
|
-
)
|
30
|
-
return api_key
|
31
|
-
|
32
|
-
|
33
25
|
def get_cache_dir() -> Path:
|
34
26
|
return Path(Settings.load().store.cache_dir).expanduser().resolve()
|
35
27
|
|
36
28
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
}
|
42
|
-
response = requests.get(url, headers=headers)
|
43
|
-
response.raise_for_status()
|
44
|
-
return UrdfResponse(**response.json())
|
29
|
+
def get_artifact_dir(artifact_id: str) -> Path:
|
30
|
+
cache_dir = get_cache_dir() / artifact_id
|
31
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
32
|
+
return cache_dir
|
45
33
|
|
46
34
|
|
47
|
-
async def
|
48
|
-
|
35
|
+
async def fetch_urdf_info(artifact_id: str, cache_dir: Path) -> SingleArtifactResponse:
|
36
|
+
response_path = cache_dir / "response.json"
|
37
|
+
if response_path.exists():
|
38
|
+
return SingleArtifactResponse.model_validate_json(response_path.read_text())
|
39
|
+
async with KScaleStoreClient() as client:
|
40
|
+
response = await client.get_artifact_info(artifact_id)
|
41
|
+
response_path.write_text(response.model_dump_json())
|
42
|
+
return response
|
43
|
+
|
44
|
+
|
45
|
+
async def download_artifact(artifact_url: str, cache_dir: Path) -> Path:
|
46
|
+
filename = cache_dir / Path(artifact_url).name
|
49
47
|
headers = {
|
50
48
|
"Authorization": f"Bearer {get_api_key()}",
|
51
49
|
}
|
52
50
|
|
53
|
-
if not
|
54
|
-
logger.info("Downloading artifact from %s"
|
51
|
+
if not filename.exists():
|
52
|
+
logger.info("Downloading artifact from %s", artifact_url)
|
55
53
|
|
56
54
|
async with httpx.AsyncClient() as client:
|
57
55
|
response = await client.get(artifact_url, headers=headers)
|
58
56
|
response.raise_for_status()
|
59
|
-
|
60
|
-
|
61
|
-
f.write(chunk)
|
62
|
-
logger.info("Artifact downloaded to %s" % filename)
|
57
|
+
filename.write_bytes(response.content)
|
58
|
+
logger.info("Artifact downloaded to %s", filename)
|
63
59
|
else:
|
64
|
-
logger.info("Artifact already cached at %s"
|
60
|
+
logger.info("Artifact already cached at %s", filename)
|
65
61
|
|
66
62
|
# Extract the .tgz file
|
67
|
-
extract_dir =
|
68
|
-
if not
|
69
|
-
logger.info(
|
63
|
+
extract_dir = cache_dir / filename.stem
|
64
|
+
if not extract_dir.exists():
|
65
|
+
logger.info("Extracting %s to %s", filename, extract_dir)
|
70
66
|
with tarfile.open(filename, "r:gz") as tar:
|
71
67
|
tar.extractall(path=extract_dir)
|
72
|
-
logger.info("Extraction complete")
|
73
68
|
else:
|
74
|
-
logger.info("Artifact already extracted at %s"
|
69
|
+
logger.info("Artifact already extracted at %s", extract_dir)
|
75
70
|
|
76
71
|
return extract_dir
|
77
72
|
|
78
73
|
|
79
|
-
def create_tarball(folder_path:
|
80
|
-
tarball_path =
|
74
|
+
def create_tarball(folder_path: Path, output_filename: str, cache_dir: Path) -> Path:
|
75
|
+
tarball_path = cache_dir / output_filename
|
81
76
|
with tarfile.open(tarball_path, "w:gz") as tar:
|
82
|
-
for
|
83
|
-
|
84
|
-
file_path =
|
85
|
-
|
86
|
-
|
87
|
-
logger.
|
88
|
-
logger.info("Created tarball %s"
|
77
|
+
for file_path in folder_path.rglob("*"):
|
78
|
+
if file_path.is_file() and file_path.suffix.lower() in (".urdf", ".mjcf", ".stl", ".obj", ".dae"):
|
79
|
+
tar.add(file_path, arcname=file_path.relative_to(folder_path))
|
80
|
+
logger.info("Added %s to tarball", file_path)
|
81
|
+
else:
|
82
|
+
logger.warning("Skipping %s", file_path)
|
83
|
+
logger.info("Created tarball %s", tarball_path)
|
89
84
|
return tarball_path
|
90
85
|
|
91
86
|
|
92
|
-
async def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
87
|
+
async def download_urdf(artifact_id: str) -> Path:
|
88
|
+
cache_dir = get_artifact_dir(artifact_id)
|
89
|
+
try:
|
90
|
+
urdf_info = await fetch_urdf_info(artifact_id, cache_dir)
|
91
|
+
artifact_url = urdf_info.urls.large
|
92
|
+
return await download_artifact(artifact_url, cache_dir)
|
93
|
+
|
94
|
+
except requests.RequestException:
|
95
|
+
logger.exception("Failed to fetch URDF info")
|
96
|
+
raise
|
97
|
+
|
98
|
+
|
99
|
+
async def show_urdf_info(artifact_id: str) -> None:
|
100
|
+
try:
|
101
|
+
urdf_info = await fetch_urdf_info(artifact_id, get_artifact_dir(artifact_id))
|
102
|
+
logger.info("URDF Artifact ID: %s", urdf_info.artifact_id)
|
103
|
+
logger.info("URDF URL: %s", urdf_info.urls.large)
|
104
|
+
except requests.RequestException:
|
105
|
+
logger.exception("Failed to fetch URDF info")
|
106
|
+
raise
|
107
|
+
|
108
|
+
|
109
|
+
async def remove_local_urdf(artifact_id: str) -> None:
|
110
|
+
try:
|
111
|
+
if artifact_id.lower() == "all":
|
112
|
+
cache_dir = get_cache_dir()
|
113
|
+
if cache_dir.exists():
|
114
|
+
logger.info("Removing all local caches at %s", cache_dir)
|
115
|
+
shutil.rmtree(cache_dir)
|
116
|
+
else:
|
117
|
+
logger.error("No local caches found")
|
118
|
+
else:
|
119
|
+
artifact_dir = get_artifact_dir(artifact_id)
|
120
|
+
if artifact_dir.exists():
|
121
|
+
logger.info("Removing local cache at %s", artifact_dir)
|
122
|
+
shutil.rmtree(artifact_dir)
|
123
|
+
else:
|
124
|
+
logger.error("No local cache found for artifact %s", artifact_id)
|
125
|
+
|
126
|
+
except Exception:
|
127
|
+
logger.error("Failed to remove local cache")
|
128
|
+
raise
|
129
|
+
|
130
|
+
|
131
|
+
async def upload_urdf(listing_id: str, args: Sequence[str]) -> None:
|
132
|
+
parser = argparse.ArgumentParser(description="K-Scale URDF Store", add_help=False)
|
133
|
+
parser.add_argument("root_dir", type=Path, help="The path to the root directory to upload")
|
134
|
+
parsed_args = parser.parse_args(args)
|
97
135
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
136
|
+
root_dir = parsed_args.root_dir
|
137
|
+
tarball_path = create_tarball(root_dir, "robot.tgz", get_artifact_dir(listing_id))
|
138
|
+
|
139
|
+
async with KScaleStoreClient() as client:
|
140
|
+
response = await client.upload_artifact(listing_id, str(tarball_path))
|
141
|
+
|
142
|
+
logger.info("Uploaded artifacts: %s", [artifact.artifact_id for artifact in response.artifacts])
|
102
143
|
|
103
|
-
response.raise_for_status()
|
104
144
|
|
105
|
-
|
145
|
+
Command = Literal["download", "info", "upload", "remove-local"]
|
106
146
|
|
107
147
|
|
108
|
-
def main(args: Sequence[str] | None = None) -> None:
|
148
|
+
async def main(args: Sequence[str] | None = None) -> None:
|
109
149
|
parser = argparse.ArgumentParser(description="K-Scale URDF Store", add_help=False)
|
110
|
-
parser.add_argument(
|
111
|
-
|
112
|
-
choices=["get", "info", "upload"],
|
113
|
-
help="The command to run",
|
114
|
-
)
|
115
|
-
parser.add_argument("listing_id", help="The listing ID to operate on")
|
150
|
+
parser.add_argument("command", choices=get_args(Command), help="The command to run")
|
151
|
+
parser.add_argument("id", help="The ID to use (artifact when downloading, listing when uploading)")
|
116
152
|
parsed_args, remaining_args = parser.parse_known_args(args)
|
117
153
|
|
118
|
-
command:
|
119
|
-
|
120
|
-
|
121
|
-
def get_listing_dir() -> Path:
|
122
|
-
(cache_dir := get_cache_dir() / listing_id).mkdir(parents=True, exist_ok=True)
|
123
|
-
return cache_dir
|
154
|
+
command: Command = parsed_args.command
|
155
|
+
id: str = parsed_args.id
|
124
156
|
|
125
157
|
match command:
|
126
|
-
case "
|
127
|
-
|
128
|
-
urdf_info = fetch_urdf_info(listing_id)
|
129
|
-
|
130
|
-
if urdf_info.urdf:
|
131
|
-
artifact_url = urdf_info.urdf.url
|
132
|
-
asyncio.run(download_artifact(artifact_url, get_listing_dir()))
|
133
|
-
else:
|
134
|
-
logger.info("No URDF found for listing %s" % listing_id)
|
135
|
-
except requests.RequestException as e:
|
136
|
-
logger.error("Failed to fetch URDF info: %s" % e)
|
137
|
-
sys.exit(1)
|
158
|
+
case "download":
|
159
|
+
await download_urdf(id)
|
138
160
|
|
139
161
|
case "info":
|
140
|
-
|
141
|
-
urdf_info = fetch_urdf_info(listing_id)
|
142
|
-
|
143
|
-
if urdf_info.urdf:
|
144
|
-
logger.info("URDF Artifact ID: %s" % urdf_info.urdf.artifact_id)
|
145
|
-
logger.info("URDF URL: %s" % urdf_info.urdf.url)
|
146
|
-
else:
|
147
|
-
logger.info("No URDF found for listing %s" % listing_id)
|
148
|
-
except requests.RequestException as e:
|
149
|
-
logger.error("Failed to fetch URDF info: %s" % e)
|
150
|
-
sys.exit(1)
|
151
|
-
|
152
|
-
case "upload":
|
153
|
-
parser = argparse.ArgumentParser(description="Upload a URDF artifact to the K-Scale store")
|
154
|
-
parser.add_argument("folder_path", help="The path to the folder containing the URDF files")
|
155
|
-
parsed_args = parser.parse_args(remaining_args)
|
156
|
-
folder_path = Path(parsed_args.folder_path).expanduser().resolve()
|
162
|
+
await show_urdf_info(id)
|
157
163
|
|
158
|
-
|
159
|
-
|
164
|
+
case "remove-local":
|
165
|
+
await remove_local_urdf(id)
|
160
166
|
|
161
|
-
|
162
|
-
|
163
|
-
asyncio.run(upload_artifact(tarball_path, listing_id))
|
164
|
-
except requests.RequestException as e:
|
165
|
-
logger.error("Failed to upload artifact: %s" % e)
|
166
|
-
sys.exit(1)
|
167
|
+
case "upload":
|
168
|
+
await upload_urdf(id, remaining_args)
|
167
169
|
|
168
170
|
case _:
|
169
171
|
logger.error("Invalid command")
|
@@ -171,4 +173,4 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
171
173
|
|
172
174
|
|
173
175
|
if __name__ == "__main__":
|
174
|
-
main()
|
176
|
+
asyncio.run(main())
|
kscale/store/utils.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
"""Utility functions for interacting with the K-Scale Store API."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
from kscale.conf import Settings
|
6
|
+
|
7
|
+
API_ROOT = "https://api.kscale.store"
|
8
|
+
|
9
|
+
|
10
|
+
def get_api_key() -> str:
|
11
|
+
api_key = Settings.load().store.api_key
|
12
|
+
if api_key is None:
|
13
|
+
api_key = os.getenv("KSCALE_API_KEY")
|
14
|
+
if not api_key:
|
15
|
+
raise ValueError(
|
16
|
+
"API key not found! Get one here and set it as the `KSCALE_API_KEY` environment variable or in your "
|
17
|
+
"config file: https://kscale.store/keys"
|
18
|
+
)
|
19
|
+
return api_key
|
kscale/utils/__init__.py
ADDED
File without changes
|
kscale/utils/api_base.py
ADDED
@@ -1,15 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: kscale
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.5
|
4
4
|
Summary: The kscale project
|
5
5
|
Home-page: https://github.com/kscalelabs/kscale
|
6
6
|
Author: Benjamin Bolte
|
7
7
|
Requires-Python: >=3.11
|
8
8
|
Description-Content-Type: text/markdown
|
9
9
|
License-File: LICENSE
|
10
|
-
Requires-Dist: omegaconf
|
11
|
-
Requires-Dist:
|
12
|
-
Requires-Dist:
|
10
|
+
Requires-Dist: omegaconf ==2.3.0
|
11
|
+
Requires-Dist: email-validator ==2.2.0
|
12
|
+
Requires-Dist: httpx ==0.27.0
|
13
|
+
Requires-Dist: requests ==2.32.2
|
13
14
|
Provides-Extra: dev
|
14
15
|
Requires-Dist: black ; extra == 'dev'
|
15
16
|
Requires-Dist: darglint ; extra == 'dev'
|
@@ -50,3 +51,32 @@ This is a command line tool for interacting with various services provided by K-
|
|
50
51
|
```bash
|
51
52
|
pip install kscale
|
52
53
|
```
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
### CLI
|
58
|
+
|
59
|
+
Download a URDF from the K-Scale Store:
|
60
|
+
|
61
|
+
```bash
|
62
|
+
kscale urdf download <artifact_id>
|
63
|
+
```
|
64
|
+
|
65
|
+
Upload a URDF to the K-Scale Store:
|
66
|
+
|
67
|
+
```bash
|
68
|
+
kscale urdf upload <artifact_id> <root_dir>
|
69
|
+
```
|
70
|
+
|
71
|
+
### Python API
|
72
|
+
|
73
|
+
Reference a URDF by ID from the K-Scale Store:
|
74
|
+
|
75
|
+
```python
|
76
|
+
from kscale import KScale
|
77
|
+
|
78
|
+
async def main():
|
79
|
+
kscale = KScale()
|
80
|
+
urdf_dir_path = await kscale.store.urdf("123456")
|
81
|
+
print(urdf_dir_path)
|
82
|
+
```
|
@@ -0,0 +1,21 @@
|
|
1
|
+
kscale/__init__.py,sha256=e1NdSlxsBKi5qyOIjXC3vAPQXleZwh2hyVlLHcKQsq8,117
|
2
|
+
kscale/api.py,sha256=xBtKj8rgZ400r1Xx9LRY0AzSgIIttoXdejmhHhdVGS0,333
|
3
|
+
kscale/conf.py,sha256=9fShFaYTbnrm_eiGjmy8ZtC4Q4m6PQkWPyoF3eNyov8,1424
|
4
|
+
kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
kscale/store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
kscale/store/api.py,sha256=L-QZMDeOsNo8ovIRjwfBlidYKD510hdoKrmrue7C5Wk,454
|
7
|
+
kscale/store/cli.py,sha256=8ygg_1tZzOOHJotEIgSN9pfumcriPmA31sI_FCFQiTo,859
|
8
|
+
kscale/store/client.py,sha256=HEyTzgCOXdLCQKdFKwNglvTBtXVipul7cxbdjz53Pb4,2544
|
9
|
+
kscale/store/pybullet.py,sha256=viTQCE2jT72miPKZpFKj4f4zGLLYtjhRFxGqzxd2Z8M,7504
|
10
|
+
kscale/store/urdf.py,sha256=zMUfJNNpZkoLf-SnhMO28f8Qj39hmZrA0vqLL3sZSqE,6069
|
11
|
+
kscale/store/utils.py,sha256=vQSFd9fByDUUSD0dAA65T_WI5R55vY-HOQjIJg_u2jw,536
|
12
|
+
kscale/store/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
kscale/store/gen/api.py,sha256=82D41J6pg9KWdgD0lx7NggLcNS32SpnN8DqE3Md6ON0,9559
|
14
|
+
kscale/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
kscale/utils/api_base.py,sha256=Kk_WtRDdJHmOg6NtHmVxVrcfARSUkhfr29ypLch_pO0,112
|
16
|
+
kscale-0.0.5.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
|
17
|
+
kscale-0.0.5.dist-info/METADATA,sha256=jAkpxXCrUa8b30BY32OaW6eIoR0VFHQzKgzO26EDFGM,2514
|
18
|
+
kscale-0.0.5.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
|
19
|
+
kscale-0.0.5.dist-info/entry_points.txt,sha256=PaVs1ivqB0BBdGUsiFkxGUYjGLz05VqagxwRVwi4yV4,54
|
20
|
+
kscale-0.0.5.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
|
21
|
+
kscale-0.0.5.dist-info/RECORD,,
|
kscale-0.0.2.dist-info/RECORD
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
kscale/__init__.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
|
2
|
-
kscale/conf.py,sha256=dnO8qii7JeMPMdjfuxnFXURRM4krwzL404RnwDkyL2M,1441
|
3
|
-
kscale/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
kscale/store/__init__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
5
|
-
kscale/store/cli.py,sha256=sf6F8ZLMenNpxi9dPB6kpaUOmRdfNBcKSnAcoN7EMo0,733
|
6
|
-
kscale/store/pybullet.py,sha256=2Pog9wPSyyhmhTJY6x5KhuUQgenpTPIvp_9wxOy9ZaU,7622
|
7
|
-
kscale/store/urdf.py,sha256=-dp0DEMoDNIqieG9egPv_XaV7Cg0GthMI_QIYvG2Xjk,6185
|
8
|
-
kscale/store/gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
-
kscale/store/gen/api.py,sha256=7r6KvlZyB-kWKSCixc-YOtaWPy2mn0v11d2tuLlRMSc,7700
|
10
|
-
kscale-0.0.2.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
|
11
|
-
kscale-0.0.2.dist-info/METADATA,sha256=fNvMO1rM9-UfnHjp3BZ4t4FmcQ-CRPAmBDAdXRkBu-8,2028
|
12
|
-
kscale-0.0.2.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
13
|
-
kscale-0.0.2.dist-info/entry_points.txt,sha256=Vfj9O643497OONpgwy5UJLJ5a5Q1CfHZSuYKDB9D_GI,49
|
14
|
-
kscale-0.0.2.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
|
15
|
-
kscale-0.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|