kscale 0.0.13__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Sign up to get free protection for your applications and to get access to all the features.
- kscale/__init__.py +9 -0
- kscale/api.py +14 -0
- kscale/artifacts/__init__.py +8 -0
- kscale/artifacts/plane.obj +18 -0
- kscale/artifacts/plane.urdf +28 -0
- kscale/cli.py +25 -0
- kscale/conf.py +45 -0
- kscale/py.typed +0 -0
- kscale/requirements-dev.txt +11 -0
- kscale/requirements.txt +17 -0
- kscale/rust.cpython-311-x86_64-linux-gnu.so +0 -0
- kscale/utils/__init__.py +0 -0
- kscale/utils/api_base.py +6 -0
- kscale/utils/checksum.py +41 -0
- kscale/utils/cli.py +28 -0
- kscale/web/__init__.py +0 -0
- kscale/web/api.py +98 -0
- kscale/web/gen/__init__.py +0 -0
- kscale/web/gen/api.py +612 -0
- kscale/web/kernels.py +207 -0
- kscale/web/krec.py +175 -0
- kscale/web/pybullet.py +188 -0
- kscale/web/urdf.py +185 -0
- kscale/web/utils.py +48 -0
- kscale/web/www_client.py +134 -0
- kscale-0.0.13.dist-info/LICENSE +21 -0
- kscale-0.0.13.dist-info/METADATA +55 -0
- kscale-0.0.13.dist-info/RECORD +31 -0
- kscale-0.0.13.dist-info/WHEEL +6 -0
- kscale-0.0.13.dist-info/entry_points.txt +2 -0
- kscale-0.0.13.dist-info/top_level.txt +1 -0
kscale/__init__.py
ADDED
kscale/api.py
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
"""Defines common functionality for the K-Scale API."""
|
2
|
+
|
3
|
+
from kscale.utils.api_base import APIBase
|
4
|
+
from kscale.web.api import WebAPI
|
5
|
+
|
6
|
+
|
7
|
+
class K(
|
8
|
+
WebAPI,
|
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Blender v2.66 (sub 1) OBJ File: ''
|
2
|
+
# www.blender.org
|
3
|
+
mtllib plane.mtl
|
4
|
+
o Plane
|
5
|
+
v 15.000000 -15.000000 0.000000
|
6
|
+
v 15.000000 15.000000 0.000000
|
7
|
+
v -15.000000 15.000000 0.000000
|
8
|
+
v -15.000000 -15.000000 0.000000
|
9
|
+
|
10
|
+
vt 15.000000 0.000000
|
11
|
+
vt 15.000000 15.000000
|
12
|
+
vt 0.000000 15.000000
|
13
|
+
vt 0.000000 0.000000
|
14
|
+
|
15
|
+
usemtl Material
|
16
|
+
s off
|
17
|
+
f 1/1 2/2 3/3
|
18
|
+
f 1/1 3/3 4/4
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<?xml version="0.0" ?>
|
2
|
+
<robot name="plane">
|
3
|
+
<link name="planeLink">
|
4
|
+
<contact>
|
5
|
+
<lateral_friction value="1"/>
|
6
|
+
</contact>
|
7
|
+
<inertial>
|
8
|
+
<origin rpy="0 0 0" xyz="0 0 0"/>
|
9
|
+
<mass value=".0"/>
|
10
|
+
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0"/>
|
11
|
+
</inertial>
|
12
|
+
<visual>
|
13
|
+
<origin rpy="0 0 0" xyz="0 0 0"/>
|
14
|
+
<geometry>
|
15
|
+
<mesh filename="plane.obj" scale="1 1 1"/>
|
16
|
+
</geometry>
|
17
|
+
<material name="white">
|
18
|
+
<color rgba="1 1 1 1"/>
|
19
|
+
</material>
|
20
|
+
</visual>
|
21
|
+
<collision>
|
22
|
+
<origin rpy="0 0 0" xyz="0 0 -5"/>
|
23
|
+
<geometry>
|
24
|
+
<box size="30 30 10"/>
|
25
|
+
</geometry>
|
26
|
+
</collision>
|
27
|
+
</link>
|
28
|
+
</robot>
|
kscale/cli.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
"""Defines the top-level KOL CLI."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from kscale.utils.cli import recursive_help
|
6
|
+
from kscale.web.kernels import cli as kernel_images_cli
|
7
|
+
from kscale.web.krec import cli as krec_cli
|
8
|
+
from kscale.web.pybullet import cli as pybullet_cli
|
9
|
+
from kscale.web.urdf import cli as urdf_cli
|
10
|
+
|
11
|
+
|
12
|
+
@click.group()
|
13
|
+
def cli() -> None:
|
14
|
+
"""Command line interface for interacting with the K-Scale web API."""
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
cli.add_command(urdf_cli, "urdf")
|
19
|
+
cli.add_command(pybullet_cli, "pybullet")
|
20
|
+
cli.add_command(kernel_images_cli, "kernel")
|
21
|
+
cli.add_command(krec_cli, "krec")
|
22
|
+
|
23
|
+
if __name__ == "__main__":
|
24
|
+
# python -m kscale.cli
|
25
|
+
print(recursive_help(cli))
|
kscale/conf.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
"""Defines the bot environment settings."""
|
2
|
+
|
3
|
+
import functools
|
4
|
+
import os
|
5
|
+
import warnings
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
from omegaconf import II, OmegaConf
|
10
|
+
|
11
|
+
|
12
|
+
def get_path() -> Path:
|
13
|
+
if "KSCALE_CONFIG_DIR" in os.environ:
|
14
|
+
return Path(os.environ["KSCALE_CONFIG_DIR"]).expanduser().resolve()
|
15
|
+
return Path("~/.kscale/").expanduser().resolve()
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class WWWSettings:
|
20
|
+
api_key: str | None = field(default=None)
|
21
|
+
cache_dir: str = field(default=II("oc.env:KSCALE_CACHE_DIR,'~/.kscale/cache/'"))
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class Settings:
|
26
|
+
www: WWWSettings = field(default_factory=WWWSettings)
|
27
|
+
|
28
|
+
def save(self) -> None:
|
29
|
+
(dir_path := get_path()).mkdir(parents=True, exist_ok=True)
|
30
|
+
with open(dir_path / "settings.yaml", "w") as f:
|
31
|
+
OmegaConf.save(config=self, f=f)
|
32
|
+
|
33
|
+
@functools.lru_cache
|
34
|
+
@staticmethod
|
35
|
+
def load() -> "Settings":
|
36
|
+
config = OmegaConf.structured(Settings)
|
37
|
+
if not (dir_path := get_path()).exists():
|
38
|
+
warnings.warn(f"Settings directory does not exist: {dir_path}. Creating it now.")
|
39
|
+
dir_path.mkdir(parents=True)
|
40
|
+
OmegaConf.save(config, dir_path / "settings.yaml")
|
41
|
+
else:
|
42
|
+
with open(dir_path / "settings.yaml", "r") as f:
|
43
|
+
raw_settings = OmegaConf.load(f)
|
44
|
+
config = OmegaConf.merge(config, raw_settings)
|
45
|
+
return config
|
kscale/py.typed
ADDED
File without changes
|
kscale/requirements.txt
ADDED
Binary file
|
kscale/utils/__init__.py
ADDED
File without changes
|
kscale/utils/api_base.py
ADDED
kscale/utils/checksum.py
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
"""Utility functions for file checksums."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Tuple
|
6
|
+
|
7
|
+
CHUNK_SIZE = 8192
|
8
|
+
|
9
|
+
|
10
|
+
async def calculate_sha256(file_path: str | Path) -> Tuple[str, int]:
|
11
|
+
"""Calculate SHA256 checksum and size of a file.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
file_path: Path to the file
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
Tuple of (checksum hex string, file size in bytes)
|
18
|
+
"""
|
19
|
+
sha256_hash = hashlib.sha256()
|
20
|
+
file_size = 0
|
21
|
+
|
22
|
+
with open(file_path, "rb") as f:
|
23
|
+
for chunk in iter(lambda: f.read(CHUNK_SIZE), b""):
|
24
|
+
sha256_hash.update(chunk)
|
25
|
+
file_size += len(chunk)
|
26
|
+
|
27
|
+
return sha256_hash.hexdigest(), file_size
|
28
|
+
|
29
|
+
|
30
|
+
class FileChecksum:
|
31
|
+
"""Helper class for handling file checksums."""
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
async def calculate(file_path: str | Path) -> Tuple[str, int]:
|
35
|
+
"""Calculate SHA256 checksum and size of a file."""
|
36
|
+
return await calculate_sha256(file_path)
|
37
|
+
|
38
|
+
@staticmethod
|
39
|
+
def update_hash(hash_obj: "hashlib._Hash", chunk: bytes) -> None:
|
40
|
+
"""Update a hash object with new data."""
|
41
|
+
hash_obj.update(chunk)
|
kscale/utils/cli.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
"""Defines utilities for working with asyncio."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import textwrap
|
5
|
+
from functools import wraps
|
6
|
+
from typing import Any, Callable, Coroutine, ParamSpec, TypeVar
|
7
|
+
|
8
|
+
import click
|
9
|
+
|
10
|
+
T = TypeVar("T")
|
11
|
+
P = ParamSpec("P")
|
12
|
+
|
13
|
+
|
14
|
+
def coro(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
|
15
|
+
@wraps(f)
|
16
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
17
|
+
return asyncio.run(f(*args, **kwargs))
|
18
|
+
|
19
|
+
return wrapper
|
20
|
+
|
21
|
+
|
22
|
+
def recursive_help(cmd: click.Command, parent: click.Context | None = None, indent: int = 0) -> str:
|
23
|
+
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
|
24
|
+
help_text = cmd.get_help(ctx)
|
25
|
+
commands = getattr(cmd, "commands", {})
|
26
|
+
for sub in commands.values():
|
27
|
+
help_text += recursive_help(sub, ctx, indent + 2)
|
28
|
+
return textwrap.indent(help_text, " " * indent)
|
kscale/web/__init__.py
ADDED
File without changes
|
kscale/web/api.py
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
"""Defines a common interface for the K-Scale WWW API."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import overload
|
6
|
+
|
7
|
+
from kscale.utils.api_base import APIBase
|
8
|
+
from kscale.web.gen.api import UploadArtifactResponse
|
9
|
+
from kscale.web.urdf import download_urdf, upload_urdf
|
10
|
+
|
11
|
+
|
12
|
+
class WebAPI(APIBase):
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
*,
|
16
|
+
api_key: str | None = None,
|
17
|
+
) -> None:
|
18
|
+
super().__init__()
|
19
|
+
|
20
|
+
self.api_key = api_key
|
21
|
+
|
22
|
+
async def artifact_root(self, artifact_id: str) -> Path:
|
23
|
+
return await download_urdf(artifact_id)
|
24
|
+
|
25
|
+
@overload
|
26
|
+
async def urdf_path(self, artifact_id: str) -> Path: ...
|
27
|
+
|
28
|
+
@overload
|
29
|
+
async def urdf_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
30
|
+
|
31
|
+
async def urdf_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
32
|
+
root_dir = await self.artifact_root(artifact_id)
|
33
|
+
urdf_path = next(root_dir.glob("*.urdf"), None)
|
34
|
+
if urdf_path is None and throw_if_missing:
|
35
|
+
raise FileNotFoundError(f"No URDF found for artifact {artifact_id}")
|
36
|
+
return urdf_path
|
37
|
+
|
38
|
+
@overload
|
39
|
+
def urdf_path_sync(self, artifact_id: str) -> Path: ...
|
40
|
+
|
41
|
+
@overload
|
42
|
+
def urdf_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
43
|
+
|
44
|
+
def urdf_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
45
|
+
return asyncio.run(self.urdf_path(artifact_id, throw_if_missing=throw_if_missing))
|
46
|
+
|
47
|
+
@overload
|
48
|
+
async def mjcf_path(self, artifact_id: str) -> Path: ...
|
49
|
+
|
50
|
+
@overload
|
51
|
+
async def mjcf_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
52
|
+
|
53
|
+
async def mjcf_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
54
|
+
root_dir = await self.artifact_root(artifact_id)
|
55
|
+
mjcf_path = next(root_dir.glob("*.mjcf"), None)
|
56
|
+
if mjcf_path is None and throw_if_missing:
|
57
|
+
raise FileNotFoundError(f"No MJCF found for artifact {artifact_id}")
|
58
|
+
return mjcf_path
|
59
|
+
|
60
|
+
@overload
|
61
|
+
def mjcf_path_sync(self, artifact_id: str) -> Path: ...
|
62
|
+
|
63
|
+
@overload
|
64
|
+
def mjcf_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
65
|
+
|
66
|
+
def mjcf_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
67
|
+
return asyncio.run(self.mjcf_path(artifact_id, throw_if_missing=throw_if_missing))
|
68
|
+
|
69
|
+
@overload
|
70
|
+
async def xml_path(self, artifact_id: str) -> Path: ...
|
71
|
+
|
72
|
+
@overload
|
73
|
+
async def xml_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
74
|
+
|
75
|
+
async def xml_path(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
76
|
+
root_dir = await self.artifact_root(artifact_id)
|
77
|
+
xml_path = next(root_dir.glob("*.xml"), None)
|
78
|
+
if xml_path is None and throw_if_missing:
|
79
|
+
raise FileNotFoundError(f"No XML found for artifact {artifact_id}")
|
80
|
+
return xml_path
|
81
|
+
|
82
|
+
async def upload_urdf(self, listing_id: str, root_dir: Path) -> UploadArtifactResponse:
|
83
|
+
return await upload_urdf(listing_id, root_dir)
|
84
|
+
|
85
|
+
def artifact_root_sync(self, artifact_id: str) -> Path:
|
86
|
+
return asyncio.run(self.artifact_root(artifact_id))
|
87
|
+
|
88
|
+
@overload
|
89
|
+
def xml_path_sync(self, artifact_id: str) -> Path: ...
|
90
|
+
|
91
|
+
@overload
|
92
|
+
def xml_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None: ...
|
93
|
+
|
94
|
+
def xml_path_sync(self, artifact_id: str, *, throw_if_missing: bool = True) -> Path | None:
|
95
|
+
return asyncio.run(self.xml_path(artifact_id, throw_if_missing=throw_if_missing))
|
96
|
+
|
97
|
+
def upload_urdf_sync(self, listing_id: str, root_dir: Path) -> UploadArtifactResponse:
|
98
|
+
return asyncio.run(self.upload_urdf(listing_id, root_dir))
|
File without changes
|