robotrt-sdk 0.1.0__tar.gz

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.
Files changed (33) hide show
  1. robotrt_sdk-0.1.0/PKG-INFO +140 -0
  2. robotrt_sdk-0.1.0/README.md +127 -0
  3. robotrt_sdk-0.1.0/pyproject.toml +25 -0
  4. robotrt_sdk-0.1.0/robotrt_sdk/__init__.py +84 -0
  5. robotrt_sdk-0.1.0/robotrt_sdk/_ffi.py +26 -0
  6. robotrt_sdk-0.1.0/robotrt_sdk/_ffi_loader.py +82 -0
  7. robotrt_sdk-0.1.0/robotrt_sdk/_ffi_signatures.py +372 -0
  8. robotrt_sdk-0.1.0/robotrt_sdk/_ffi_types.py +79 -0
  9. robotrt_sdk-0.1.0/robotrt_sdk/_version.py +1 -0
  10. robotrt_sdk-0.1.0/robotrt_sdk/action.py +264 -0
  11. robotrt_sdk-0.1.0/robotrt_sdk/buffer.py +116 -0
  12. robotrt_sdk-0.1.0/robotrt_sdk/core.py +51 -0
  13. robotrt_sdk-0.1.0/robotrt_sdk/errors.py +20 -0
  14. robotrt_sdk-0.1.0/robotrt_sdk/executor.py +870 -0
  15. robotrt_sdk-0.1.0/robotrt_sdk/lifecycle.py +76 -0
  16. robotrt_sdk-0.1.0/robotrt_sdk/logger.py +31 -0
  17. robotrt_sdk-0.1.0/robotrt_sdk/mission.py +113 -0
  18. robotrt_sdk-0.1.0/robotrt_sdk/models.py +39 -0
  19. robotrt_sdk-0.1.0/robotrt_sdk/node.py +498 -0
  20. robotrt_sdk-0.1.0/robotrt_sdk/pubsub.py +244 -0
  21. robotrt_sdk-0.1.0/robotrt_sdk/service.py +153 -0
  22. robotrt_sdk-0.1.0/robotrt_sdk/status.py +584 -0
  23. robotrt_sdk-0.1.0/robotrt_sdk/utils.py +59 -0
  24. robotrt_sdk-0.1.0/robotrt_sdk.egg-info/PKG-INFO +140 -0
  25. robotrt_sdk-0.1.0/robotrt_sdk.egg-info/SOURCES.txt +31 -0
  26. robotrt_sdk-0.1.0/robotrt_sdk.egg-info/dependency_links.txt +1 -0
  27. robotrt_sdk-0.1.0/robotrt_sdk.egg-info/top_level.txt +1 -0
  28. robotrt_sdk-0.1.0/setup.cfg +4 -0
  29. robotrt_sdk-0.1.0/tests/test_data_plane_batch.py +113 -0
  30. robotrt_sdk-0.1.0/tests/test_embedded_zero_copy.py +90 -0
  31. robotrt_sdk-0.1.0/tests/test_executor.py +295 -0
  32. robotrt_sdk-0.1.0/tests/test_full_comms.py +139 -0
  33. robotrt_sdk-0.1.0/tests/test_status_mode_parity.py +233 -0
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotrt-sdk
3
+ Version: 0.1.0
4
+ Summary: RobotRT Python SDK (FFI thin wrapper)
5
+ Author: RobotRT Maintainers
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # RobotRT Python SDK (Embedded + Daemon)
15
+
16
+ This directory contains the Python-first SDK implementation on top of the frozen Core FFI ABI.
17
+
18
+ ## Scope
19
+
20
+ - Embedded mode SDK surface for node and buffer lease lifecycle.
21
+ - Embedded mode full communication APIs: pub/sub, service, action, mission.
22
+ - Embedded mode executor APIs: SimpleExecutor, CallbackExecutor, and MultiThreadExecutor.
23
+ - MultiThread one-shot APIs are `enqueue_*_once`; persistent grouped path uses explicit methods:
24
+ `register_subscription_handler_grouped_persistent`,
25
+ `register_service_handler_grouped_persistent`,
26
+ `register_action_handler_grouped_persistent`,
27
+ `register_mission_handler_grouped_persistent`.
28
+ - Daemon mode status SDK surface for snapshot/list/info/watch query.
29
+ - Unified error mapping from C ABI status and error payload.
30
+ - Zero-copy readable view via borrowed lease pointer to Python `memoryview`.
31
+
32
+ ## Embedded Communication API (MVP)
33
+
34
+ ```python
35
+ import robotrt_sdk as sdk
36
+
37
+ with sdk.Node.create() as node:
38
+ pub = node.create_publisher("/demo/topic")
39
+ sub = node.create_subscriber("/demo/topic")
40
+ pub.publish_batch([b"hello", b"robotrt", b"batch"])
41
+ batch = sub.recv_batch(8)
42
+ for _ in batch:
43
+ # Reliable topics require explicit ack to release capacity.
44
+ sub.ack()
45
+ print(batch)
46
+ print("backpressure:", sub.backpressure_signal().value)
47
+
48
+ server = node.create_service_server("/demo/service")
49
+ client = node.create_service_client("/demo/service")
50
+ request_id = client.call(b"ping")
51
+ rid, req = server.recv_request()
52
+ server.reply(rid, b"pong")
53
+ print(client.poll_response(request_id))
54
+
55
+ action_server = node.create_action_server("/demo/action")
56
+ action_client = node.create_action_client("/demo/action")
57
+ goal_id, accepted = action_client.send_goal(b"goal")
58
+ gid, goal = action_server.recv_goal()
59
+ action_server.accept_goal(gid)
60
+ action_server.publish_feedback(gid, b"50%")
61
+ action_server.succeed(gid, b"done")
62
+ print(action_client.poll_result(goal_id))
63
+
64
+ mission = node.create_mission_session(42)
65
+ mission.open("demo_mission")
66
+ mission.send_command("start:demo")
67
+ mission.reconcile("cp-1", 2)
68
+ ```
69
+
70
+ Subscriber with optional callback:
71
+
72
+ ```python
73
+ # Basic polling style
74
+ sub = node.create_subscriber("/demo/topic")
75
+
76
+ # With inline message handler
77
+ sub = node.create_subscriber("/demo/topic", on_message=lambda payload: print(payload))
78
+ ```
79
+
80
+ ## Quick Start
81
+
82
+ 1. Build the shared library:
83
+
84
+ ```bash
85
+ cargo build -p core-ffi --lib
86
+ ```
87
+
88
+ 2. Run tests:
89
+
90
+ ```bash
91
+ PYTHONPATH=bindings/python python3 -m unittest discover -s bindings/python/tests -p 'test_*.py'
92
+ ```
93
+
94
+ 3. Run the embedded zero-copy demo:
95
+
96
+ ```bash
97
+ PYTHONPATH=bindings/python python3 bindings/python/examples/embedded_zero_copy_demo.py
98
+ ```
99
+
100
+ 4. Run the daemon status demo:
101
+
102
+ ```bash
103
+ PYTHONPATH=bindings/python python3 bindings/python/examples/daemon_status_demo.py
104
+ ```
105
+
106
+ 5. Run additional examples:
107
+
108
+ ```bash
109
+ PYTHONPATH=bindings/python python3 bindings/python/examples/full_comms_demo.py
110
+ PYTHONPATH=bindings/python python3 bindings/python/examples/executor_demo.py
111
+ PYTHONPATH=bindings/python python3 bindings/python/examples/status_watch_demo.py
112
+ PYTHONPATH=bindings/python python3 bindings/python/examples/node_full_stack_demo.py
113
+ PYTHONPATH=bindings/python python3 bindings/python/examples/assembly_full_stack_demo.py
114
+ PYTHONPATH=bindings/python python3 bindings/python/examples/node_inherit_full_stack_demo.py
115
+ ```
116
+
117
+ ## Unified Sample Naming
118
+
119
+ - `node_full_stack_demo`: node-centric style, encapsulates pub/sub, service, action, mission in one app object and uses blocking `spin()`.
120
+ - `assembly_full_stack_demo`: assembly style, creates resources directly and uses `spin_async()` to keep business logic on the main thread.
121
+ - `node_inherit_full_stack_demo`: inherit-node style, directly derives from `sdk.Node` and adds app-level behavior.
122
+
123
+ ## Mode Switch API
124
+
125
+ ```python
126
+ import robotrt_sdk as sdk
127
+
128
+ # Embedded mode: read local status report.
129
+ embedded = sdk.StatusClient(mode="embedded", report_path="artifacts/introspection/local-loop-status.json")
130
+ print(embedded.node_list())
131
+
132
+ # Daemon mode: query robotrt-host status service.
133
+ daemon = sdk.StatusClient(mode="daemon", endpoint="127.0.0.1:7588", timeout_ms=1000)
134
+ print(daemon.service_list())
135
+ ```
136
+
137
+ ## Environment Variables
138
+
139
+ - `ROBOTRT_CORE_FFI_LIB`: optional absolute path to the `core-ffi` dynamic library.
140
+ - If not set, SDK auto-discovers under `target/debug` and `target/debug/deps`.
@@ -0,0 +1,127 @@
1
+ # RobotRT Python SDK (Embedded + Daemon)
2
+
3
+ This directory contains the Python-first SDK implementation on top of the frozen Core FFI ABI.
4
+
5
+ ## Scope
6
+
7
+ - Embedded mode SDK surface for node and buffer lease lifecycle.
8
+ - Embedded mode full communication APIs: pub/sub, service, action, mission.
9
+ - Embedded mode executor APIs: SimpleExecutor, CallbackExecutor, and MultiThreadExecutor.
10
+ - MultiThread one-shot APIs are `enqueue_*_once`; persistent grouped path uses explicit methods:
11
+ `register_subscription_handler_grouped_persistent`,
12
+ `register_service_handler_grouped_persistent`,
13
+ `register_action_handler_grouped_persistent`,
14
+ `register_mission_handler_grouped_persistent`.
15
+ - Daemon mode status SDK surface for snapshot/list/info/watch query.
16
+ - Unified error mapping from C ABI status and error payload.
17
+ - Zero-copy readable view via borrowed lease pointer to Python `memoryview`.
18
+
19
+ ## Embedded Communication API (MVP)
20
+
21
+ ```python
22
+ import robotrt_sdk as sdk
23
+
24
+ with sdk.Node.create() as node:
25
+ pub = node.create_publisher("/demo/topic")
26
+ sub = node.create_subscriber("/demo/topic")
27
+ pub.publish_batch([b"hello", b"robotrt", b"batch"])
28
+ batch = sub.recv_batch(8)
29
+ for _ in batch:
30
+ # Reliable topics require explicit ack to release capacity.
31
+ sub.ack()
32
+ print(batch)
33
+ print("backpressure:", sub.backpressure_signal().value)
34
+
35
+ server = node.create_service_server("/demo/service")
36
+ client = node.create_service_client("/demo/service")
37
+ request_id = client.call(b"ping")
38
+ rid, req = server.recv_request()
39
+ server.reply(rid, b"pong")
40
+ print(client.poll_response(request_id))
41
+
42
+ action_server = node.create_action_server("/demo/action")
43
+ action_client = node.create_action_client("/demo/action")
44
+ goal_id, accepted = action_client.send_goal(b"goal")
45
+ gid, goal = action_server.recv_goal()
46
+ action_server.accept_goal(gid)
47
+ action_server.publish_feedback(gid, b"50%")
48
+ action_server.succeed(gid, b"done")
49
+ print(action_client.poll_result(goal_id))
50
+
51
+ mission = node.create_mission_session(42)
52
+ mission.open("demo_mission")
53
+ mission.send_command("start:demo")
54
+ mission.reconcile("cp-1", 2)
55
+ ```
56
+
57
+ Subscriber with optional callback:
58
+
59
+ ```python
60
+ # Basic polling style
61
+ sub = node.create_subscriber("/demo/topic")
62
+
63
+ # With inline message handler
64
+ sub = node.create_subscriber("/demo/topic", on_message=lambda payload: print(payload))
65
+ ```
66
+
67
+ ## Quick Start
68
+
69
+ 1. Build the shared library:
70
+
71
+ ```bash
72
+ cargo build -p core-ffi --lib
73
+ ```
74
+
75
+ 2. Run tests:
76
+
77
+ ```bash
78
+ PYTHONPATH=bindings/python python3 -m unittest discover -s bindings/python/tests -p 'test_*.py'
79
+ ```
80
+
81
+ 3. Run the embedded zero-copy demo:
82
+
83
+ ```bash
84
+ PYTHONPATH=bindings/python python3 bindings/python/examples/embedded_zero_copy_demo.py
85
+ ```
86
+
87
+ 4. Run the daemon status demo:
88
+
89
+ ```bash
90
+ PYTHONPATH=bindings/python python3 bindings/python/examples/daemon_status_demo.py
91
+ ```
92
+
93
+ 5. Run additional examples:
94
+
95
+ ```bash
96
+ PYTHONPATH=bindings/python python3 bindings/python/examples/full_comms_demo.py
97
+ PYTHONPATH=bindings/python python3 bindings/python/examples/executor_demo.py
98
+ PYTHONPATH=bindings/python python3 bindings/python/examples/status_watch_demo.py
99
+ PYTHONPATH=bindings/python python3 bindings/python/examples/node_full_stack_demo.py
100
+ PYTHONPATH=bindings/python python3 bindings/python/examples/assembly_full_stack_demo.py
101
+ PYTHONPATH=bindings/python python3 bindings/python/examples/node_inherit_full_stack_demo.py
102
+ ```
103
+
104
+ ## Unified Sample Naming
105
+
106
+ - `node_full_stack_demo`: node-centric style, encapsulates pub/sub, service, action, mission in one app object and uses blocking `spin()`.
107
+ - `assembly_full_stack_demo`: assembly style, creates resources directly and uses `spin_async()` to keep business logic on the main thread.
108
+ - `node_inherit_full_stack_demo`: inherit-node style, directly derives from `sdk.Node` and adds app-level behavior.
109
+
110
+ ## Mode Switch API
111
+
112
+ ```python
113
+ import robotrt_sdk as sdk
114
+
115
+ # Embedded mode: read local status report.
116
+ embedded = sdk.StatusClient(mode="embedded", report_path="artifacts/introspection/local-loop-status.json")
117
+ print(embedded.node_list())
118
+
119
+ # Daemon mode: query robotrt-host status service.
120
+ daemon = sdk.StatusClient(mode="daemon", endpoint="127.0.0.1:7588", timeout_ms=1000)
121
+ print(daemon.service_list())
122
+ ```
123
+
124
+ ## Environment Variables
125
+
126
+ - `ROBOTRT_CORE_FFI_LIB`: optional absolute path to the `core-ffi` dynamic library.
127
+ - If not set, SDK auto-discovers under `target/debug` and `target/debug/deps`.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "robotrt-sdk"
7
+ version = "0.1.0"
8
+ description = "RobotRT Python SDK (FFI thin wrapper)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "RobotRT Maintainers" }]
12
+ license = "Apache-2.0"
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [tool.setuptools]
21
+ include-package-data = true
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+ include = ["robotrt_sdk", "robotrt_sdk.*"]
@@ -0,0 +1,84 @@
1
+ from ._ffi import _CORE
2
+ from ._version import __version__
3
+ import ctypes
4
+ from .core import (
5
+ ActionClient,
6
+ ActionGoalHealth,
7
+ ActionServer,
8
+ BackpressureSignal,
9
+ CallbackGroupType,
10
+ BufferDescriptor,
11
+ BufferLease,
12
+ ExternalBufferRef,
13
+ LeaseView,
14
+ MissionSession,
15
+ Node,
16
+ NodeCallbackBinding,
17
+ Publisher,
18
+ CallbackExecutor,
19
+ MultiThreadExecutor,
20
+ MultiThreadExecutorStats,
21
+ RegisteredWork,
22
+ RegisteredWorkKind,
23
+ ServiceClient,
24
+ ServiceServer,
25
+ SimpleExecutor,
26
+ Subscriber,
27
+ TimerRegistration,
28
+ )
29
+ from .errors import ErrorInfo, RobotRtError
30
+ from .lifecycle import LifecycleNode, LifecycleState
31
+ from .logger import Logger
32
+ from .status import StatusClient
33
+
34
+ __all__ = [
35
+ "BufferDescriptor",
36
+ "BufferLease",
37
+ "ErrorInfo",
38
+ "LeaseView",
39
+ "LifecycleNode",
40
+ "LifecycleState",
41
+ "Logger",
42
+ "Node",
43
+ "NodeCallbackBinding",
44
+ "Publisher",
45
+ "Subscriber",
46
+ "ServiceClient",
47
+ "ServiceServer",
48
+ "ActionClient",
49
+ "ActionGoalHealth",
50
+ "ActionServer",
51
+ "BackpressureSignal",
52
+ "CallbackGroupType",
53
+ "CallbackExecutor",
54
+ "ExternalBufferRef",
55
+ "MissionSession",
56
+ "MultiThreadExecutor",
57
+ "MultiThreadExecutorStats",
58
+ "RobotRtError",
59
+ "RegisteredWork",
60
+ "RegisteredWorkKind",
61
+ "StatusClient",
62
+ "SimpleExecutor",
63
+ "TimerRegistration",
64
+ "__version__",
65
+ "abi_version",
66
+ "protocol_version",
67
+ "build_info",
68
+ ]
69
+
70
+
71
+ def abi_version() -> int:
72
+ return int(_CORE.lib.robotrt_core_abi_version())
73
+
74
+
75
+ def protocol_version() -> int:
76
+ return int(_CORE.lib.robotrt_core_protocol_version())
77
+
78
+
79
+ def build_info() -> str:
80
+ needed = ctypes.c_uint32(0)
81
+ _CORE.lib.robotrt_core_build_info_copy(None, 0, ctypes.byref(needed))
82
+ buf = ctypes.create_string_buffer(needed.value + 1)
83
+ _CORE.lib.robotrt_core_build_info_copy(buf, needed.value + 1, ctypes.byref(needed))
84
+ return buf.value.decode("utf-8", errors="replace")
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from ._ffi_loader import CoreFfi, _CORE
4
+ from ._ffi_types import (
5
+ RT_DURABILITY_TRANSIENT_LOCAL,
6
+ RT_DURABILITY_VOLATILE,
7
+ RT_OK,
8
+ RtActionGoalHealth,
9
+ RtBufferDescriptor,
10
+ RtErrorInfo,
11
+ RtExternalBufferRef,
12
+ RtTopicQosProfile,
13
+ )
14
+
15
+ __all__ = [
16
+ "RT_OK",
17
+ "CoreFfi",
18
+ "RT_DURABILITY_TRANSIENT_LOCAL",
19
+ "RT_DURABILITY_VOLATILE",
20
+ "RtActionGoalHealth",
21
+ "RtBufferDescriptor",
22
+ "RtErrorInfo",
23
+ "RtExternalBufferRef",
24
+ "RtTopicQosProfile",
25
+ "_CORE",
26
+ ]
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import os
5
+ import platform
6
+ from pathlib import Path
7
+
8
+ from ._ffi_signatures import declare_signatures
9
+ from ._ffi_types import RT_OK, RtErrorInfo
10
+ from .errors import ErrorInfo, RobotRtError
11
+
12
+
13
+ def _workspace_root() -> Path:
14
+ return Path(__file__).resolve().parents[3]
15
+
16
+
17
+ def _dylib_name() -> str:
18
+ name = platform.system().lower()
19
+ if name == "darwin":
20
+ return "librobotrt_core_ffi.dylib"
21
+ if name == "windows":
22
+ return "robotrt_core_ffi.dll"
23
+ return "librobotrt_core_ffi.so"
24
+
25
+
26
+ def _resolve_library_path() -> Path:
27
+ env = os.environ.get("ROBOTRT_CORE_FFI_LIB")
28
+ if env:
29
+ p = Path(env).expanduser().resolve()
30
+ if p.exists():
31
+ return p
32
+ raise FileNotFoundError(f"ROBOTRT_CORE_FFI_LIB does not exist: {p}")
33
+
34
+ root = _workspace_root()
35
+ direct = root / "target" / "debug" / _dylib_name()
36
+ if direct.exists():
37
+ return direct
38
+
39
+ deps = root / "target" / "debug" / "deps"
40
+ if deps.exists():
41
+ suffixes = {".dylib", ".so", ".dll"}
42
+ for candidate in sorted(deps.iterdir()):
43
+ if candidate.suffix not in suffixes:
44
+ continue
45
+ if candidate.name.startswith("librobotrt_core_ffi") or candidate.name == _dylib_name():
46
+ return candidate
47
+
48
+ raise FileNotFoundError("robotrt core ffi dynamic library not found; run cargo build -p core-ffi --lib")
49
+
50
+
51
+ class CoreFfi:
52
+ def __init__(self) -> None:
53
+ self.library_path = _resolve_library_path()
54
+ self.lib = ctypes.CDLL(str(self.library_path))
55
+ declare_signatures(self.lib)
56
+
57
+ def last_error(self) -> ErrorInfo:
58
+ info = RtErrorInfo()
59
+ self.lib.robotrt_error_last(ctypes.byref(info))
60
+
61
+ needed = ctypes.c_uint32(0)
62
+ self.lib.robotrt_error_message_copy(None, 0, ctypes.byref(needed))
63
+ message = ""
64
+ if needed.value > 0:
65
+ buf = ctypes.create_string_buffer(needed.value)
66
+ self.lib.robotrt_error_message_copy(buf, needed.value, ctypes.byref(needed))
67
+ message = buf.value.decode("utf-8", errors="replace")
68
+
69
+ return ErrorInfo(
70
+ code=int(info.code),
71
+ domain=int(info.domain),
72
+ retryable=bool(info.retryable),
73
+ message=message,
74
+ )
75
+
76
+ def check(self, status: int, context: str) -> None:
77
+ if status == RT_OK:
78
+ return
79
+ raise RobotRtError(self.last_error(), context)
80
+
81
+
82
+ _CORE = CoreFfi()