reactor-runtime 2.3.2__tar.gz → 2.5.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 (141) hide show
  1. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/PKG-INFO +22 -3
  2. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/README.md +20 -1
  3. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/pyproject.toml +9 -4
  4. reactor_runtime-2.5.0/src/reactor_cli/commands/init.py +155 -0
  5. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/commands/run.py +35 -8
  6. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/commands/schema.py +11 -3
  7. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/main.py +1 -1
  8. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/__init__.py +4 -0
  9. reactor_runtime-2.5.0/src/reactor_runtime/config.py +170 -0
  10. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/__init__.py +6 -0
  11. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/driver/pipeline_executor.py +24 -6
  12. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/events/messages.py +10 -0
  13. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/internal/input_buffer.py +33 -20
  14. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/internal/reactor_core.py +11 -3
  15. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtime_api.py +125 -3
  16. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/headless/input_feeder.py +11 -2
  17. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/http/http_runtime.py +81 -8
  18. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/http/types.py +37 -0
  19. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/schema.py +71 -5
  20. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/__init__.py +6 -0
  21. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/audio_track.py +148 -2
  22. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/client.py +144 -4
  23. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/events.py +23 -3
  24. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/client.py +185 -37
  25. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +2 -0
  26. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +5 -1
  27. reactor_runtime-2.5.0/src/reactor_runtime/transports/gstreamer/decoders/opus.py +54 -0
  28. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +15 -16
  29. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/interface.py +20 -0
  30. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/media.py +43 -0
  31. reactor_runtime-2.5.0/src/reactor_runtime/utils/paths.py +83 -0
  32. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime.egg-info/PKG-INFO +22 -3
  33. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime.egg-info/SOURCES.txt +4 -1
  34. reactor_runtime-2.5.0/src/reactor_runtime.egg-info/entry_points.txt +2 -0
  35. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime.egg-info/requires.txt +1 -1
  36. reactor_runtime-2.5.0/src/template/Dockerfile +66 -0
  37. reactor_runtime-2.5.0/src/template/README.md +85 -0
  38. reactor_runtime-2.3.2/src/template/reactor.yaml → reactor_runtime-2.5.0/src/template/config.yaml +3 -3
  39. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/template/model.py +1 -1
  40. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/template/pipeline.py +1 -1
  41. reactor_runtime-2.5.0/src/template/reactor.yaml +7 -0
  42. reactor_runtime-2.5.0/src/template/requirements.txt +16 -0
  43. reactor_runtime-2.3.2/src/reactor_cli/commands/init.py +0 -90
  44. reactor_runtime-2.3.2/src/reactor_runtime/config.py +0 -49
  45. reactor_runtime-2.3.2/src/reactor_runtime.egg-info/entry_points.txt +0 -2
  46. reactor_runtime-2.3.2/src/template/README.md +0 -45
  47. reactor_runtime-2.3.2/src/template/config.yml +0 -3
  48. reactor_runtime-2.3.2/src/template/requirements.txt +0 -2
  49. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/setup.cfg +0 -0
  50. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/api/__init__.py +0 -0
  51. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/commands/__init__.py +0 -0
  52. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/utils/__init__.py +0 -0
  53. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/utils/config.py +0 -0
  54. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/utils/runtime.py +0 -0
  55. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_cli/utils/version.py +0 -0
  56. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/defaults.py +0 -0
  57. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  58. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  59. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
  60. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/events/connected.py +0 -0
  61. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/events/event.py +0 -0
  62. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/events/upload.py +0 -0
  63. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  64. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  65. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
  66. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/model/decorators.py +0 -0
  67. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/model/handlers.py +0 -0
  68. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  69. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  70. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  71. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  72. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  73. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  74. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  75. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
  76. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  77. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/interface/upload.py +0 -0
  78. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/model_state.py +0 -0
  79. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/__init__.py +0 -0
  80. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  81. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
  82. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
  83. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  84. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/helpers.py +0 -0
  85. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  86. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  87. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/profiler.py +0 -0
  88. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/profiling/singleton.py +0 -0
  89. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  90. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -0
  91. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
  92. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/schema_validator.py +0 -0
  93. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  94. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  95. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  96. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  97. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/config.py +0 -0
  98. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  99. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  100. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  101. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  102. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  103. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  104. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  105. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  106. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  107. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  108. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  109. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  110. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  111. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  112. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  113. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  114. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  115. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  116. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  117. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  118. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  119. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  120. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  121. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  122. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  123. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  124. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  125. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  126. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  127. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  128. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  129. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
  130. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  131. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  132. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
  133. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/transports/types.py +0 -0
  134. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/utils/launch.py +0 -0
  135. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/utils/loader.py +0 -0
  136. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/utils/log.py +0 -0
  137. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/utils/messages.py +0 -0
  138. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime/utils/typing.py +0 -0
  139. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  140. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
  141. {reactor_runtime-2.3.2 → reactor_runtime-2.5.0}/src/template/__init__.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.3.2
