kscale 0.0.3__tar.gz → 0.0.6__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. {kscale-0.0.3/kscale.egg-info → kscale-0.0.6}/PKG-INFO +34 -4
  2. {kscale-0.0.3 → kscale-0.0.6}/README.md +29 -0
  3. kscale-0.0.6/kscale/__init__.py +5 -0
  4. kscale-0.0.6/kscale/api.py +14 -0
  5. {kscale-0.0.3 → kscale-0.0.6}/kscale/conf.py +1 -1
  6. kscale-0.0.6/kscale/requirements.txt +9 -0
  7. kscale-0.0.6/kscale/store/api.py +24 -0
  8. kscale-0.0.6/kscale/store/client.py +81 -0
  9. kscale-0.0.6/kscale/store/gen/__init__.py +0 -0
  10. {kscale-0.0.3 → kscale-0.0.6}/kscale/store/gen/api.py +86 -52
  11. {kscale-0.0.3 → kscale-0.0.6}/kscale/store/pybullet.py +0 -2
  12. kscale-0.0.6/kscale/store/urdf.py +182 -0
  13. kscale-0.0.6/kscale/store/utils.py +33 -0
  14. kscale-0.0.6/kscale/utils/__init__.py +0 -0
  15. kscale-0.0.6/kscale/utils/api_base.py +6 -0
  16. {kscale-0.0.3 → kscale-0.0.6/kscale.egg-info}/PKG-INFO +34 -4
  17. {kscale-0.0.3 → kscale-0.0.6}/kscale.egg-info/SOURCES.txt +6 -1
  18. kscale-0.0.6/kscale.egg-info/requires.txt +12 -0
  19. kscale-0.0.3/kscale/__init__.py +0 -1
  20. kscale-0.0.3/kscale/formats/mjcf.py +0 -509
  21. kscale-0.0.3/kscale/requirements.txt +0 -8
  22. kscale-0.0.3/kscale/store/__init__.py +0 -1
  23. kscale-0.0.3/kscale/store/urdf.py +0 -213
  24. kscale-0.0.3/kscale.egg-info/requires.txt +0 -11
  25. {kscale-0.0.3 → kscale-0.0.6}/LICENSE +0 -0
  26. {kscale-0.0.3 → kscale-0.0.6}/MANIFEST.in +0 -0
  27. {kscale-0.0.3 → kscale-0.0.6}/kscale/py.typed +0 -0
  28. {kscale-0.0.3 → kscale-0.0.6}/kscale/requirements-dev.txt +0 -0
  29. {kscale-0.0.3/kscale/store/gen → kscale-0.0.6/kscale/store}/__init__.py +0 -0
  30. {kscale-0.0.3 → kscale-0.0.6}/kscale/store/bullet/MANIFEST.in +0 -0
  31. {kscale-0.0.3 → kscale-0.0.6}/kscale/store/cli.py +0 -0
  32. {kscale-0.0.3 → kscale-0.0.6}/kscale.egg-info/dependency_links.txt +0 -0
  33. {kscale-0.0.3 → kscale-0.0.6}/kscale.egg-info/entry_points.txt +0 -0
  34. {kscale-0.0.3 → kscale-0.0.6}/kscale.egg-info/top_level.txt +0 -0
  35. {kscale-0.0.3 → kscale-0.0.6}/pyproject.toml +0 -0
  36. {kscale-0.0.3 → kscale-0.0.6}/setup.cfg +0 -0
  37. {kscale-0.0.3 → kscale-0.0.6}/setup.py +0 -0
  38. {kscale-0.0.3 → kscale-0.0.6}/tests/test_dummy.py +0 -0
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kscale
3
- Version: 0.0.3
3
+ Version: 0.0.6
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: httpx
12
- Requires-Dist: requests
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
+ ```
@@ -30,3 +30,32 @@ This is a command line tool for interacting with various services provided by K-
30
30
  ```bash
31
31
  pip install kscale
