kscale 0.0.2__py3-none-any.whl → 0.0.5__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 +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
|