reactor-runtime 2.0.2__tar.gz → 2.2.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 (130) hide show
  1. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/PKG-INFO +2 -1
  2. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/pyproject.toml +3 -2
  3. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/capabilities.py +3 -1
  4. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/__init__.py +17 -1
  5. reactor_runtime-2.2.0/src/reactor_runtime/interface/driver/__init__.py +7 -0
  6. reactor_runtime-2.2.0/src/reactor_runtime/interface/driver/pipeline_executor.py +297 -0
  7. reactor_runtime-2.2.0/src/reactor_runtime/interface/driver/step_result.py +30 -0
  8. reactor_runtime-2.2.0/src/reactor_runtime/interface/events/upload.py +21 -0
  9. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/model/decorators.py +40 -1
  10. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/model/handlers.py +24 -3
  11. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/model/reactor_model.py +9 -0
  12. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/pipeline/input_state.py +24 -3
  13. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +91 -23
  14. reactor_runtime-2.2.0/src/reactor_runtime/interface/upload.py +26 -0
  15. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtime_api.py +118 -2
  16. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/headless/headless_runtime.py +4 -0
  17. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/http/http_runtime.py +78 -2
  18. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/http/types.py +13 -0
  19. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +8 -8
  20. reactor_runtime-2.2.0/src/reactor_runtime/transports/gstreamer/settings.py +207 -0
  21. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/utils/messages.py +1 -0
  22. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/PKG-INFO +2 -1
  23. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/SOURCES.txt +5 -0
  24. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/requires.txt +1 -0
  25. reactor_runtime-2.0.2/src/reactor_runtime/transports/gstreamer/settings.py +0 -62
  26. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/README.md +0 -0
  27. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/setup.cfg +0 -0
  28. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/api/__init__.py +0 -0
  29. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/commands/__init__.py +0 -0
  30. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/commands/capabilities.py +0 -0
  31. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/commands/init.py +0 -0
  32. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/commands/run.py +0 -0
  33. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/main.py +0 -0
  34. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/utils/__init__.py +0 -0
  35. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/utils/config.py +0 -0
  36. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/utils/runtime.py +0 -0
  37. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_cli/utils/version.py +0 -0
  38. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/__init__.py +0 -0
  39. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/config.py +0 -0
  40. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
  41. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/events/connected.py +0 -0
  42. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/events/event.py +0 -0
  43. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/events/messages.py +0 -0
  44. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  45. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  46. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  47. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  48. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
  49. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/model/input_fields.py +0 -0
  50. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  51. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  52. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  53. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  54. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
  55. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  56. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/model_state.py +0 -0
  57. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/__init__.py +0 -0
  58. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  59. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
  60. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
  61. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  62. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/helpers.py +0 -0
  63. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  64. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  65. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/profiler.py +0 -0
  66. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/profiling/singleton.py +0 -0
  67. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  68. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  69. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
  70. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/__init__.py +0 -0
  71. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  72. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  73. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  74. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  75. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  76. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  77. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/config.py +0 -0
  78. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/events.py +0 -0
  79. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  80. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
  81. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  82. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  83. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  84. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  85. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  86. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  87. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  88. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  89. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  90. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  91. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  92. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  93. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  94. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  95. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  96. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  97. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  98. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  99. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  100. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  101. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  102. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  103. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  104. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  105. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  106. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  107. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  108. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  109. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  110. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  111. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  112. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  113. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
  114. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/interface.py +0 -0
  115. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/media.py +0 -0
  116. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/transports/types.py +0 -0
  117. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/utils/launch.py +0 -0
  118. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/utils/loader.py +0 -0
  119. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/utils/log.py +0 -0
  120. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime/utils/schema.py +0 -0
  121. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  122. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/entry_points.txt +0 -0
  123. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
  124. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/README.md +0 -0
  125. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/__init__.py +0 -0
  126. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/config.yml +0 -0
  127. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/model.py +0 -0
  128. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/pipeline.py +0 -0
  129. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/reactor.yaml +0 -0
  130. {reactor_runtime-2.0.2 → reactor_runtime-2.2.0}/src/template/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -12,6 +12,7 @@ Requires-Dist: av>=14.0.0
