kscale 0.0.3__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 CHANGED
@@ -1 +1,5 @@
1
- __version__ = "0.0.3"
1
+ """Defines the common interface for the K-Scale Python API."""
2
+
3
+ __version__ = "0.0.5"
4
+
5
+ from kscale.api import KScale
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=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
 
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/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-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")
kscale/store/pybullet.py CHANGED
@@ -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:
kscale/store/urdf.py CHANGED
@@ -3,7 +3,6 @@
3
3
  import argparse
4
4
  import asyncio
5
5
  import logging
6
- import os
7
6
  import shutil
8
7
  import sys
9
8
  import tarfile
@@ -14,63 +13,54 @@ import httpx
14
13
  import requests
15
14
 
16
15
  from kscale.conf import Settings
17
- from kscale.store.gen.api import UrdfResponse
16
+ from kscale.store.client import KScaleStoreClient
17
+ from kscale.store.gen.api import SingleArtifactResponse
18
+ from kscale.store.utils import get_api_key
18
19
 
19
20
  # Set up logging
20
21
  logging.basicConfig(level=logging.INFO)
21
22
  logger = logging.getLogger(__name__)
22
23
 
23
24
 
24
- def get_api_key() -> str:
25
- api_key = Settings.load().store.api_key
26
- if not api_key:
27
- raise ValueError(
28
- "API key not found! Get one here and set it as the `KSCALE_API_KEY` environment variable or in your "
29
- "config file: https://kscale.store/keys"
30
- )
31
- return api_key
32
-
33
-
34
25
  def get_cache_dir() -> Path:
35
26
  return Path(Settings.load().store.cache_dir).expanduser().resolve()
36
27
 
37
28
 
38
- def get_listing_dir(listing_id: str) -> Path:
39
- (cache_dir := get_cache_dir() / listing_id).mkdir(parents=True, exist_ok=True)
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)
40
32
  return cache_dir
41
33
 
42
34
 
43
- def fetch_urdf_info(listing_id: str) -> UrdfResponse:
44
- url = f"https://api.kscale.store/urdf/info/{listing_id}"
45
- headers = {
46
- "Authorization": f"Bearer {get_api_key()}",
47
- }
48
- response = requests.get(url, headers=headers)
49
- response.raise_for_status()
50
- return UrdfResponse(**response.json())
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
51
43
 
52
44
 
53
45
  async def download_artifact(artifact_url: str, cache_dir: Path) -> Path:
54
- filename = os.path.join(cache_dir, artifact_url.split("/")[-1])
46
+ filename = cache_dir / Path(artifact_url).name
55
47
  headers = {
56
48
  "Authorization": f"Bearer {get_api_key()}",
57
49
  }
58
50
 
59
- if not os.path.exists(filename):
51
+ if not filename.exists():
60
52
  logger.info("Downloading artifact from %s", artifact_url)
61
53
 
62
54
  async with httpx.AsyncClient() as client:
63
55
  response = await client.get(artifact_url, headers=headers)
64
56
  response.raise_for_status()
65
- with open(filename, "wb") as f:
66
- for chunk in response.iter_bytes(chunk_size=8192):
67
- f.write(chunk)
57
+ filename.write_bytes(response.content)
68
58
  logger.info("Artifact downloaded to %s", filename)
69
59
  else:
70
60
  logger.info("Artifact already cached at %s", filename)
71
61
 
72
62
  # Extract the .tgz file
73
- extract_dir = cache_dir / os.path.splitext(os.path.basename(filename))[0]
63
+ extract_dir = cache_dir / filename.stem
74
64
  if not extract_dir.exists():
75
65
  logger.info("Extracting %s to %s", filename, extract_dir)
76
66
  with tarfile.open(filename, "r:gz") as tar:
@@ -81,85 +71,44 @@ async def download_artifact(artifact_url: str, cache_dir: Path) -> Path:
81
71
  return extract_dir
82
72
 
83
73
 
