armnet-runtime 0.1.0__py3-none-any.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.
@@ -0,0 +1,66 @@
1
+ """armnet runtime SDK \u2014 used inside customer containers on a cell.
2
+
3
+ Customer-facing surface: a ``@main`` decorator and a :class:`Context`. The
4
+ container's entrypoint is the ``armnet-runtime`` console script
5
+ (installed by this package); it loads the user's file, finds the
6
+ ``@main``-decorated function, builds the context, and calls it.
7
+
8
+ Typical use::
9
+
10
+ from armnet_runtime import main, Context
11
+
12
+ @main
13
+ def run(ctx: Context):
14
+ seed = ctx.args.get("seed", 0)
15
+ ctx.report_progress("starting")
16
+ ...
17
+ return {"success_rate": 1.0}
18
+
19
+ Wire types (``Embodiment``, ``Task``, ``JobSpec``, ``JobResult``, ...) are
20
+ re-exported from :mod:`armnet_core` for ergonomics, so customer code
21
+ never needs to know about the core package directly.
22
+ """
23
+
24
+ from armnet_core import (
25
+ Embodiment,
26
+ Job,
27
+ JobResult,
28
+ JobSpec,
29
+ JobStatus,
30
+ RemoteExecutionError,
31
+ Task,
32
+ TerminalStatus,
33
+ )
34
+
35
+ from armnet_runtime.context import (
36
+ BIMANUAL_SO101_EMBODIMENT,
37
+ SO101_EMBODIMENT,
38
+ Cell,
39
+ CompletionStatus,
40
+ Context,
41
+ ResetTimeoutException,
42
+ require_so101_embodiment,
43
+ )
44
+ from armnet_runtime.decorator import MainRegistrationError, main
45
+
46
+ __all__ = [
47
+ "BIMANUAL_SO101_EMBODIMENT",
48
+ "SO101_EMBODIMENT",
49
+ "Cell",
50
+ "CompletionStatus",
51
+ "Context",
52
+ "require_so101_embodiment",
53
+ "Embodiment",
54
+ "Job",
55
+ "JobResult",
56
+ "JobSpec",
57
+ "JobStatus",
58
+ "MainRegistrationError",
59
+ "RemoteExecutionError",
60
+ "ResetTimeoutException",
61
+ "Task",
62
+ "TerminalStatus",
63
+ "main",
64
+ ]
65
+
66
+ __version__ = "0.1.0"
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: rerun.proto
4
+ # Protobuf Python Version: 5.26.1
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+
16
+
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0brerun.proto\x12\x10remoterobo.rerun\"y\n\x05Image\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\x31\n\x08\x65ncoding\x18\x02 \x01(\x0e\x32\x1f.remoterobo.rerun.ImageEncoding\x12\r\n\x05width\x18\x03 \x01(\r\x12\x0e\n\x06height\x18\x04 \x01(\r\x12\x10\n\x08\x63hannels\x18\x05 \x01(\r\"f\n\nRerunEntry\x12\x13\n\x0b\x65ntity_path\x18\x01 \x01(\t\x12\x10\n\x06scalar\x18\x02 \x01(\x01H\x00\x12(\n\x05image\x18\x03 \x01(\x0b\x32\x17.remoterobo.rerun.ImageH\x00\x42\x07\n\x05value\"\x82\x01\n\x0bRerunPacket\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x01\x12-\n\x07\x65ntries\x18\x03 \x03(\x0b\x32\x1c.remoterobo.rerun.RerunEntry\x12\x0e\n\x06obs_ts\x18\x04 \x01(\x01\x12\x11\n\tsubmit_ts\x18\x05 \x01(\x01*d\n\rImageEncoding\x12\x1e\n\x1aIMAGE_ENCODING_UNSPECIFIED\x10\x00\x12\x17\n\x13IMAGE_ENCODING_JPEG\x10\x01\x12\x1a\n\x16IMAGE_ENCODING_RAW_RGB\x10\x02\x62\x06proto3')
18
+
19
+ _globals = globals()
20
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rerun_pb2', _globals)
22
+ if not _descriptor._USE_C_DESCRIPTORS:
23
+ DESCRIPTOR._loaded_options = None
24
+ _globals['_IMAGEENCODING']._serialized_start=393
25
+ _globals['_IMAGEENCODING']._serialized_end=493
26
+ _globals['_IMAGE']._serialized_start=33
27
+ _globals['_IMAGE']._serialized_end=154
28
+ _globals['_RERUNENTRY']._serialized_start=156
29
+ _globals['_RERUNENTRY']._serialized_end=258
30
+ _globals['_RERUNPACKET']._serialized_start=261
31
+ _globals['_RERUNPACKET']._serialized_end=391
32
+ # @@protoc_insertion_point(module_scope)
armnet_runtime/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ """``armnet-runtime`` console script.
2
+
3
+ This is the container entrypoint for any image built on top of the
4
+ armnet runtime SDK. The container's ``CMD`` looks like::
5
+
6
+ CMD ["armnet-runtime", "/app/hello.py"]
7
+
8
+ and this script:
9
+
10
+ 1. (M3+) Performs the LeRobot import-system swap so customer code
11
+ transparently routes hardware operations through the safety-aware
12
+ robot connector. Currently a marker; nothing is swapped yet.
13
+ 2. Adds the user script's directory to ``sys.path`` so it can import
14
+ sibling modules.
15
+ 3. Imports the user script \u2014 which executes the ``@main`` decorator as
16
+ a side effect, registering the entry-point function.
17
+ 4. Builds a :class:`~armnet_runtime.Context` from cell-injected env
18
+ vars.
19
+ 5. Calls the registered function with the context.
20
+ 6. Prints the function's return value with the
21
+ :data:`~armnet_runtime.markers.RESULT_MARKER_JSON` prefix; the
22
+ cell extracts that and surfaces it as
23
+ :attr:`~armnet_core.JobResult.return_value`.
24
+ 7. Maps exceptions to a non-zero exit so the cell records the job as
25
+ ``FAILED``; the traceback ends up in the captured stderr.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import importlib.util
32
+ import json
33
+ import sys
34
+ import traceback
35
+ from datetime import datetime
36
+ from pathlib import Path
37
+ from typing import TextIO
38
+ from typing import Sequence
39
+
40
+ from armnet_runtime.context import build_context
41
+ from armnet_runtime.decorator import registered_main
42
+ from armnet_runtime.markers import (
43
+ MAX_TRACEBACK_BYTES,
44
+ RESULT_MARKER,
45
+ RESULT_MARKER_JSON,
46
+ TRACEBACK_MARKER_JSON,
47
+ )
48
+
49
+ # Exit codes. Keep stable \u2014 the cell may eventually distinguish
50
+ # runtime-CLI failures from user-code failures, e.g. by mapping these to
51
+ # JobResult.error.
52
+ EXIT_OK = 0
53
+ EXIT_USER_RAISED = 1
54
+ EXIT_USAGE = 2
55
+ EXIT_NO_MAIN = 3
56
+ EXIT_USER_IMPORT_FAILED = 4
57
+ EXIT_BAD_CONTEXT = 5
58
+
59
+ _USER_MODULE_NAME = "__armnet_user_main__"
60
+
61
+
62
+ class _TimestampedStream:
63
+ """Prefix every stdout/stderr line with a Unix timestamp."""
64
+
65
+ def __init__(self, wrapped: TextIO) -> None:
66
+ self._wrapped = wrapped
67
+ self._at_line_start = True
68
+
69
+ def write(self, text: str) -> int:
70
+ written = 0
71
+ for chunk in text.splitlines(keepends=True):
72
+ if self._at_line_start and chunk:
73
+ prefix = f"[{datetime.now().isoformat(timespec='milliseconds')}] "
74
+ self._wrapped.write(prefix)
75
+ self._wrapped.write(chunk)
76
+ written += len(chunk)
77
+ self._at_line_start = chunk.endswith(("\n", "\r"))
78
+ return written
79
+
80
+ def flush(self) -> None:
81
+ self._wrapped.flush()
82
+
83
+ def isatty(self) -> bool:
84
+ return self._wrapped.isatty()
85
+
86
+ def __getattr__(self, name: str) -> object:
87
+ return getattr(self._wrapped, name)
88
+
89
+
90
+ def _install_timestamped_streams() -> None:
91
+ sys.stdout = _TimestampedStream(sys.stdout) # type: ignore[assignment]
92
+ sys.stderr = _TimestampedStream(sys.stderr) # type: ignore[assignment]
93
+
94
+
95
+ def _parse_args(argv: Sequence[str]) -> argparse.Namespace:
96
+ parser = argparse.ArgumentParser(
97
+ prog="armnet-runtime",
98
+ description="Run a armnet @main-decorated script inside a cell container.",
99
+ )
100
+ parser.add_argument(
101
+ "script",
102
+ type=Path,
103
+ help="Path to the customer's Python script containing one @main-decorated function.",
104
+ )
105
+ return parser.parse_args(argv)
106
+
107
+
108
+ def _load_user_script(script_path: Path) -> None:
109
+ """Import the user's script; the @main side-effect populates the registry."""
110
+
111
+ if not script_path.is_file():
112
+ raise FileNotFoundError(f"script not found: {script_path}")
113
+
114
+ sys.path.insert(0, str(script_path.parent))
115
+
116
+ spec = importlib.util.spec_from_file_location(_USER_MODULE_NAME, script_path)
117
+ if spec is None or spec.loader is None:
118
+ raise ImportError(f"could not build import spec for {script_path}")
119
+ module = importlib.util.module_from_spec(spec)
120
+ sys.modules[_USER_MODULE_NAME] = module
121
+ spec.loader.exec_module(module)
122
+
123
+
124
+ def _print_result(value: object) -> None:
125
+ """Render the user's return value to stdout.
126
+
127
+ Two lines: a human-readable line for ergonomic logs, and a marker line
128
+ with JSON for the cell to parse into ``JobResult.return_value``.
129
+ """
130
+
131
+ print(f"\n{RESULT_MARKER} {value!r}", flush=True)
132
+ try:
133
+ encoded = json.dumps(value, default=repr)
134
+ except (TypeError, ValueError):
135
+ encoded = json.dumps(repr(value))
136
+ print(f"{RESULT_MARKER_JSON}{encoded}", flush=True)
137
+
138
+
139
+ def run(argv: Sequence[str] | None = None) -> int:
140
+ args = _parse_args(sys.argv[1:] if argv is None else list(argv))
141
+ _install_timestamped_streams()
142
+
143
+ # Install guarded LeRobot import replacement before loading user code so
144
+ # `from lerobot...` inside the script sees the remote Feetech proxy.
145
+ from armnet_runtime.lerobot import install_lerobot_remote_mode
146
+
147
+ install_lerobot_remote_mode()
148
+
149
+ try:
150
+ _load_user_script(args.script)
151
+ except Exception:
152
+ print("armnet-runtime: failed to import user script:", file=sys.stderr)
153
+ traceback.print_exc()
154
+ # An import failure is still a Python traceback in user code (think
155
+ # syntax error in `hello.py`); surface it the same way as a runtime
156
+ # raise so the cell lifts it onto JobResult.traceback.
157
+ _print_traceback_marker(traceback.format_exc())
158
+ return EXIT_USER_IMPORT_FAILED
159
+
160
+ fn = registered_main()
161
+ if fn is None:
162
+ print(
163
+ f"armnet-runtime: no @main-decorated function found in {args.script}. "
164
+ "Decorate exactly one function with `@main` to mark it as the entry point.",
165
+ file=sys.stderr,
166
+ )
167
+ return EXIT_NO_MAIN
168
+
169
+ try:
170
+ ctx = build_context()
171
+ except Exception as exc: # noqa: BLE001
172
+ print(f"armnet-runtime: failed to build context: {exc}", file=sys.stderr)
173
+ return EXIT_BAD_CONTEXT
174
+
175
+ try:
176
+ result = fn(ctx)
177
+ except SystemExit:
178
+ # Customer code called sys.exit explicitly \u2014 let that propagate.
179
+ raise
180
+ except Exception:
181
+ # Stderr: full, human-readable for `docker logs` users.
182
+ print(
183
+ f"armnet-runtime: @main function raised; "
184
+ f"job {ctx.job_id} will be marked FAILED.",
185
+ file=sys.stderr,
186
+ )
187
+ traceback.print_exc()
188
+ # Stdout: structured marker the cell parses into JobResult.traceback.
189
+ _print_traceback_marker(traceback.format_exc())
190
+ return EXIT_USER_RAISED
191
+
192
+ if result is not None:
193
+ _print_result(result)
194
+ return EXIT_OK
195
+
196
+
197
+ def _print_traceback_marker(tb_text: str) -> None:
198
+ """Emit the `[armnet:traceback]:json` marker on stdout.
199
+
200
+ Truncates at :data:`MAX_TRACEBACK_BYTES` of UTF-8 to keep NATS payloads
201
+ bounded. The full untruncated traceback is still in stderr (printed by
202
+ ``traceback.print_exc()`` above), so power users on the cell side can
203
+ read ``JobResult.stderr`` if the truncated head isn't enough.
204
+ """
205
+
206
+ encoded = tb_text.encode("utf-8")
207
+ if len(encoded) > MAX_TRACEBACK_BYTES:
208
+ # Truncate on a UTF-8 boundary, then append a marker visible inside
209
+ # the truncated payload itself.
210
+ head = encoded[:MAX_TRACEBACK_BYTES].decode("utf-8", errors="ignore")
211
+ tb_text = head + "\n... [traceback truncated; full version in stderr]"
212
+
213
+ print(f"{TRACEBACK_MARKER_JSON}{json.dumps(tb_text)}", flush=True)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ sys.exit(run())