kscale 0.0.13__cp312-cp312-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 ADDED
@@ -0,0 +1,9 @@
1
+ """Defines the common interface for the K-Scale Python API."""
2
+
3
+ __version__ = "0.0.13"
4
+
5
+ from pathlib import Path
6
+
7
+ from kscale.api import K
8
+
9
+ ROOT_DIR = Path(__file__).parent
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,8 @@
1
+ """Defines some helper functions for working with artifacts."""
2
+
3
+ from pathlib import Path
4
+
5
+ ARTIFACTS_DIR = Path(__file__).parent.resolve()
6
+
7
+ PLANE_OBJ_PATH = ARTIFACTS_DIR / "plane.obj"
8
+ PLANE_URDF_PATH = ARTIFACTS_DIR / "plane.urdf"
@@ -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
@@ -0,0 +1,11 @@
1
+ # requirements-dev.txt
2
+
3
+ # For linting code.
4
+ black
5
+ darglint
6
+ mypy
7
+ pytest
8
+ ruff
9
+
10
+ # For generating API types.
11
+ datamodel-code-generator
@@ -0,0 +1,17 @@
1
+ # requirements.txt
2
+
3
+ # Configuration
4
+ omegaconf
5
+ email_validator
6
+
7
+ # HTTP requests
8
+ httpx
9
+ requests
10
+ pydantic
11
+
12
+ # CLI
13
+ click
14
+ aiofiles
15
+
16
+ # K-Scale
17
+ krec
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
@@ -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