84
- def create_tarball(folder_path: str | Path, output_filename: str, cache_dir: Path) -> str:
85
- tarball_path = os.path.join(cache_dir, output_filename)
74
+ def create_tarball(folder_path: Path, output_filename: str, cache_dir: Path) -> Path:
75
+ tarball_path = cache_dir / output_filename
86
76
  with tarfile.open(tarball_path, "w:gz") as tar:
87
- for root, _, files in os.walk(folder_path):
88
- for file in files:
89
- file_path = os.path.join(root, file)
90
- arcname = os.path.relpath(file_path, start=folder_path)
91
- tar.add(file_path, arcname=arcname)
92
- logger.info("Added %s as %s", file_path, arcname)
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)
93
83
  logger.info("Created tarball %s", tarball_path)
94
84
  return tarball_path
95
85
 
96
86
 
97
- async def upload_artifact(tarball_path: str, listing_id: str) -> None:
98
- url = f"https://api.kscale.store/urdf/upload/{listing_id}"
99
- headers = {
100
- "Authorization": f"Bearer {get_api_key()}",
101
- }
102
-
103
- async with httpx.AsyncClient() as client:
104
- with open(tarball_path, "rb") as f:
105
- files = {"file": (f.name, f, "application/gzip")}
106
- response = await client.post(url, headers=headers, files=files)
107
-
108
- response.raise_for_status()
109
-
110
- logger.info("Uploaded artifact to %s", url)
111
-
112
-
113
- async def download_urdf(listing_id: str) -> Path:
87
+ async def download_urdf(artifact_id: str) -> Path:
88
+ cache_dir = get_artifact_dir(artifact_id)
114
89
  try:
115
- urdf_info = fetch_urdf_info(listing_id)
116
-
117
- if urdf_info.urdf is None:
118
- breakpoint()
119
- raise ValueError(f"No URDF found for listing {listing_id}")
120
-
121
- artifact_url = urdf_info.urdf.url
122
- return await download_artifact(artifact_url, get_listing_dir(listing_id))
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)
123
93
 
124
94
  except requests.RequestException:
125
95
  logger.exception("Failed to fetch URDF info")
126
96
  raise
127
97
 
128
98
 
129
- async def show_urdf_info(listing_id: str) -> None:
99
+ async def show_urdf_info(artifact_id: str) -> None:
130
100
  try:
131
- urdf_info = fetch_urdf_info(listing_id)
132
-
133
- if urdf_info.urdf:
134
- logger.info("URDF Artifact ID: %s", urdf_info.urdf.artifact_id)
135
- logger.info("URDF URL: %s", urdf_info.urdf.url)
136
- else:
137
- logger.info("No URDF found for listing %s", listing_id)
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)
138
104
  except requests.RequestException:
139
105
  logger.exception("Failed to fetch URDF info")
140
106
  raise
141
107
 
142
108
 
143
- async def upload_urdf(listing_id: str, args: Sequence[str] | None = None) -> None:
144
- parser = argparse.ArgumentParser(description="Upload a URDF artifact to the K-Scale store")
145
- parser.add_argument("folder_path", help="The path to the folder containing the URDF files")
146
- parsed_args = parser.parse_args(args)
147
- folder_path = Path(parsed_args.folder_path).expanduser().resolve()
148
-
149
- output_filename = f"{listing_id}.tgz"
150
- tarball_path = create_tarball(folder_path, output_filename, get_listing_dir(listing_id))
151
-
152
- try:
153
- fetch_urdf_info(listing_id)
154
- await upload_artifact(tarball_path, listing_id)
155
- except requests.RequestException:
156
- logger.exception("Failed to upload artifact")
157
- raise
158
-
159
-
160
- async def remove_local_urdf(listing_id: str) -> None:
109
+ async def remove_local_urdf(artifact_id: str) -> None:
161
110
  try:
162
- if listing_id.lower() == "all":
111
+ if artifact_id.lower() == "all":
163
112
  cache_dir = get_cache_dir()
164
113
  if cache_dir.exists():
