diff --git a/packages/README.md b/packages/README.md index 9264737..6f76644 100644 --- a/packages/README.md +++ b/packages/README.md @@ -1,9 +1,13 @@ # Extension Packages This folder contains Python package splits for provider-specific integrations. +Each package re-exports the relevant subset of the core `rfx-sdk` so users can +install only what they need. -- `rfx-sdk-sim`: simulation backends and simulation-specific controllers -- `rfx-sdk-go2`: Unitree Go2 integrations -- `rfx-sdk-lerobot`: LeRobot recorder/export helpers +| Package | Install | Provides | +|---------|---------|----------| +| `rfx-sdk-go2` | `pip install rfx-sdk-go2` | `Go2Robot`, `Go2Backend`, `Go2Env`, `make_go2` factory | +| `rfx-sdk-sim` | `pip install rfx-sdk-sim` | `SimRobot`, `MockRobot`, `BaseEnv`, `VecEnv`, `make_vec_env` | +| `rfx-sdk-lerobot` | `pip install rfx-sdk-lerobot` | `collect`, `Dataset`, `Recorder`, `LeRobotPackageWriter`, Hub helpers | The base Python import remains `rfx` (published as `rfx-sdk`). diff --git a/packages/rfx-go2/pyproject.toml b/packages/rfx-go2/pyproject.toml index 0ca1a6f..363b07c 100644 --- a/packages/rfx-go2/pyproject.toml +++ b/packages/rfx-go2/pyproject.toml @@ -7,7 +7,7 @@ name = "rfx-sdk-go2" version = "0.2.0" description = "Unitree Go2 integrations for rfx" requires-python = ">=3.13" -dependencies = ["rfx-sdk>=0.2.0"] +dependencies = ["rfx-sdk[teleop]>=0.2.0", "torch>=2.2"] [tool.setuptools] package-dir = {"" = "src"} diff --git a/packages/rfx-go2/src/rfx_go2/__init__.py b/packages/rfx-go2/src/rfx_go2/__init__.py index 72aad72..ca2c385 100644 --- a/packages/rfx-go2/src/rfx_go2/__init__.py +++ b/packages/rfx-go2/src/rfx_go2/__init__.py @@ -1,3 +1,34 @@ -"""rfx-sdk-go2 package scaffold.""" +"""rfx-sdk-go2 — Unitree Go2 integrations for rfx. -__all__ = [] +Convenience package that re-exports Go2-specific components from the core +``rfx-sdk`` so users can ``pip install rfx-sdk-go2`` for a focused install. + +Example:: + + from rfx_go2 import Go2Robot, Go2Backend + robot = Go2Robot(ip_address="192.168.123.161") + obs = robot.observe() +""" + +from __future__ import annotations + +from rfx.envs import Go2Env + +try: + from rfx.real.go2 import Go2Backend, Go2Robot +except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "rfx-sdk-go2 requires torch. Install with: pip install rfx-sdk[teleop]" + ) from exc + +try: + from rfx.robot.lerobot import go2 as make_go2 +except ModuleNotFoundError: + make_go2 = None # type: ignore[assignment] + +__all__ = [ + "Go2Backend", + "Go2Env", + "Go2Robot", + "make_go2", +] diff --git a/packages/rfx-lerobot/pyproject.toml b/packages/rfx-lerobot/pyproject.toml index 26b7ca9..869c42f 100644 --- a/packages/rfx-lerobot/pyproject.toml +++ b/packages/rfx-lerobot/pyproject.toml @@ -7,7 +7,7 @@ name = "rfx-sdk-lerobot" version = "0.2.0" description = "LeRobot integration package for rfx" requires-python = ">=3.13" -dependencies = ["rfx-sdk>=0.2.0", "lerobot"] +dependencies = ["rfx-sdk[collection]>=0.2.0"] [tool.setuptools] package-dir = {"" = "src"} diff --git a/packages/rfx-lerobot/src/rfx_lerobot/__init__.py b/packages/rfx-lerobot/src/rfx_lerobot/__init__.py index d3ce745..9d003da 100644 --- a/packages/rfx-lerobot/src/rfx_lerobot/__init__.py +++ b/packages/rfx-lerobot/src/rfx_lerobot/__init__.py @@ -1,3 +1,29 @@ -"""rfx-sdk-lerobot package scaffold.""" +"""rfx-sdk-lerobot — LeRobot integration package for rfx. -__all__ = [] +Convenience package that re-exports LeRobot data-collection and dataset +components from the core ``rfx-sdk`` so users can +``pip install rfx-sdk-lerobot`` for a focused install. + +Example:: + + from rfx_lerobot import collect, Dataset + dataset = collect("so101", "my-org/demos", episodes=10) + dataset.push() +""" + +from __future__ import annotations + +from rfx.collection import Dataset, Recorder, collect, from_hub, open_dataset, pull, push +from rfx.teleop.lerobot_writer import LeRobotExportConfig, LeRobotPackageWriter + +__all__ = [ + "Dataset", + "LeRobotExportConfig", + "LeRobotPackageWriter", + "Recorder", + "collect", + "from_hub", + "open_dataset", + "pull", + "push", +] diff --git a/packages/rfx-sim/pyproject.toml b/packages/rfx-sim/pyproject.toml index af7eb0a..3c44528 100644 --- a/packages/rfx-sim/pyproject.toml +++ b/packages/rfx-sim/pyproject.toml @@ -7,7 +7,7 @@ name = "rfx-sdk-sim" version = "0.2.0" description = "Simulation adapters and controllers for rfx" requires-python = ">=3.13" -dependencies = ["rfx-sdk>=0.2.0"] +dependencies = ["rfx-sdk[sim-mock]>=0.2.0", "torch>=2.2"] [tool.setuptools] package-dir = {"" = "src"} diff --git a/packages/rfx-sim/src/rfx_sim/__init__.py b/packages/rfx-sim/src/rfx_sim/__init__.py index 84b51b1..efcddc1 100644 --- a/packages/rfx-sim/src/rfx_sim/__init__.py +++ b/packages/rfx-sim/src/rfx_sim/__init__.py @@ -1,3 +1,31 @@ -"""rfx-sdk-sim package scaffold.""" +"""rfx-sdk-sim — Simulation adapters and controllers for rfx. -__all__ = [] +Convenience package that re-exports simulation components from the core +``rfx-sdk`` so users can ``pip install rfx-sdk-sim`` for a focused install. + +Example:: + + from rfx_sim import SimRobot, MockRobot + robot = SimRobot.from_config("so101.yaml", backend="mock", num_envs=16) + obs = robot.observe() +""" + +from __future__ import annotations + +from rfx.envs import BaseEnv, Box, VecEnv, make_vec_env + +try: + from rfx.sim import MockRobot, SimRobot +except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "rfx-sdk-sim requires torch. Install with: pip install rfx-sdk[sim-mock]" + ) from exc + +__all__ = [ + "BaseEnv", + "Box", + "MockRobot", + "SimRobot", + "VecEnv", + "make_vec_env", +] diff --git a/rfx/tests/test_decorators.py b/rfx/tests/test_decorators.py index ccc377e..28e5e30 100644 --- a/rfx/tests/test_decorators.py +++ b/rfx/tests/test_decorators.py @@ -1,5 +1,8 @@ """Tests for rfx.decorators module.""" +from __future__ import annotations + +import importlib.util from typing import Any import pytest @@ -7,13 +10,14 @@ from rfx.decorators import MotorCommands, policy from rfx.robot.config import JointConfig, RobotConfig +TORCH_AVAILABLE = importlib.util.find_spec("torch") is not None + # --------------------------------------------------------------------------- # @policy decorator # --------------------------------------------------------------------------- class TestPolicyDecorator: - def test_policy_bare(self) -> None: """@rfx.policy without parentheses.""" @@ -83,7 +87,6 @@ def _make_config(joints: list[tuple[str, int]], action_dim: int = 6) -> RobotCon class TestMotorCommands: - def test_empty(self) -> None: cmd = MotorCommands() assert cmd.positions == {} @@ -111,8 +114,8 @@ def test_from_positions(self) -> None: assert cmd.config is config +@pytest.mark.skipif(not TORCH_AVAILABLE, reason="torch is required") class TestMotorCommandsToTensor: - def test_to_tensor_basic(self) -> None: config = _make_config(SO101_JOINTS) cmd = MotorCommands({"gripper": 0.8, "elbow": -0.3}, config=config) @@ -146,7 +149,6 @@ def test_to_tensor_no_config_raises(self) -> None: class TestMotorCommandsToList: - def test_to_list_basic(self) -> None: config = _make_config(SO101_JOINTS) cmd = MotorCommands({"gripper": 0.8, "elbow": -0.3}, config=config) diff --git a/rfx/tests/test_so101_auto_pair.py b/rfx/tests/test_so101_auto_pair.py index db95505..bd22157 100644 --- a/rfx/tests/test_so101_auto_pair.py +++ b/rfx/tests/test_so101_auto_pair.py @@ -1,8 +1,15 @@ from __future__ import annotations +import importlib.util from pathlib import Path -import rfx.real.so101 as so101_mod +import pytest + +TORCH_AVAILABLE = importlib.util.find_spec("torch") is not None +pytestmark = pytest.mark.skipif(not TORCH_AVAILABLE, reason="torch is required") + +if TORCH_AVAILABLE: + import rfx.real.so101 as so101_mod def test_auto_pair_persists_identity_across_port_renumber(monkeypatch, tmp_path: Path) -> None: diff --git a/rfx/tests/test_zenoh_defaults.py b/rfx/tests/test_zenoh_defaults.py index c239433..7a6eef5 100644 --- a/rfx/tests/test_zenoh_defaults.py +++ b/rfx/tests/test_zenoh_defaults.py @@ -2,6 +2,8 @@ from __future__ import annotations +import importlib.util + import pytest @@ -162,6 +164,7 @@ def zenoh(connect, listen, shared_memory, key_prefix): assert captured["shared_memory"] is False +@pytest.mark.skipif(not importlib.util.find_spec("torch"), reason="torch is required") def test_go2_dds_backend_deprecation_warning() -> None: """Using dds_backend= should emit FutureWarning.""" import warnings