32
32
  ```
33
+
34
+ ## Usage
35
+
36
+ ### CLI
37
+
38
+ Download a URDF from the K-Scale Store:
39
+
40
+ ```bash
41
+ kscale urdf download <artifact_id>
42
+ ```
43
+
44
+ Upload a URDF to the K-Scale Store:
45
+
46
+ ```bash
47
+ kscale urdf upload <artifact_id> <root_dir>
48
+ ```
49
+
50
+ ### Python API
51
+
52
+ Reference a URDF by ID from the K-Scale Store:
53
+
54
+ ```python
55
+ from kscale import KScale
56
+
57
+ async def main():
58
+ kscale = KScale()
59
+ urdf_dir_path = await kscale.store.urdf("123456")
60
+ print(urdf_dir_path)
61
+ ```
@@ -0,0 +1,5 @@
1
+ """Defines the common interface for the K-Scale Python API."""
2
+
3
+ __version__ = "0.0.6"
4
+
5
+ from kscale.api import KScale
@@ -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
@@ -17,7 +17,7 @@ def get_path() -> Path:
17
17
 
18
18
  @dataclass
19
19
  class StoreSettings:
20
- api_key: str = field(default=II("oc.env:KSCALE_API_KEY,"))
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
 
@@ -0,0 +1,9 @@
1
+ # requirements.txt
2
+
3
+ # Configuration
4
+ omegaconf==2.3.0
5
+ email_validator==2.2.0
6
+
7
+ # HTTP requests
8
+ httpx==0.27.0
9
+ requests==2.32.2
@@ -0,0 +1,24 @@
1
+ """Defines a common interface for the K-Scale Store API."""
2
+
3
+ from pathlib import Path
4
+
5
+ from kscale.store.gen.api import UploadArtifactResponse
6
+ from kscale.store.urdf import download_urdf, upload_urdf
7
+ from kscale.utils.api_base import APIBase
8
+
9
+
10
+ class StoreAPI(APIBase):
11
+ def __init__(
12
+ self,
13
+ *,
14
+ api_key: str | None = None,
15
+ ) -> None:
16
+ super().__init__()
17
+
18
+ self.api_key = api_key
19
+
20
+ async def urdf(self, artifact_id: str) -> Path:
21
+ return await download_urdf(artifact_id)
22
+
23
+ async def upload_urdf(self, listing_id: str, root_dir: Path) -> UploadArtifactResponse:
24
+ return await upload_urdf(listing_id, root_dir)
@@ -0,0 +1,81 @@
1
+ """Defines a typed client for the K-Scale Store API."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from types import TracebackType
6
+ from typing import Any, Dict, Type
7
+ from urllib.parse import urljoin
8
+
9
+ import httpx
10
+ from pydantic import BaseModel
11
+
12
+ from kscale.store.gen.api import (
13
+ NewListingRequest,
14
+ NewListingResponse,
15
+ SingleArtifactResponse,
16
+ UploadArtifactResponse,
17
+ )
18
+ from kscale.store.utils import get_api_key, get_api_root
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class KScaleStoreClient:
24
+ def __init__(self, base_url: str = get_api_root()) -> None:
25
+ self.base_url = base_url
26
+ self.client = httpx.AsyncClient(
27
+ base_url=self.base_url,
28
+ headers={"Authorization": f"Bearer {get_api_key()}"},
29
+ )
30
+
31
+ async def _request(
32
+ self,
33
+ method: str,
34
+ endpoint: str,
35
+ *,
36
+ params: Dict[str, Any] | None = None,
37
+ data: BaseModel | None = None,
38
+ files: Dict[str, Any] | None = None,
39
+ ) -> Dict[str, Any]:
40
+ url = urljoin(self.base_url, endpoint)
41
+ kwargs: Dict[str, Any] = {"params": params}
42
+
43
+ if data:
44
+ kwargs["json"] = data.dict(exclude_unset=True)
45
+ if files:
46
+ kwargs["files"] = files
47
+
48
+ response = await self.client.request(method, url, **kwargs)
49
+ if response.is_error:
50
+ logger.error(f"Error response from K-Scale Store: {response.text}")
51
+ response.raise_for_status()
52
+ return response.json()
53
+
54
+ async def get_artifact_info(self, artifact_id: str) -> SingleArtifactResponse:
55
+ data = await self._request("GET", f"/artifacts/info/{artifact_id}")
56
+ return SingleArtifactResponse(**data)
57
+
58
+ async def upload_artifact(self, listing_id: str, file_path: str) -> UploadArtifactResponse:
59
+ file_name = Path(file_path).name
60
+ with open(file_path, "rb") as f:
61
+ files = {"files": (file_name, f, "application/gzip")}
62
+ data = await self._request("POST", f"/artifacts/upload/{listing_id}", files=files)
63
+ return UploadArtifactResponse(**data)
64
+
65
+ async def create_listing(self, request: NewListingRequest) -> NewListingResponse:
66
+ data = await self._request("POST", "/listings", data=request)
67
+ return NewListingResponse(**data)
68
+
69
+ async def close(self) -> None:
70
+ await self.client.aclose()
71
+
72
+ async def __aenter__(self) -> "KScaleStoreClient":
73
+ return self
74
+
75
+ async def __aexit__(
76
+ self,
77
+ exc_type: Type[BaseException] | None,
78
+ exc_val: BaseException | None,
79
+ exc_tb: TracebackType | None,
80
+ ) -> None:
81
+ await self.close()
File without changes
@@ -2,12 +2,12 @@
2
2
 
3
3
  # generated by datamodel-codegen:
4
4
  # filename: openapi.json
5
- # timestamp: 2024-08-19T06:07:36+00:00
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 BodySetUrdfUrdfUploadListingIdPost(BaseModel):
25
- file: bytes = Field(..., title="File")
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(..., title="Description")
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 UploadArtifactResponse(BaseModel):
216
- artifacts: List[ListArtifactsItem] = Field(..., title="Artifacts")
217
-
218
-
219
- class UrdfInfo(BaseModel):
220
- artifact_id: str = Field(..., title="Artifact Id")
221
- url: str = Field(..., title="Url")
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 UrdfResponse(BaseModel):
225
- urdf: Optional[UrdfInfo]
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: Optional[int] = Field(None, title="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")
@@ -29,8 +29,6 @@ async def main(args: Sequence[str] | None = None) -> None:
29
29
  # Gets the URDF path.
30
30
  urdf_path = await download_urdf(parsed_args.listing_id)
31
31
 
32
- breakpoint()
33
-
34
32
  try:
35
33
  import pybullet as p # type: ignore[import-not-found]
36
34
  except ImportError:
@@ -0,0 +1,182 @@
1
+ """Utility functions for managing artifacts in the K-Scale store."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+ import shutil
7
+ import sys
8
+ import tarfile
9
+ from pathlib import Path
10
+ from typing import Literal, Sequence, get_args
11
+
12
+ import httpx
13
+ import requests
14
+
15
+ from kscale.conf import Settings
16
+ from kscale.store.client import KScaleStoreClient
17
+ from kscale.store.gen.api import SingleArtifactResponse, UploadArtifactResponse
18
+ from kscale.store.utils import get_api_key
19
+
20
+ # Set up logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def get_cache_dir() -> Path:
26
+ return Path(Settings.load().store.cache_dir).expanduser().resolve()
27
+
28
+
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
33
+
34
+
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
47
+ headers = {
48
+ "Authorization": f"Bearer {get_api_key()}",
49
+ }
50
+
51
+ if not filename.exists():
52
+ logger.info("Downloading artifact from %s", artifact_url)
53
+
54
+ async with httpx.AsyncClient() as client:
55
+ response = await client.get(artifact_url, headers=headers)
56
+ response.raise_for_status()
57
+ filename.write_bytes(response.content)
58
+ logger.info("Artifact downloaded to %s", filename)
59
+ else:
60
+ logger.info("Artifact already cached at %s", filename)
61
+
62
+ # Extract the .tgz file
63
+ extract_dir = cache_dir / filename.stem
64
+ if not extract_dir.exists():
65
+ logger.info("Extracting %s to %s", filename, extract_dir)
66
+ with tarfile.open(filename, "r:gz") as tar:
67
+ tar.extractall(path=extract_dir)
68
+ else:
69
+ logger.info("Artifact already extracted at %s", extract_dir)
70
+
71
+ return extract_dir
72
+
73
+
74
+ def create_tarball(folder_path: Path, output_filename: str, cache_dir: Path) -> Path:
75
+ tarball_path = cache_dir / output_filename
76
+ with tarfile.open(tarball_path, "w:gz") as tar:
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)
84
+ return tarball_path
85
+
86
+
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, root_dir: Path) -> UploadArtifactResponse:
132
+ tarball_path = create_tarball(root_dir, "robot.tgz", get_artifact_dir(listing_id))
133
+
134
+ async with KScaleStoreClient() as client:
135
+ response = await client.upload_artifact(listing_id, str(tarball_path))
136
+
137
+ logger.info("Uploaded artifacts: %s", [artifact.artifact_id for artifact in response.artifacts])
138
+ return response
139
+
140
+
141
+ async def upload_urdf_cli(listing_id: str, args: Sequence[str]) -> UploadArtifactResponse:
142
+ parser = argparse.ArgumentParser(description="K-Scale URDF Store", add_help=False)
143
+ parser.add_argument("root_dir", type=Path, help="The path to the root directory to upload")
144
+ parsed_args = parser.parse_args(args)
145
+
146
+ root_dir = parsed_args.root_dir
147
+ response = await upload_urdf(listing_id, root_dir)
148
+ return response
149
+
150
+
151
+ Command = Literal["download", "info", "upload", "remove-local"]
152
+
153
+
154
+ async def main(args: Sequence[str] | None = None) -> None:
155
+ parser = argparse.ArgumentParser(description="K-Scale URDF Store", add_help=False)
156
+ parser.add_argument("command", choices=get_args(Command), help="The command to run")
157
+ parser.add_argument("id", help="The ID to use (artifact when downloading, listing when uploading)")
158
+ parsed_args, remaining_args = parser.parse_known_args(args)
159
+
160
+ command: Command = parsed_args.command
161
+ id: str = parsed_args.id
162
+
163
+ match command:
164
+ case "download":
165
+ await download_urdf(id)
166
+
167
+ case "info":
168
+ await show_urdf_info(id)
169
+
170
+ case "remove-local":
171
+ await remove_local_urdf(id)
172
+
173
+ case "upload":
174
+ await upload_urdf_cli(id, remaining_args)
175
+
176
+ case _:
177
+ logger.error("Invalid command")
178
+ sys.exit(1)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ asyncio.run(main())
@@ -0,0 +1,33 @@
1
+ """Utility functions for interacting with the K-Scale Store API."""
2
+
3
+ import os
4
+
5
+ from kscale.conf import Settings
6
+
7
+
8
+ def get_api_root() -> str:
9
+ """Returns the base URL for the K-Scale Store API.
10
+
11
+ This can be overridden when targetting a different server.
12
+
13
+ Returns:
14
+ The base URL for the K-Scale Store API.
15
+ """
16
+ return os.getenv("KSCALE_API_ROOT", "https://api.kscale.store")
17
+
18
+
19
+ def get_api_key() -> str:
20
+ """Returns the API key for the K-Scale Store API.
21
+
22
+ Returns:
23
+ The API key for the K-Scale Store API.
24
+ """
25
+ api_key = Settings.load().store.api_key
26
+ if api_key is None:
27
+ api_key = os.getenv("KSCALE_API_KEY")
28
+ if not api_key:
29
+ raise ValueError(
30
+ "API key not found! Get one here and set it as the `KSCALE_API_KEY` environment variable or in your "
31
+ "config file: https://kscale.store/keys"
32
+ )
33
+ return api_key
File without changes