3
+ Version: 2.5.0
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
7
7
  Description-Content-Type: text/markdown
8
- Requires-Dist: numpy<2.0
8
+ Requires-Dist: numpy>=1.24.0
9
9
  Requires-Dist: pydantic>=2.0.0
10
10
  Requires-Dist: omegaconf>=2.3.0
11
11
  Requires-Dist: av>=14.0.0
@@ -68,4 +68,23 @@ Implement `inference()` as a generator that yields frames. Clients can update `s
68
68
  - **Real-time streaming** — frames delivered over WebRTC as they're generated
69
69
  - **Live interaction** — clients change inputs mid-generation via typed, validated state
70
70
  - **No transport code** — no WebRTC, WebSocket, or video encoding to manage
71
- - **Scaffold & run** — `reactor init` creates a project; `reactor run` starts it locally
71
+ - **Scaffold & run** — the Go `reactor` CLI gives you `reactor init` / `reactor run`; the
72
+ Python runtime ships its own `reactor-runtime` script for standalone use
73
+
74
+ ## Standalone usage (no Go CLI)
75
+
76
+ The runtime CLI is installed as `reactor-runtime` so it never shadows the Go
77
+ `reactor` binary. If you only use the Python runtime:
78
+
79
+ ```bash
80
+ pip install reactor-runtime
81
+ reactor-runtime init my-model
82
+ cd my-model
83
+ python -m venv .venv && . .venv/bin/activate
84
+ pip install -r requirements.txt
85
+ reactor-runtime run
86
+ ```
87
+
88
+ The Go `reactor` CLI delegates to this same `reactor-runtime` script under the
89
+ hood; both `reactor run` (Go) and `reactor-runtime run` (standalone) execute the
90
+ same Python entry point.
@@ -41,4 +41,23 @@ Implement `inference()` as a generator that yields frames. Clients can update `s
41
41
  - **Real-time streaming** — frames delivered over WebRTC as they're generated
42
42
  - **Live interaction** — clients change inputs mid-generation via typed, validated state
43
43
  - **No transport code** — no WebRTC, WebSocket, or video encoding to manage
44
- - **Scaffold & run** — `reactor init` creates a project; `reactor run` starts it locally
44
+ - **Scaffold & run** — the Go `reactor` CLI gives you `reactor init` / `reactor run`; the
45
+ Python runtime ships its own `reactor-runtime` script for standalone use
46
+
47
+ ## Standalone usage (no Go CLI)
48
+
49
+ The runtime CLI is installed as `reactor-runtime` so it never shadows the Go
50
+ `reactor` binary. If you only use the Python runtime:
51
+
52
+ ```bash
53
+ pip install reactor-runtime
54
+ reactor-runtime init my-model
55
+ cd my-model
56
+ python -m venv .venv && . .venv/bin/activate
57
+ pip install -r requirements.txt
58
+ reactor-runtime run
59
+ ```
60
+
61
+ The Go `reactor` CLI delegates to this same `reactor-runtime` script under the
62
+ hood; both `reactor run` (Go) and `reactor-runtime run` (standalone) execute the
63
+ same Python entry point.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.3.2"
7
+ version = "2.5.0"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -13,7 +13,7 @@ readme = "README.md"
13
13
  requires-python = ">=3.9"
14
14
 