12
12
  Requires-Dist: aiortc>=1.14.0
13
13
  Requires-Dist: fastapi>=0.100.0
14
14
  Requires-Dist: uvicorn[standard]>=0.23.0
15
+ Requires-Dist: aiohttp>=3.9.0
15
16
  Requires-Dist: redis
16
17
  Requires-Dist: opentelemetry-api~=1.39
17
18
  Requires-Dist: opentelemetry-sdk~=1.39
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.0.2"
7
+ version = "2.2.0"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "aiortc>=1.14.0",
21
21
  "fastapi>=0.100.0",
22
22
  "uvicorn[standard]>=0.23.0",
23
+ "aiohttp>=3.9.0",
23
24
  "redis",
24
25
  "opentelemetry-api~=1.39",
25
26
  "opentelemetry-sdk~=1.39",
@@ -45,4 +46,4 @@ template = ["*.yaml", "*.yml", "*.txt", "*.md"]
45
46
  # Proto dependency from reactor-team/reactor-proto
46
47
  # Release tag format: golang/v{version}
47
48
  # Wheel name format: reactor_proto-{version_without_hash}-py3-none-any.whl
48
- version = "1.1.0-g20f00e2"
49
+ version = "1.4.1-g5660eed"
@@ -24,15 +24,17 @@ from reactor_runtime.interface.model.input_fields import FieldInfo
24
24
  from reactor_runtime.interface.tracks.descriptors import Audio
25
25
  from reactor_runtime.interface.tracks.input import INPUT_REGISTRY
26
26
  from reactor_runtime.interface.tracks.output import OUTPUT_REGISTRY
27
+ from reactor_runtime.interface.upload import UploadedFile
27
28
 
28
29
 
29
- CAPABILITIES_VERSION = "1.1"
30
+ CAPABILITIES_VERSION = "1.2"
30
31
 
31
32
  _TYPE_MAP = {
32
33
  int: "integer",
33
34
  float: "number",
34
35
  str: "string",
35
36
  bool: "boolean",
37
+ UploadedFile: "file",
36
38
  }
37
39
 
38
40
 