165
114
  logger.info("Removing all local caches at %s", cache_dir)
@@ -167,42 +116,56 @@ async def remove_local_urdf(listing_id: str) -> None:
167
116
  else:
168
117
  logger.error("No local caches found")
169
118
  else:
170
- listing_dir = get_listing_dir(listing_id)
171
- if listing_dir.exists():
172
- logger.info("Removing local cache at %s", listing_dir)
173
- shutil.rmtree(listing_dir)
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)
174
123
  else:
175
- logger.error("No local cache found for listing %s", listing_id)
124
+ logger.error("No local cache found for artifact %s", artifact_id)
176
125
 
177
126
  except Exception:
178
127
  logger.error("Failed to remove local cache")
179
128
  raise
180
129
 
181
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)
135
+
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])
143
+
144
+
182
145
  Command = Literal["download", "info", "upload", "remove-local"]
183
146
 
184
147
 
185
148
  async def main(args: Sequence[str] | None = None) -> None:
186
149
  parser = argparse.ArgumentParser(description="K-Scale URDF Store", add_help=False)
187
150
  parser.add_argument("command", choices=get_args(Command), help="The command to run")
188
- parser.add_argument("listing_id", help="The listing ID to operate on")
151
+ parser.add_argument("id", help="The ID to use (artifact when downloading, listing when uploading)")
189
152
  parsed_args, remaining_args = parser.parse_known_args(args)
190
153
 
191
154
  command: Command = parsed_args.command
192
- listing_id: str = parsed_args.listing_id
155
+ id: str = parsed_args.id
193
156
 
194
157
  match command:
195
158
  case "download":
196
- await download_urdf(listing_id)
159
+ await download_urdf(id)
197
160
 
198
161
  case "info":
199
- await show_urdf_info(listing_id)
200
-
201
- case "upload":
202
- await upload_urdf(listing_id, remaining_args)
162
+ await show_urdf_info(id)
203
163
 
204
164
  case "remove-local":
205
- await remove_local_urdf(listing_id)
165
+ await remove_local_urdf(id)
166
+
167
+ case "upload":
168
+ await upload_urdf(id, remaining_args)
206
169
 
207
170
  case _:
208
171
  logger.error("Invalid command")
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
File without changes
@@ -0,0 +1,6 @@
1
+ """Defines the base class for the K-Scale API."""
2
+
3
+
4
+ class APIBase:
5
+ def __init__(self) -> None:
6
+ pass
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kscale
3
- Version: 0.0.3
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: 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
+ ```
@@ -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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.1)
2
+ Generator: setuptools (74.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,15 +0,0 @@
1
- kscale/__init__.py,sha256=4GZKi13lDTD25YBkGakhZyEQZWTER_OWQMNPoH_UM2c,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=8ygg_1tZzOOHJotEIgSN9pfumcriPmA31sI_FCFQiTo,859
6
- kscale/store/pybullet.py,sha256=y51Oc6ZjOGe38O-L-Lm6HDbgNWc-5mpeXCeXRtCUl18,7522
7
- kscale/store/urdf.py,sha256=0u6c2UxYlKwgUl0n3VmMSmfTqdixNP5b-hjox5mIgqM,7064
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.3.dist-info/LICENSE,sha256=HCN2bImAzUOXldAZZI7JZ9PYq6OwMlDAP_PpX1HnuN0,1071
11
- kscale-0.0.3.dist-info/METADATA,sha256=KN2mHTYV9mkJFxYqe4u1PFkCFQkc5o-E3460k66gN_M,2028
12
- kscale-0.0.3.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
13
- kscale-0.0.3.dist-info/entry_points.txt,sha256=PaVs1ivqB0BBdGUsiFkxGUYjGLz05VqagxwRVwi4yV4,54
14
- kscale-0.0.3.dist-info/top_level.txt,sha256=C2ynjYwopg6YjgttnI2dJjasyq3EKNmYp-IfQg9Xms4,7
15
- kscale-0.0.3.dist-info/RECORD,,