kscale 0.0.13__cp311-cp311-macosx_11_0_arm64.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 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
Binary file
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