@@ -19,11 +19,18 @@ from reactor_runtime.interface.tracks.output import (
19
19
  # Events & Messages
20
20
  from reactor_runtime.interface.events.event import EVENT_REGISTRY, Event
21
21
  from reactor_runtime.interface.events.connected import Connected, Disconnected
22
+ from reactor_runtime.interface.events.upload import FileUploaded
23
+ from reactor_runtime.interface.upload import UploadedFile
22
24
  from reactor_runtime.interface.events.messages import MESSAGE_REGISTRY, ModelMessage
23
25
 
24
26
  # Model layer (canonical) — includes decorators and field metadata
25
27
  from reactor_runtime.interface.model.reactor_model import ReactorModel
26
- from reactor_runtime.interface.model.decorators import connected, disconnected, event
28
+ from reactor_runtime.interface.model.decorators import (
29
+ connected,
30
+ disconnected,
31
+ event,
32
+ file_uploaded,
33
+ )
27
34
  from reactor_runtime.interface.model.input_fields import FieldInfo, InputField
28
35
 
29
36
  # Pipeline — generator + typed state layer
@@ -31,6 +38,9 @@ from reactor_runtime.interface.pipeline.reactor_pipeline import ReactorPipeline
31
38
  from reactor_runtime.interface.pipeline.input_state import InputState
32
39
  from reactor_runtime.interface.pipeline.idle import Idle
33
40
 
41
+ # Driver — programmatic step-by-step control
42
+ from reactor_runtime.interface.driver import PipelineExecutor, StepResult
43
+
34
44
  # Internal (exposed for runtime integration, not stable API)
35
45
  from reactor_runtime.interface.internal.reactor_core import ReactorCore
36
46
  from reactor_runtime.interface.internal.output_buffer import OutputBuffer
@@ -54,6 +64,8 @@ __all__ = [
54
64
  "Event",
55
65
  "Connected",
56
66
  "Disconnected",
67
+ "FileUploaded",
68
+ "UploadedFile",
57
69
  "EVENT_REGISTRY",
58
70
  "ModelMessage",
59
71
  "MESSAGE_REGISTRY",
@@ -62,12 +74,16 @@ __all__ = [
62
74
  "event",
63
75
  "connected",
64
76
  "disconnected",
77
+ "file_uploaded",
65
78
  "FieldInfo",
66
79
  "InputField",
67
80
  # Pipeline
68
81
  "ReactorPipeline",
69
82
  "InputState",
70
83
  "Idle",
84
+ # Driver
85
+ "PipelineExecutor",
86
+ "StepResult",
71
87
  # Internal
72
88
  "ReactorCore",
73
89
  "OutputBuffer",
@@ -0,0 +1,7 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """Programmatic pipeline executor — step-by-step control without a server."""
3
+
4
+ from reactor_runtime.interface.driver.pipeline_executor import PipelineExecutor
5
+ from reactor_runtime.interface.driver.step_result import StepResult
6
+
7
+ __all__ = ["PipelineExecutor", "StepResult"]
@@ -0,0 +1,297 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """PipelineExecutor — programmatic step-by-step control of a ReactorPipeline."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import inspect
8
+ from typing import Any, Dict, Iterator, List, Optional, Type
9
+
10
+ from reactor_runtime.interface.driver.step_result import StepResult
11
+ from reactor_runtime.interface.events.messages import ModelMessage
12
+ from reactor_runtime.interface.model.handlers import Handlers, build_dispatch_table
13
+ from reactor_runtime.interface.model.input_fields import validate_field
14
+ from reactor_runtime.interface.pipeline.idle import Idle
15
+ from reactor_runtime.interface.pipeline.reactor_pipeline import (
16
+ GeneratorEnded,
17
+ ReactorPipeline,
18
+ )
19
+
20
+
21
+ class PipelineExecutor(Iterator[StepResult]):
22
+ """Execute a :class:`ReactorPipeline` step-by-step from synchronous code.
23
+
24
+ Wraps any ``ReactorPipeline`` subclass and exposes a clean
25
+ sequential API: ``connect()``, iteration via ``next()`` / ``for``,
26
+ ``send_event()``, ``push_media()``, ``disconnect()``.
27
+ No server, no transport, no background threads.
28
+
29
+ Implements ``Iterator[StepResult]`` — each call to ``next(exe)``
30
+ advances the inference generator by one ``yield``. Iteration
31
+ stops with ``StopIteration`` when no session is active.
32
+
33
+ Each action returns the :class:`ModelMessage` instances the model
34
+ sent via ``self.send()`` during that action, enabling deterministic
35
+ assertions in tests and structured output capture in scripts.
36
+
37
+ Example::
38
+
39
+ with PipelineExecutor(EchoPipeline) as exe:
40
+ exe.connect()
41
+ exe.push_media("webcam", frame)
42
+ for r in exe:
43
+ if r.output is not Idle:
44
+ break
45
+ msgs = exe.send_event("set_effect", effect="sepia")
46
+ exe.disconnect()
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ pipeline_cls: Type[ReactorPipeline],
52
+ config: Optional[Dict[str, Any]] = None,
53
+ ) -> None:
54
+ # Instantiate the pipeline and run its one-time load().
55
+ self._model: ReactorPipeline = pipeline_cls()
56
+ self._model.load(config or {})
57
+
58
+ # Build the handler dispatch table from @event / @connected /
59
+ # @disconnected decorators discovered on the pipeline class.
60
+ self._handlers: Handlers = self._model._get_handlers()
61
+ self._dispatch_table = build_dispatch_table(self._handlers)
62
+
63
+ # Private event loop — _advance() and some handlers are async,
64
+ # so we need a loop. Everything still runs on the caller's
65
+ # thread; there are no background threads.
66
+ self._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()
67
+ self._loop.run_until_complete(self._init())
68
+
69
+ # Track whether inference() is sync or async so _advance()
70
+ # uses the right iteration protocol (next vs __anext__).
71
+ self._gen = None
72
+ self._is_async_gen: bool = inspect.isasyncgenfunction(self._model.inference)
73
+ self._model._async_inference = self._is_async_gen
74
+
75
+ # Message capture — override send() to collect ModelMessage
76
+ # instances instead of serializing them over transport.
77
+ # _run() clears this list before every action so that
78
+ # messages are scoped to the action that produced them.
79
+ self._captured: List[ModelMessage] = []
80
+
81
+ async def _capturing_send(message: ModelMessage) -> None:
82
+ self._captured.append(message)
83
+
84
+ self._model.send = _capturing_send # type: ignore[assignment]
85
+
86
+ async def _init(self) -> None:
87
+ self._model._init_async()
88
+
89
+ # ------------------------------------------------------------------
90
+ # Public API
91
+ # ------------------------------------------------------------------
92
+
93
+ def connect(self) -> List[ModelMessage]:
94
+ """Begin a session.
95
+
96
+ Creates a fresh :class:`InputState`, starts the ``inference()``
97
+ generator, sets the ``connected`` signal, and fires the
98
+ ``@connected`` handler if defined.
99
+
100
+ Returns:
101
+ Messages the model sent during the ``@connected`` handler.
102
+ """
103
+ if self._gen is not None:
104
+ raise RuntimeError(
105
+ "connect() called on an already-connected executor. "
106
+ "Call disconnect() first."
107
+ )
108
+ return self._run(self._connect())
109
+
110
+ def __iter__(self) -> Iterator[StepResult]:
111
+ """Return self — PipelineExecutor is its own iterator."""
112
+ return self
113
+
114
+ def __next__(self) -> StepResult:
115
+ """Advance the inference generator by one ``yield``.
116
+
117
+ Returns:
118
+ A :class:`StepResult` with the output, compute time, and
119
+ any messages sent during this step.
120
+
121
+ Raises:
122
+ StopIteration: No active session (before ``connect()``
123
+ or after ``disconnect()``).
124
+ """
125
+ if self._gen is None:
126
+ raise StopIteration
127
+ return self._run(self._step())
128
+
129
+ def send_event(self, name: str, **kwargs: Any) -> List[ModelMessage]:
130
+ """Deliver a named event to the model.
131
+
132
+ Validates strictly before calling the handler:
133
+
134
+ 1. ``ValueError`` if the event name is unknown.
135
+ 2. ``TypeError`` if the kwargs don't match the event dataclass.
136
+ 3. ``ValueError`` if an ``InputField`` constraint is violated.
137
+
138
+ Returns:
139
+ Messages the model sent during the event handler.
140
+ """
141
+ entry = self._handlers.events.get(name)
142
+ if entry is None:
143
+ available = sorted(self._handlers.events.keys())
144
+ raise ValueError(f"Unknown event {name!r}. Available events: {available}")
145
+
146
+ try:
147
+ entry.event_cls(**kwargs)
148
+ except TypeError as exc:
149
+ raise TypeError(f"Invalid arguments for event {name!r}: {exc}") from None
150
+
151
+ field_infos: Dict = getattr(entry.event_cls, "_field_infos", {})
152
+ for field_name, info in field_infos.items():
153
+ if field_name in kwargs:
154
+ ok, reason = validate_field(field_name, kwargs[field_name], info)
155
+ if not ok:
156
+ raise ValueError(f"Validation failed for event {name!r}: {reason}")
157
+
158
+ return self._run(self._send_event(entry, **kwargs))
159
+
160
+ def push_media(self, track_name: str, data: Any) -> None:
161
+ """Push a media frame into an input buffer.
162
+
163
+ The frame becomes available to the model on the next
164
+ iteration when ``inference()`` calls ``try_read()`` or
165
+ ``read()``.
166
+ """
167
+ self._model._push_media(track_name, data)
168
+
169
+ def disconnect(self) -> List[ModelMessage]:
170
+ """End the current session.
171
+
172
+ Clears the ``connected`` signal, fires the ``@disconnected``
173
+ handler, closes the generator, and resets state and buffers.
174
+
175
+ Returns:
176
+ Messages the model sent during the ``@disconnected`` handler.
177
+ """
178
+ if self._gen is None:
179
+ raise RuntimeError(
180
+ "disconnect() called without an active session. Call connect() first."
181
+ )
182
+ return self._run(self._disconnect())
183
+
184
+ @property
185
+ def state(self):
186
+ """The current session state, or ``None`` if no session is active."""
187
+ return self._model.state
188
+
189
+ def close(self) -> None:
190
+ """Shut down the executor.
191
+
192
+ Disconnects if a session is active and closes the event loop.
193
+ """
194
+ try:
195
+ if self._gen is not None:
196
+ self._run(self._disconnect())
197
+ finally:
198
+ if not self._loop.is_closed():
199
+ self._loop.close()
200
+
201
+ def __enter__(self):
202
+ return self
203
+
204
+ def __exit__(self, *exc: Any) -> None:
205
+ self.close()
206
+
207
+ # ------------------------------------------------------------------
208
+ # Async implementations
209
+ #
210
+ # Each method below is the async core of a public API method.
211
+ # The public wrappers delegate to these via _run(), which clears
212
+ # captured messages and runs the coroutine synchronously with
213
+ # run_until_complete().
214
+ # ------------------------------------------------------------------
215
+
216
+ async def _connect(self) -> List[ModelMessage]:
217
+ """Create a fresh InputState, start inference(), fire @connected."""
218
+ self._model.state = self._model._state_cls()
219
+ self._gen = self._model.inference()
220
+ self._model.connected.set()
221
+ await self._fire_handler(
222
+ self._handlers.connected,
223
+ self._handlers.connected_is_async,
224
+ )
225
+ return list(self._captured)
226
+
227
+ async def _step(self) -> StepResult:
228
+ """Advance the inference generator by one ``yield``.
229
+
230
+ On ``GeneratorEnded`` the generator is restarted automatically
231
+ (same behavior as the production run-loop) and ``Idle`` is
232
+ returned for that tick.
233
+ """
234
+ try:
235
+ output, compute_time = await self._model._advance(
236
+ self._gen, self._is_async_gen
237
+ )
238
+ except GeneratorEnded:
239
+ self._gen = self._model.inference()
240
+ return StepResult(
241
+ output=Idle, compute_time=0.0, messages=list(self._captured)
242
+ )
243
+ return StepResult(
244
+ output=output,
245
+ compute_time=compute_time,
246
+ messages=list(self._captured),
247
+ )
248
+
249
+ async def _send_event(self, entry: Any, **kwargs: Any) -> List[ModelMessage]:
250
+ """Dispatch a validated event to its handler function."""
251
+ await self._fire_handler(entry.handler, entry.is_async, **kwargs)
252
+ return list(self._captured)
253
+
254
+ async def _disconnect(self) -> List[ModelMessage]:
255
+ """Clear connected, fire @disconnected, close generator, reset state."""
256
+ self._model.connected.clear()
257
+ await self._fire_handler(
258
+ self._handlers.disconnected,
259
+ self._handlers.disconnected_is_async,
260
+ )
261
+ if self._gen is not None:
262
+ if self._is_async_gen:
263
+ await self._gen.aclose()
264
+ else:
265
+ self._gen.close()
266
+ self._gen = None
267
+ self._model.state = None
268
+ for buf in self._model._input_buffers.values():
269
+ buf.reset()
270
+ return list(self._captured)
271
+
272
+ # ------------------------------------------------------------------
273
+ # Infrastructure
274
+ # ------------------------------------------------------------------
275
+
276
+ async def _fire_handler(
277
+ self,
278
+ handler: Any,
279
+ is_async: bool,
280
+ **kwargs: Any,
281
+ ) -> None:
282
+ """Call a lifecycle or event handler, dispatching sync/async."""
283
+ if handler is None:
284
+ return
285
+ if is_async:
286
+ await handler(self._model, **kwargs)
287
+ else:
288
+ handler(self._model, **kwargs)
289
+
290
+ def _run(self, coro: Any) -> Any:
291
+ """Clear captured messages then run *coro* synchronously.
292
+
293
+ The clear-before-run pattern scopes messages: each public
294
+ action sees only the messages produced during that action.
295
+ """
296
+ self._captured.clear()
297
+ return self._loop.run_until_complete(coro)
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """StepResult — return type of ``next(PipelineExecutor)``."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import List, Union
8
+
9
+ from reactor_runtime.interface.events.messages import ModelMessage
10
+ from reactor_runtime.interface.pipeline.idle import _IdleType
11
+ from reactor_runtime.interface.tracks.output import Output
12
+
13
+
14
+ @dataclass
15
+ class StepResult:
16
+ """Result of a single inference step.
17
+
18
+ Attributes:
19
+ output: The resolved :class:`Output` instance that the
20
+ generator yielded, or :data:`Idle` when it yielded
21
+ ``Idle`` or ``None``.
22
+ compute_time: Wall-clock seconds spent inside the generator
23
+ for this step (excludes transport and emission overhead).
24
+ messages: :class:`ModelMessage` instances the model sent via
25
+ ``self.send()`` during this step.
26
+ """
27
+
28
+ output: Union[Output, _IdleType]
29
+ compute_time: float
30
+ messages: List[ModelMessage] = field(default_factory=list)
@@ -0,0 +1,21 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """Built-in file upload lifecycle event — :class:`FileUploaded`."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from reactor_runtime.interface.events.event import Event
9
+ from reactor_runtime.interface.upload import UploadedFile
10
+
11
+
12
+ @dataclass
13
+ class FileUploaded(Event, internal=True): # type: ignore[call-arg]
14
+ """Fired when an uploaded file has been downloaded and is ready.
15
+
16
+ Handle with the ``@file_uploaded`` decorator on your model method.
17
+ This is a lifecycle event pushed by the runtime — clients cannot
18
+ trigger it via the data channel.
19
+ """
20
+
21
+ file: UploadedFile
@@ -1,6 +1,6 @@
1
1
  # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
2
  """
3
- Model decorators — ``@connected``, ``@disconnected``, and ``@event``.
3
+ Model decorators — ``@connected``, ``@disconnected``, ``@file_uploaded``, and ``@event``.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -12,9 +12,11 @@ from typing import Any, Callable, List, Tuple, Type, Union, get_type_hints
12
12
 
13
13
  from reactor_runtime.interface.events.event import Event, _snake_to_pascal
14
14
  from reactor_runtime.interface.model.input_fields import FieldInfo, _NO_DEFAULT
15
+ from reactor_runtime.interface.upload import UploadedFile
15
16
 
16
17
  _CONNECTED_ATTR = "__reactor_connected__"
17
18
  _DISCONNECTED_ATTR = "__reactor_disconnected__"
19
+ _FILE_UPLOADED_ATTR = "__reactor_file_uploaded__"
18
20
  _EVENT_HANDLER_ATTR = "__reactor_event_meta__"
19
21
  _IS_ASYNC_ATTR = "__reactor_is_async__"
20
22
 
@@ -59,6 +61,36 @@ def disconnected(func: Callable) -> Callable:
59
61
  return func
60
62
 
61
63
 
64
+ def file_uploaded(func: Callable) -> Callable:
65
+ """Mark a method as the handler called when a file is uploaded.
66
+
67
+ Called each time the client uploads a file to the runtime.
68
+ Only one ``@file_uploaded`` handler is allowed per model class.
69
+ The handler receives a single ``uploaded_file`` keyword argument
70
+ of type :class:`UploadedFile`. The handler may be ``async def``
71
+ or a plain ``def``.
72
+
73
+ Example::
74
+
75
+ @file_uploaded
76
+ async def on_file(self, uploaded_file: UploadedFile):
77
+ if uploaded_file.mime_type.startswith("image/"):
78
+ pil_image = Image.open(io.BytesIO(uploaded_file.data))
79
+ """
80
+ if not callable(func):
81
+ raise TypeError(f"@file_uploaded expected a callable, got {type(func)}")
82
+ sig = inspect.signature(func)
83
+ params = [p for p in sig.parameters if p != "self"]
84
+ if params != ["uploaded_file"]:
85
+ raise TypeError(
86
+ f"@file_uploaded handler must accept exactly one parameter named "
87
+ f"'uploaded_file', got: {params}"
88
+ )
89
+ setattr(func, _FILE_UPLOADED_ATTR, True)
90
+ setattr(func, _IS_ASYNC_ATTR, inspect.iscoroutinefunction(func))
91
+ return func
92
+
93
+
62
94
  def event(*, name: str, description: str = "", dedupe: bool = False):
63
95
  """Mark a method as a named event handler.
64
96
 
@@ -143,12 +175,16 @@ def _make_event_from_method(func: Callable, event_name: str) -> Type[Event]:
143
175
 
144
176
  fields: list = []
145
177
  field_infos: dict[str, FieldInfo] = {}
178
+ upload_fields: set[str] = set()
146
179
 
147
180
  for param_name, param in sig.parameters.items():
148
181
  if param_name == "self":
149
182
  continue
150
183
  param_type = hints.get(param_name, Any)
151
184
 
185
+ if param_type is UploadedFile:
186
+ upload_fields.add(param_name)
187
+
152
188
  if param.default != inspect.Parameter.empty:
153
189
  raw_default = param.default
154
190
  if isinstance(raw_default, FieldInfo):
@@ -175,4 +211,7 @@ def _make_event_from_method(func: Callable, event_name: str) -> Type[Event]:
175
211
  if field_infos:
176
212
  event_cls._field_infos = field_infos # type: ignore[attr-defined]
177
213
 
214
+ if upload_fields:
215
+ event_cls._upload_fields = upload_fields # type: ignore[attr-defined]
216
+
178
217
  return event_cls
@@ -2,9 +2,9 @@
2
2
  """
3
3
  Handler collection and dispatch table construction.
4
4
 
5
- Scans a model class's MRO for ``@connected``, ``@disconnected``, and
6
- ``@event`` decorated methods and builds a dispatch table used by
7
- :class:`ReactorModel`.
5
+ Scans a model class's MRO for ``@connected``, ``@disconnected``,
6
+ ``@file_uploaded``, and ``@event`` decorated methods and builds a
7
+ dispatch table used by :class:`ReactorModel`.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -16,6 +16,7 @@ from reactor_runtime.interface.model.decorators import (
16
16
  _CONNECTED_ATTR,
17
17
  _DISCONNECTED_ATTR,
18
18
  _EVENT_HANDLER_ATTR,
19
+ _FILE_UPLOADED_ATTR,
19
20
  _IS_ASYNC_ATTR,
20
21
  )
21
22
 
@@ -48,6 +49,8 @@ class Handlers:
48
49
  "connected_is_async",
49
50
  "disconnected",
50
51
  "disconnected_is_async",
52
+ "file_uploaded",
53
+ "file_uploaded_is_async",
51
54
  "events",
52
55
  )
53
56
 
@@ -56,6 +59,8 @@ class Handlers:
56
59
  self.connected_is_async: bool = False
57
60
  self.disconnected: Optional[Callable] = None
58
61
  self.disconnected_is_async: bool = False
62
+ self.file_uploaded: Optional[Callable] = None
63
+ self.file_uploaded_is_async: bool = False
59
64
  self.events: Dict[str, EventEntry] = {}
60
65
 
61
66
 
@@ -78,6 +83,7 @@ def collect_handlers(cls: type, skip_classes: tuple = ()) -> Handlers:
78
83
 
79
84
  connected_in_class: List[Callable] = []
80
85
  disconnected_in_class: List[Callable] = []
86
+ file_uploaded_in_class: List[Callable] = []
81
87
 
82
88
  for _attr_name, attr_value in vars(klass).items():
83
89
  if not callable(attr_value):
@@ -89,6 +95,9 @@ def collect_handlers(cls: type, skip_classes: tuple = ()) -> Handlers:
89
95
  if getattr(attr_value, _DISCONNECTED_ATTR, False):
90
96
  disconnected_in_class.append(attr_value)
91
97
 
98
+ if getattr(attr_value, _FILE_UPLOADED_ATTR, False):
99
+ file_uploaded_in_class.append(attr_value)
100
+
92
101
  meta = getattr(attr_value, _EVENT_HANDLER_ATTR, None)
93
102
  if meta is not None and meta["name"] not in seen_events:
94
103
  handlers.events[meta["name"]] = EventEntry(
@@ -117,6 +126,11 @@ def collect_handlers(cls: type, skip_classes: tuple = ()) -> Handlers:
117
126
  f"Multiple @disconnected handlers in {klass.__qualname__}: "
118
127
  f"{[f.__qualname__ for f in disconnected_in_class]}"
119
128
  )
129
+ if len(file_uploaded_in_class) > 1:
130
+ raise TypeError(
131
+ f"Multiple @file_uploaded handlers in {klass.__qualname__}: "
132
+ f"{[f.__qualname__ for f in file_uploaded_in_class]}"
133
+ )
120
134
 
121
135
  if connected_in_class and handlers.connected is None:
122
136
  handlers.connected = connected_in_class[0]
@@ -132,6 +146,13 @@ def collect_handlers(cls: type, skip_classes: tuple = ()) -> Handlers:
132
146
  _IS_ASYNC_ATTR,
133
147
  False,
134
148
  )
149
+ if file_uploaded_in_class and handlers.file_uploaded is None:
150
+ handlers.file_uploaded = file_uploaded_in_class[0]
151
+ handlers.file_uploaded_is_async = getattr(
152
+ file_uploaded_in_class[0],
153
+ _IS_ASYNC_ATTR,
154
+ False,
155
+ )
135
156
 
136
157
  return handlers
137
158
 
@@ -16,6 +16,7 @@ from typing import Any, Callable, List, Tuple, Type
16
16
 
17
17
  from reactor_runtime.interface.events.connected import Connected, Disconnected
18
18
  from reactor_runtime.interface.events.event import Event
19
+ from reactor_runtime.interface.events.upload import FileUploaded
19
20
  from reactor_runtime.interface.model.handlers import (
20
21
  Handlers,
21
22
  build_dispatch_table,
@@ -156,6 +157,14 @@ class ReactorModel(ReactorCore):
156
157
  handlers.disconnected_is_async,
157
158
  )
158
159
 
160
+ elif isinstance(event, FileUploaded):
161
+ if handlers.file_uploaded is not None:
162
+ await self._invoke(
163
+ handlers.file_uploaded,
164
+ handlers.file_uploaded_is_async,
165
+ uploaded_file=event.file,
166
+ )
167
+
159
168
  else:
160
169
  await self._dispatch_event(event, dispatch_table)
161
170