15
15
  dependencies = [
16
- "numpy<2.0",
16
+ "numpy>=1.24.0",
17
17
  "pydantic>=2.0.0",
18
18
  "omegaconf>=2.3.0",
19
19
  "av>=14.0.0",
@@ -38,10 +38,15 @@ gst = [
38
38
  ]
39
39
 
40
40
  [project.scripts]
41
- reactor = "reactor_cli.main:main"
41
+ # The runtime ships a CLI for standalone use (no Go reactor-cli required).
42
+ # Named `reactor-runtime` so it never collides with the Go `reactor` binary,
43
+ # which delegates to this script when both are installed.
44
+ reactor-runtime = "reactor_cli.main:main"
42
45
 
43
46
  [tool.setuptools.package-data]
44
- template = ["*.yaml", "*.yml", "*.txt", "*.md"]
47
+ # Dockerfile is included so the scaffolded workspace ships a partner-ready
48
+ # image baseline. The literal name (no extension) requires an explicit entry.
49
+ template = ["*.yaml", "*.yml", "*.txt", "*.md", "Dockerfile"]
45
50
 
46
51
  [tool.reactor-proto]
47
52
  # Proto dependency from reactor-team/reactor-proto
@@ -0,0 +1,155 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """Init command — scaffolds a new Reactor model workspace."""
3
+
4
+ import pathlib
5
+ import importlib.resources
6
+
7
+
8
+ _TEMPLATE_FILES = {
9
+ "pipeline.py": "pipeline.py",
10
+ "model.py": "model.py",
11
+ "reactor.yaml": "reactor.yaml",
12
+ "config.yaml": "config.yaml",
13
+ "requirements.txt": "requirements.txt",
14
+ "README.md": "README.md",
15
+ # Partner-facing Dockerfile baseline (python:3.12-slim + GStreamer apt
16
+ # deps + reactor-runtime run). Listed last so the printed file order
17
+ # mirrors the order partners typically read them in.
18
+ "Dockerfile": "Dockerfile",
19
+ }
20
+
21
+
22
+ class InitCommand:
23
+ @staticmethod
24
+ def register_subcommand(subparsers):
25
+ """Register init command"""
26
+ init_parser = subparsers.add_parser(
27
+ "init", help="Initialize a new reactor model workspace"
28
+ )
29
+ init_parser.add_argument(
30
+ "name",
31
+ nargs="?",
32
+ default=None,
33
+ help=(
34
+ "Name of the model. When provided, a new directory with "
35
+ "this name is created in the current folder. When omitted, "
36
+ "the current folder is used (must be empty)."
37
+ ),
38
+ )
39
+ # --cli is set by the Go `reactor` CLI when it forwards to this
40
+ # Python init via `runtime.PythonRun`. The Go side then prints its
41
+ # own next-steps message tailored to the CLI workflow (workspace
42
+ # .venv was created, `reactor run` picks it up, etc.), so when
43
+ # this flag is present we suppress the standalone post-init
44
+ # message here to avoid duplicate / contradictory guidance.
45
+ # Behavior is otherwise identical — only the trailing print block
46
+ # is gated on this flag.
47
+ init_parser.add_argument(
48
+ "--cli",
49
+ action="store_true",
50
+ default=False,
51
+ help="Reserved for use by the reactor CLI; suppresses the standalone post-init message.",
52
+ )
53
+ init_parser.set_defaults(func=InitCommand)
54
+
55
+ def __init__(self, args):
56
+ self.args = args
57
+
58
+ def run(self):
59
+ """Initialize a Reactor model workspace.
60
+
61
+ Two modes:
62
+ * ``reactor-runtime init <name>``: create a new ``<name>`` directory
63
+ under the current folder and scaffold into it.
64
+ * ``reactor-runtime init`` (no arg): scaffold into the current
65
+ folder. The folder must be empty (modulo dotfiles) so we never
66
+ clobber anything by accident.
67
+ """
68
+ if self.args.name:
69
+ model_name = self.args.name
70
+ target_dir = pathlib.Path.cwd() / model_name
71
+ if target_dir.exists():
72
+ print(f"Error: Directory '{model_name}' already exists")
73
+ print("Please choose a different name or remove the existing directory")
74
+ return
75
+ try:
76
+ target_dir.mkdir()
77
+ except Exception as e:
78
+ print(f"Error creating directory '{model_name}': {e}")
79
+ return
80
+ else:
81
+ target_dir = pathlib.Path.cwd()
82
+ model_name = target_dir.name
83
+ visible = [p for p in target_dir.iterdir() if not p.name.startswith(".")]
84
+ if visible:
85
+ # The Go `reactor` CLI streams our stdout verbatim when it
86
+ # forwards via PythonRun (--cli), so the retry hint needs
87
+ # to use whichever entrypoint the user is actually driving:
88
+ # `reactor` for the Go-CLI flow (standalone `reactor-runtime`
89
+ # may not even be on PATH), `reactor-runtime` otherwise.
90
+ cli_name = "reactor" if self.args.cli else "reactor-runtime"
91
+ print(
92
+ f"Error: current folder ({target_dir}) is not empty. "
93
+ f"Pass a name (e.g. `{cli_name} init my-model`) or "
94
+ "run from an empty folder."
95
+ )
96
+ return
97
+
98
+ try:
99
+ import template as template_package
100
+ except ImportError:
101
+ print(
102
+ "Error: template package not found. "
103
+ "Make sure reactor_runtime is properly installed."
104
+ )
105
+ if self.args.name:
106
+ target_dir.rmdir()
107
+ return
108
+
109
+ created = []
110
+ for dest_name, src_name in _TEMPLATE_FILES.items():
111
+ dest_path = target_dir / dest_name
112
+ try:
113
+ content = (
114
+ importlib.resources.files(template_package)
115
+ .joinpath(src_name)
116
+ .read_text(encoding="utf-8")
117
+ )
118
+ if dest_name == "reactor.yaml":
119
+ content = content.replace("name: my-model", f"name: {model_name}")
120
+ dest_path.write_text(content)
121
+ created.append(dest_name)
122
+ except FileNotFoundError:
123
+ print(f"Warning: template file {src_name} not found, skipping")
124
+
125
+ if not created:
126
+ print("Error: no template files were created")
127
+ if self.args.name:
128
+ target_dir.rmdir()
129
+ return
130
+
131
+ for name in created:
132
+ print(f" {name}")
133
+
134
+ print(f"\nReactor workspace '{model_name}' created.")
135
+
136
+ # When invoked from the Go reactor CLI (--cli), the caller emits
137
+ # its own next-steps message that knows about the workspace .venv
138
+ # it just created and the `reactor run` / `reactor model register`
139
+ # commands. We stay silent here so the user only sees one
140
+ # consistent block of guidance.
141
+ if self.args.cli:
142
+ return
143
+
144
+ # Standalone path: partners using `pip install reactor-runtime`
145
+ # directly. They don't have a Go CLI to lean on, so spell out the
146
+ # full workspace-venv provisioning recipe.
147
+ if self.args.name:
148
+ print(f"\n cd {model_name}")
149
+ print(" # With uv (https://docs.astral.sh/uv/ — recommended):")
150
+ print(" uv venv .venv && . .venv/bin/activate")
151
+ print(" uv pip install -r requirements.txt")
152
+ print(" # Or with plain pip:")
153
+ print(" python -m venv .venv && . .venv/bin/activate")
154
+ print(" pip install -r requirements.txt")
155
+ print(" reactor-runtime run\n")
@@ -20,6 +20,7 @@ from reactor_runtime.transports.config import (
20
20
  )
21
21
  from reactor_runtime.utils.launch import run_reactor_runtime
22
22
  from reactor_runtime.utils.log import get_logger
23
+ from reactor_runtime.utils.paths import _set_configured_weights_path
23
24
 
24
25
  logger = get_logger(__name__)
25
26
 
@@ -102,22 +103,30 @@ class RunCommand:
102
103
  if not reactor_yaml_path.exists():
103
104
  print(
104
105
  f"Error: {_REACTOR_YAML} not found in {model_path}.\n"
105
- f"Create a {_REACTOR_YAML} with at least: model: module:ClassName"
106
+ f"Create a {_REACTOR_YAML} with at least:\n"
107
+ " runtime:\n"
108
+ " import: module:ClassName"
106
109
  )
107
110
  return
108
111
 
109
112
  raw_yaml = OmegaConf.to_container(
110
113
  OmegaConf.load(str(reactor_yaml_path)), resolve=True
111
114
  )
112
- if not isinstance(raw_yaml, dict) or "model" not in raw_yaml:
115
+ if not isinstance(raw_yaml, dict):
116
+ print(f"Error: {_REACTOR_YAML} must be a YAML mapping")
117
+ return
118
+
119
+ try:
120
+ reactor_config = ReactorConfig.from_dict(raw_yaml)
121
+ except KeyError:
113
122
  print(
114
- f"Error: {_REACTOR_YAML} must contain a 'model' field "
115
- "(e.g. model: my_module:MyModel)"
123
+ f"Error: {_REACTOR_YAML} must declare a model entrypoint, "
124
+ "either as 'runtime.import' (e.g. 'runtime:\\n import: "
125
+ "my_module:MyModel') or, for legacy configs, as a top-level "
126
+ "'model: my_module:MyModel' field."
116
127
  )
117
128
  return
118
129
 
119
- reactor_config = ReactorConfig.from_dict(raw_yaml)
120
-
121
130
  # ---------------------------------------------------------
122
131
  # Model name (from YAML or env override)
123
132
  # ---------------------------------------------------------
@@ -148,7 +157,7 @@ class RunCommand:
148
157
  if model_overrides and not reactor_config.config:
149
158
  print(
150
159
  "Error: --model.* overrides require a 'config' field in "
151
- f"{_REACTOR_YAML} (e.g. config: config.yml)"
160
+ f"{_REACTOR_YAML} (e.g. config: config.yaml)"
152
161
  )
153
162
  return
154
163
 
@@ -157,6 +166,21 @@ class RunCommand:
157
166
  reactor_config.config = str(model_path / reactor_config.config)
158
167
  reactor_config.config_overrides = model_overrides
159
168
 
169
+ # ---------------------------------------------------------
170
+ # Wire reactor.yaml's weights_path into the get_weights_path()
171
+ # helper so model code can call it with no arguments and still
172
+ # see the YAML override. Relative values are resolved against the
173
+ # model root, mirroring how `runtime.config` is resolved above —
174
+ # partners write `runtime.weights_path: ./weights` and have it
175
+ # work no matter which cwd `reactor-runtime run` is invoked from.
176
+ # ~ expansion happens lazily inside get_weights_path().
177
+ # ---------------------------------------------------------
178
+ if reactor_config.weights_path:
179
+ resolved = reactor_config.weights_path
180
+ if not resolved.startswith("~") and not Path(resolved).is_absolute():
181
+ resolved = str(model_path / resolved)
182
+ _set_configured_weights_path(resolved)
183
+
160
184
  # Handle --config-help
161
185
  if self.args.config_help:
162
186
  if reactor_config.config:
@@ -179,8 +203,11 @@ class RunCommand:
179
203
  runtime_parser = config_class.parser()
180
204
 
181
205
  if self.args.help:
206
+ # Show the standalone command name (`reactor-runtime run`) since
207
+ # `--help` is what partners not using the Go CLI most often hit;
208
+ # the Go `reactor run` passthrough delegates here transparently.
182
209
  print(
183
- "Usage: reactor run [--path DIR] [--runtime NAME] [-v/--verbose] [--debug] [runtime options]"
210
+ "Usage: reactor-runtime run [--path DIR] [--runtime NAME] [-v/--verbose] [--debug] [runtime options]"
184
211
  )
185
212
  print("\nBase options:")
186
213
  print(" --path, -p DIR Path to model directory (default: cwd)")
@@ -96,14 +96,22 @@ class SchemaCommand:
96
96
  raw_yaml = OmegaConf.to_container(
97
97
  OmegaConf.load(str(reactor_yaml_path)), resolve=True
98
98
  )
99
- if not isinstance(raw_yaml, dict) or "model" not in raw_yaml:
99
+ if not isinstance(raw_yaml, dict):
100
100
  print(
101
- f"Error: {_REACTOR_YAML} must contain a 'model' field",
101
+ f"Error: {_REACTOR_YAML} must be a YAML mapping",
102
102
  file=sys.stderr,
103
103
  )
104
104
  sys.exit(1)
105
105
 
106
- reactor_config = ReactorConfig.from_dict(raw_yaml)
106
+ try:
107
+ reactor_config = ReactorConfig.from_dict(raw_yaml)
108
+ except KeyError:
109
+ print(
110
+ f"Error: {_REACTOR_YAML} must declare a model entrypoint via "
111
+ "'runtime.import' (or, for legacy configs, top-level 'model').",
112
+ file=sys.stderr,
113
+ )
114
+ sys.exit(1)
107
115
 
108
116
  add_import_paths([str(model_path)])
109
117
 
@@ -4,7 +4,7 @@ import sys
4
4
 
5
5
 
6
6
  def main():
7
- parser = argparse.ArgumentParser(prog="reactor")
7
+ parser = argparse.ArgumentParser(prog="reactor-runtime")
8
8
  subparsers = parser.add_subparsers(dest="command")
9
9
 
10
10
  from .commands import (
@@ -14,6 +14,7 @@ from reactor_runtime.interface import (
14
14
  MESSAGE_REGISTRY,
15
15
  FieldInfo,
16
16
  InputField,
17
+ InputFrame,
17
18
  Output,
18
19
  Input,
19
20
  Video,
@@ -27,6 +28,7 @@ from reactor_runtime.interface import (
27
28
  disconnected,
28
29
  )
29
30
  from reactor_runtime.profiling.singleton import get_profiler
31
+ from reactor_runtime.utils.paths import get_weights_path
30
32
 
31
33
  try:
32
34
  __version__ = version("reactor_runtime")
@@ -45,6 +47,7 @@ __all__ = [
45
47
  "MESSAGE_REGISTRY",
46
48
  "FieldInfo",
47
49
  "InputField",
50
+ "InputFrame",
48
51
  "Output",
49
52
  "Input",
50
53
  "Video",
@@ -57,5 +60,6 @@ __all__ = [
57
60
  "connected",
58
61
  "disconnected",
59
62
  "get_profiler",
63
+ "get_weights_path",
60
64
  "__version__",
61
65
  ]
@@ -0,0 +1,170 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """
3
+ Typed configuration parsed from ``reactor.yaml``.
4
+
5
+ :class:`ReactorConfig` is the runtime-level configuration that identifies
6
+ the model entry point and slug. Track topology is inferred from the
7
+ model class via the ``OUTPUT_REGISTRY`` / ``INPUT_REGISTRY``.
8
+
9
+ Emission settings (``fps``, ``buffer_size``) are declared as class
10
+ attributes on the model class, not in YAML. Model-specific parameters
11
+ (latent dims, learning rate, etc.) live in a separate config file
12
+ pointed to by ``ReactorConfig.config``.
13
+
14
+ Two YAML shapes are supported:
15
+
16
+ * Modern (richer ``reactor.yaml`` shared with the ``reactor`` CLI)::
17
+
18
+ model:
19
+ name: my-model # slug, optional
20
+ # ...other CLI-only fields are ignored here
21
+ runtime:
22
+ import: my_module:MyModel # entrypoint, required
23
+ config: config.yaml # optional model config
24
+ weights_path: ./weights # optional weights root override
25
+
26
+ * Legacy (top-level keys, kept for backwards compatibility)::
27
+
28
+ model: my_module:MyModel # entrypoint
29
+ name: my-model # optional slug
30
+ config: config.yaml # optional model config
31
+ weights_path: ./weights # optional weights root override
32
+
33
+ Loading a legacy file emits a one-shot deprecation warning pointing
34
+ partners at the modern shape and the ``reactor`` Go CLI.
35
+
36
+ The runtime only reads the few fields it needs; any additional keys —
37
+ in particular the rich ``model.*`` metadata used by the ``reactor`` CLI
38
+ (``display-name``, ``description``, ``gpu``, ``extra-args``, ...) —
39
+ are deliberately ignored.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from dataclasses import dataclass, field
45
+ from typing import Any, Dict, List, Optional
46
+
47
+ from reactor_runtime.utils.log import get_logger
48
+
49
+ _logger = get_logger(__name__)
50
+
51
+ # Multi-line WARNING surfaced once per process when ``from_dict`` is
52
+ # invoked on a legacy (top-level ``model``/``name``/``config``) reactor.yaml.
53
+ # The runtime keeps reading the legacy shape so partners aren't broken,
54
+ # but we want them to migrate to the richer shape — either by hand or by
55
+ # regenerating the file with the ``reactor`` Go CLI.
56
+ _LEGACY_DEPRECATION_MSG = (
57
+ "reactor.yaml is using the legacy top-level shape "
58
+ "(model: / name: / config:); this is DEPRECATED and will be removed "
59
+ "in a future release. Please migrate to the modern shape:\n"
60
+ " model:\n"
61
+ " name: my-model\n"
62
+ " runtime:\n"
63
+ " import: my_module:MyModel\n"
64
+ " config: config.yaml\n"
65
+ "The 'reactor' Go CLI generates this format automatically; install "
66
+ "with `brew install reactor-team/tools/reactor-cli` and run "
67
+ "`reactor init <name>` in a fresh workspace, or copy the new shape "
68
+ "by hand."
69
+ )
70
+
71
+ # Module-level guard so the warning fires once per process even if multiple
72
+ # reactor.yaml files are loaded (e.g. from tests). Reset by tests via
73
+ # ``_reset_legacy_warning_for_tests``.
74
+ _legacy_warned = False
75
+
76
+
77
+ def _reset_legacy_warning_for_tests() -> None:
78
+ """Test helper: clear the once-per-process warning latch."""
79
+ global _legacy_warned
80
+ _legacy_warned = False
81
+
82
+
83
+ @dataclass
84
+ class ReactorConfig:
85
+ """Parsed ``reactor.yaml``.
86
+
87
+ Attributes:
88
+ model: The model entrypoint spec, ``module:ClassName``. Sourced
89
+ from ``runtime.import`` (modern) or top-level ``model``
90
+ (legacy).
91
+ name: Model slug used on the wire (Redis streams, logs).
92
+ Sourced from ``model.name`` (modern) or top-level ``name``
93
+ (legacy). Empty string when not set.
94
+ config: Optional path to a model-specific YAML config. Sourced
95
+ from ``runtime.config`` (modern) or top-level ``config``
96
+ (legacy). The caller is responsible for resolving relative
97
+ paths to absolute before passing to :func:`build_model`.
98
+ config_overrides: CLI dotlist overrides
99
+ (e.g. ``["lr=0.001", "batch=4"]``) merged on top of the
100
+ loaded config file.
101
+ weights_path: Optional override for the weights root consumed by
102
+ :func:`reactor_runtime.get_weights_path`. Sourced from
103
+ ``runtime.weights_path`` (modern) or top-level ``weights_path``
104
+ (legacy). When set, takes precedence over the built-in default
105
+ but is itself overridden by ``$REACTOR_WEIGHTS_PATH``. The
106
+ runtime CLI resolves relative paths against the model root
107
+ before registering this with the helper, so partners can
108
+ commit ``runtime.weights_path: ./weights`` and have it work
109
+ from any cwd.
110
+ """
111
+
112
+ model: str
113
+ name: str = ""
114
+ config: Optional[str] = None
115
+ config_overrides: List[str] = field(default_factory=list)
116
+ weights_path: Optional[str] = None
117
+
118
+ @classmethod
119
+ def from_dict(cls, raw: Dict[str, Any]) -> "ReactorConfig":
120
+ """Parse a raw YAML dict into a :class:`ReactorConfig`.
121
+
122
+ Accepts both the modern (``model:`` / ``runtime:`` sections) and
123
+ legacy (top-level ``model`` / ``name`` / ``config``) shapes.
124
+ Detection is based on whether ``runtime`` is a mapping or
125
+ ``model`` is a mapping (legacy ``model`` is always a string).
126
+
127
+ Raises:
128
+ KeyError: when neither schema provides a model entrypoint.
129
+ """
130
+ runtime_section = raw.get("runtime")
131
+ model_section = raw.get("model")
132
+
133
+ modern = isinstance(runtime_section, dict) or isinstance(model_section, dict)
134
+
135
+ if modern:
136
+ runtime_section = (
137
+ runtime_section if isinstance(runtime_section, dict) else {}
138
+ )
139
+ model_section = model_section if isinstance(model_section, dict) else {}
140
+
141
+ entrypoint = runtime_section.get("import")
142
+ if not entrypoint:
143
+ raise KeyError(
144
+ "runtime.import is required in reactor.yaml "
145
+ "(e.g. 'runtime:\\n import: my_module:MyModel')"
146
+ )
147
+
148
+ slug = model_section.get("name") or ""
149
+
150
+ return cls(
151
+ model=str(entrypoint),
152
+ name=str(slug),
153
+ config=runtime_section.get("config"),
154
+ weights_path=runtime_section.get("weights_path") or None,
155
+ )
156
+
157
+ if "model" not in raw:
158
+ raise KeyError("model")
159
+
160
+ global _legacy_warned
161
+ if not _legacy_warned:
162
+ _legacy_warned = True
163
+ _logger.warning(_LEGACY_DEPRECATION_MSG)
164
+
165
+ return cls(
166
+ model=raw["model"],
167
+ name=raw.get("name", "") or "",
168
+ config=raw.get("config"),
169
+ weights_path=raw.get("weights_path") or None,
170
+ )
@@ -54,6 +54,10 @@ from reactor_runtime.interface.internal.input_buffer import (
54
54
  ReadMode,
55
55
  )
56
56
 
57
+ # Inbound frame with timing metadata (re-exported from transports so model
58
+ # authors can ``from reactor_runtime.interface import InputFrame``).
59
+ from reactor_runtime.transports.media import InputFrame
60
+
57
61
  __all__ = [
58
62
  # Tracks
59
63
  "Output",
@@ -95,4 +99,6 @@ __all__ = [
95
99
  "InputBuffer",
96
100
  "BufferClosed",
97
101
  "ReadMode",
102
+ # Inbound frame
103
+ "InputFrame",
98
104
  ]
@@ -6,7 +6,9 @@ from __future__ import annotations
6
6
  import asyncio
7
7
  import dataclasses
8
8
  import inspect
9
- from typing import Any, Dict, Iterator, List, Optional, Type
9
+ from typing import Any, Dict, Iterator, List, Optional, Type, Union
10
+
11
+ import numpy as np
10
12
 
11
13
  from reactor_runtime.interface.driver.step_result import StepResult
12
14
  from reactor_runtime.interface.events.messages import ModelMessage
@@ -17,6 +19,7 @@ from reactor_runtime.interface.pipeline.reactor_pipeline import (
17
19
  GeneratorEnded,
18
20
  ReactorPipeline,
19
21
  )
22
+ from reactor_runtime.transports.media import InputFrame
20
23
 
21
24
 
22
25
  class PipelineExecutor(Iterator[StepResult]):
@@ -166,14 +169,29 @@ class PipelineExecutor(Iterator[StepResult]):
166
169
  }
167
170
  return self._run(self._send_event(entry, **handler_kwargs))
168
171
 
169
- def push_media(self, track_name: str, data: Any) -> None:
172
+ def push_media(self, track_name: str, data: Union[np.ndarray, InputFrame]) -> None:
170
173
  """Push a media frame into an input buffer.
171
174
 
172
- The frame becomes available to the model on the next
173
- iteration when ``inference()`` calls ``try_read()`` or
174
- ``read()``.
175
+ Accepts either a raw ``np.ndarray`` (auto-wrapped into an
176
+ :class:`~reactor_runtime.transports.media.InputFrame` with
177
+ ``pts=None``) or a prebuilt :class:`InputFrame` for callers
178
+ that want to stamp the frame with an explicit presentation
179
+ timestamp.
180
+
181
+ The frame becomes available to the model on the next iteration
182
+ when ``inference()`` calls ``try_read()`` / ``read()`` — both
183
+ of which now return ``List[InputFrame]``.
175
184
  """
176
- self._model._push_media(track_name, data)
185
+ if isinstance(data, InputFrame):
186
+ frame = data
187
+ elif isinstance(data, np.ndarray):
188
+ frame = InputFrame(data=data)
189
+ else:
190
+ raise TypeError(
191
+ "push_media expects an np.ndarray or InputFrame, got "
192
+ f"{type(data).__name__}"
193
+ )
194
+ self._model._push_media(track_name, frame)
177
195
 
178
196
  def disconnect(self) -> List[ModelMessage]:
179
197
  """End the current session.
@@ -93,10 +93,20 @@ class ModelMessage:
93
93
 
94
94
  name: ClassVar[str]
95
95
  _field_descriptions: ClassVar[Dict[str, str]]
96
+ _user_doc: ClassVar[Optional[str]]
96
97
 
97
98
  def __init_subclass__(cls, **kwargs: object) -> None:
98
99
  super().__init_subclass__(**kwargs)
99
100
 
101
+ # Snapshot the user-supplied docstring before any `@dataclass`
102
+ # decoration runs. ``dataclasses.dataclass`` rewrites
103
+ # ``__doc__`` to an auto-generated signature like
104
+ # ``"Foo(x: int)"`` whenever the class body has no explicit
105
+ # docstring, which would leak meaningless noise into the
106
+ # schema description. Reading ``cls.__dict__`` (not
107
+ # ``cls.__doc__``) avoids inheriting the parent's docstring.
108
+ cls._user_doc = cls.__dict__.get("__doc__", None)
109
+
100
110
  descriptions: Dict[str, str] = {}
101
111
  has_default: list[str] = []
102
112
  no_default: list[str] = []