reactor-runtime 2.2.0__tar.gz → 2.3.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 (136) hide show
  1. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/PKG-INFO +2 -1
  2. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/pyproject.toml +2 -1
  3. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/__init__.py +2 -2
  4. reactor_runtime-2.3.0/src/reactor_cli/commands/schema.py +146 -0
  5. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/main.py +2 -2
  6. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/__init__.py +7 -2
  7. reactor_runtime-2.3.0/src/reactor_runtime/interface/defaults.py +229 -0
  8. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/pipeline_executor.py +1 -1
  9. reactor_runtime-2.3.0/src/reactor_runtime/interface/events/event.py +245 -0
  10. reactor_runtime-2.3.0/src/reactor_runtime/interface/events/messages.py +155 -0
  11. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/__init__.py +1 -4
  12. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/decorators.py +27 -42
  13. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/input_state.py +24 -3
  14. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +6 -11
  15. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtime_api.py +67 -67
  16. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/headless_runtime.py +19 -7
  17. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/http_runtime.py +11 -6
  18. reactor_runtime-2.3.0/src/reactor_runtime/schema.py +571 -0
  19. reactor_runtime-2.3.0/src/reactor_runtime/schema_validator.py +123 -0
  20. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/ice_connection.py +65 -32
  21. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/client.py +98 -10
  22. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +13 -1
  23. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +2 -0
  24. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +88 -38
  25. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +30 -0
  26. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +73 -12
  27. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/settings.py +50 -0
  28. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/messages.py +2 -0
  29. reactor_runtime-2.3.0/src/reactor_runtime/utils/typing.py +22 -0
  30. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/PKG-INFO +2 -1
  31. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/SOURCES.txt +5 -4
  32. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/requires.txt +1 -0
  33. reactor_runtime-2.2.0/src/reactor_cli/commands/capabilities.py +0 -76
  34. reactor_runtime-2.2.0/src/reactor_runtime/capabilities.py +0 -324
  35. reactor_runtime-2.2.0/src/reactor_runtime/interface/events/event.py +0 -57
  36. reactor_runtime-2.2.0/src/reactor_runtime/interface/events/messages.py +0 -68
  37. reactor_runtime-2.2.0/src/reactor_runtime/interface/model/input_fields.py +0 -123
  38. reactor_runtime-2.2.0/src/reactor_runtime/utils/schema.py +0 -25
  39. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/README.md +0 -0
  40. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/setup.cfg +0 -0
  41. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/api/__init__.py +0 -0
  42. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/init.py +0 -0
  43. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/run.py +0 -0
  44. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/__init__.py +0 -0
  45. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/config.py +0 -0
  46. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/runtime.py +0 -0
  47. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/version.py +0 -0
  48. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/__init__.py +0 -0
  49. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/config.py +0 -0
  50. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  51. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  52. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
  53. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/connected.py +0 -0
  54. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/upload.py +0 -0
  55. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  56. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  57. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  58. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  59. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/handlers.py +0 -0
  60. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  61. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  62. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  63. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  64. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  65. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
  66. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
  67. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/upload.py +0 -0
  68. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/model_state.py +0 -0
  69. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/__init__.py +0 -0
  70. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  71. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
  72. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
  73. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  74. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/helpers.py +0 -0
  75. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  76. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  77. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/profiler.py +0 -0
  78. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/singleton.py +0 -0
  79. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  80. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  81. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
  82. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/types.py +0 -0
  83. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/__init__.py +0 -0
  84. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  85. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  86. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  87. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  88. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  89. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/config.py +0 -0
  90. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/events.py +0 -0
  91. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  92. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  93. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  94. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  95. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  96. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  97. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  98. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  99. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  100. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  101. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  102. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  103. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  104. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  105. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  106. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  107. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  108. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  109. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  110. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  111. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  112. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  113. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  114. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  115. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  116. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  117. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  118. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  119. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  120. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
  121. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/interface.py +0 -0
  122. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/media.py +0 -0
  123. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/types.py +0 -0
  124. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/launch.py +0 -0
  125. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/loader.py +0 -0
  126. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/log.py +0 -0
  127. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  128. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/entry_points.txt +0 -0
  129. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
  130. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/README.md +0 -0
  131. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/__init__.py +0 -0
  132. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/config.yml +0 -0
  133. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/model.py +0 -0
  134. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/pipeline.py +0 -0
  135. {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/reactor.yaml +0 -0
  136. {reactor_runtime-2.2.0 → reactor_runtime-2.3.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.2.0
3
+ Version: 2.3.0
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -14,6 +14,7 @@ Requires-Dist: fastapi>=0.100.0
14
14
  Requires-Dist: uvicorn[standard]>=0.23.0
15
15
  Requires-Dist: aiohttp>=3.9.0
16
16
  Requires-Dist: redis
17
+ Requires-Dist: jsonschema>=4.20.0
17
18
  Requires-Dist: opentelemetry-api~=1.39
18
19
  Requires-Dist: opentelemetry-sdk~=1.39
19
20
  Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.39
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "uvicorn[standard]>=0.23.0",
23
23
  "aiohttp>=3.9.0",
24
24
  "redis",
25
+ "jsonschema>=4.20.0",
25
26
  "opentelemetry-api~=1.39",
26
27
  "opentelemetry-sdk~=1.39",
27
28
  "opentelemetry-exporter-otlp-proto-http~=1.39",
@@ -7,10 +7,10 @@ Each command is implemented as a class following the HuggingFace pattern.
7
7
 
8
8
  from .run import RunCommand
9
9
  from .init import InitCommand
10
- from .capabilities import CapabilitiesCommand
10
+ from .schema import SchemaCommand
11
11
 
12
12
  __all__ = [
13
13
  "RunCommand",
14
14
  "InitCommand",
15
- "CapabilitiesCommand",
15
+ "SchemaCommand",
16
16
  ]
@@ -0,0 +1,146 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """Schema command — prints or writes the model's OpenAPI 3.1 schema."""
3
+
4
+ import json
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from omegaconf import OmegaConf
10
+
11
+ from reactor_runtime.schema import ModelSchema
12
+ from reactor_runtime.config import ReactorConfig
13
+ from reactor_runtime.utils.loader import resolve_class
14
+ from reactor_runtime.utils.launch import add_import_paths
15
+
16
+ _REACTOR_YAML = "reactor.yaml"
17
+
18
+ _VERSION_PATTERN = re.compile(r"^v?\d+\.\d+\.\d+(-g[0-9a-f]+)?$")
19
+ # Describes valid --version input. Reused by both the --version argparse
20
+ # help text and the error message on pattern mismatch. Starts with a
21
+ # capitalised "Format:" so it reads naturally both standalone and after
22
+ # "Error: invalid --version 'bad': ".
23
+ _VERSION_FORMAT_HINT = (
24
+ "Format: 'X.Y.Z' or 'X.Y.Z-g<hex>', optionally with a 'v' prefix "
25
+ "(e.g. '2.2.1', 'v2.2.1', '2.2.1-gac767ec', 'v2.2.1-gac767ec'). "
26
+ "The 'v' prefix is optional on input — the emitted schema always "
27
+ "carries it."
28
+ )
29
+
30
+
31
+ class SchemaCommand:
32
+ @staticmethod
33
+ def register_subcommand(subparsers):
34
+ """Register schema command."""
35
+ run_parser = subparsers.add_parser(
36
+ "schema", help="Print or write the model's OpenAPI schema."
37
+ )
38
+ run_parser.add_argument(
39
+ "--path",
40
+ "-p",
41
+ type=str,
42
+ default=None,
43
+ help="Path to model directory (default: current directory)",
44
+ )
45
+ run_parser.add_argument(
46
+ "--version",
47
+ "-v",
48
+ type=str,
49
+ default=None,
50
+ help=(
51
+ "Override the schema version emitted in info.version. "
52
+ f"{_VERSION_FORMAT_HINT} Default: v0.0.0."
53
+ ),
54
+ )
55
+ run_parser.add_argument(
56
+ "--out",
57
+ "-o",
58
+ type=str,
59
+ default=None,
60
+ help=(
61
+ "Write the schema JSON to this path instead of printing to "
62
+ "stdout. Parent directories are created if missing."
63
+ ),
64
+ )
65
+ run_parser.set_defaults(func=SchemaCommand)
66
+
67
+ def __init__(self, args):
68
+ """Initialize with parsed arguments."""
69
+ self.args = args
70
+
71
+ def run(self):
72
+ """Emit the OpenAPI schema of a Reactor model.
73
+
74
+ Loads the model class (triggering auto-registration of tracks,
75
+ events, and messages in the global registries), builds the
76
+ schema from those registries, optionally overrides the version,
77
+ and either prints the JSON to stdout or writes it to ``--out``.
78
+ """
79
+ if self.args.version is not None and not _VERSION_PATTERN.match(
80
+ self.args.version
81
+ ):
82
+ print(
83
+ f"Error: invalid --version {self.args.version!r}: "
84
+ f"{_VERSION_FORMAT_HINT}",
85
+ file=sys.stderr,
86
+ )
87
+ sys.exit(1)
88
+
89
+ model_path = Path(self.args.path).resolve() if self.args.path else Path.cwd()
90
+
91
+ reactor_yaml_path = model_path / _REACTOR_YAML
92
+ if not reactor_yaml_path.exists():
93
+ print(f"Error: {_REACTOR_YAML} not found in {model_path}", file=sys.stderr)
94
+ sys.exit(1)
95
+
96
+ raw_yaml = OmegaConf.to_container(
97
+ OmegaConf.load(str(reactor_yaml_path)), resolve=True
98
+ )
99
+ if not isinstance(raw_yaml, dict) or "model" not in raw_yaml:
100
+ print(
101
+ f"Error: {_REACTOR_YAML} must contain a 'model' field",
102
+ file=sys.stderr,
103
+ )
104
+ sys.exit(1)
105
+
106
+ reactor_config = ReactorConfig.from_dict(raw_yaml)
107
+
108
+ add_import_paths([str(model_path)])
109
+
110
+ try:
111
+ model_cls = resolve_class(reactor_config.model)
112
+ except Exception as e:
113
+ print(f"Error loading model class: {e}", file=sys.stderr)
114
+ sys.exit(1)
115
+
116
+ try:
117
+ schema = ModelSchema.from_config(reactor_config, model_cls=model_cls)
118
+ except Exception as e:
119
+ print(f"Error extracting schema: {e}", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ if self.args.version is not None:
123
+ # Honour the ModelSchema.version invariant: the internal value
124
+ # always carries a 'v' prefix, matching the release-tag format
125
+ # stored in the Coordinator's schema registry. to_openapi()
126
+ # emits it as-is; to_legacy_tracks_only() is the only place
127
+ # that strips it for the legacy wire protocol_version field.
128
+ # See ModelSchema's class docstring.
129
+ v = self.args.version
130
+ schema.version = v if v.startswith("v") else f"v{v}"
131
+
132
+ openapi_doc = schema.to_openapi()
133
+ model_name = reactor_config.name or reactor_config.model
134
+
135
+ if self.args.out is not None:
136
+ out_path = Path(self.args.out).expanduser().resolve()
137
+ out_path.parent.mkdir(parents=True, exist_ok=True)
138
+ out_path.write_text(json.dumps(openapi_doc, indent=4))
139
+ emitted_version = openapi_doc["info"]["version"]
140
+ print(
141
+ f"Model: {model_name} — schema ({emitted_version}) written to "
142
+ f"{out_path}"
143
+ )
144
+ else:
145
+ print(f"Model: {model_name}")
146
+ print(json.dumps(openapi_doc, indent=4))
@@ -10,12 +10,12 @@ def main():
10
10
  from .commands import (
11
11
  RunCommand,
12
12
  InitCommand,
13
- CapabilitiesCommand,
13
+ SchemaCommand,
14
14
  )
15
15
 
16
16
  RunCommand.register_subcommand(subparsers)
17
17
  InitCommand.register_subcommand(subparsers)
18
- CapabilitiesCommand.register_subcommand(subparsers)
18
+ SchemaCommand.register_subcommand(subparsers)
19
19
 
20
20
  args, remaining = parser.parse_known_args()
21
21
  if hasattr(args, "func"):
@@ -21,7 +21,11 @@ from reactor_runtime.interface.events.event import EVENT_REGISTRY, Event
21
21
  from reactor_runtime.interface.events.connected import Connected, Disconnected
22
22
  from reactor_runtime.interface.events.upload import FileUploaded
23
23
  from reactor_runtime.interface.upload import UploadedFile
24
- from reactor_runtime.interface.events.messages import MESSAGE_REGISTRY, ModelMessage
24
+ from reactor_runtime.interface.events.messages import (
25
+ MESSAGE_REGISTRY,
26
+ MessageField,
27
+ ModelMessage,
28
+ )
25
29
 
26
30
  # Model layer (canonical) — includes decorators and field metadata
27
31
  from reactor_runtime.interface.model.reactor_model import ReactorModel
@@ -31,7 +35,7 @@ from reactor_runtime.interface.model.decorators import (
31
35
  event,
32
36
  file_uploaded,
33
37
  )
34
- from reactor_runtime.interface.model.input_fields import FieldInfo, InputField
38
+ from reactor_runtime.interface.defaults import FieldInfo, InputField
35
39
 
36
40
  # Pipeline — generator + typed state layer
37
41
  from reactor_runtime.interface.pipeline.reactor_pipeline import ReactorPipeline
@@ -67,6 +71,7 @@ __all__ = [
67
71
  "FileUploaded",
68
72
  "UploadedFile",
69
73
  "EVENT_REGISTRY",
74
+ "MessageField",
70
75
  "ModelMessage",
71
76
  "MESSAGE_REGISTRY",
72
77
  # Model — decorators & fields
@@ -0,0 +1,229 @@
1
+ # Copyright (c) 2026 Reactor Technologies, Inc. All rights reserved.
2
+ """
3
+ Field metadata and default-value validation primitives.
4
+
5
+ Single home for the types and helpers that both :class:`Event` /
6
+ :class:`ModelMessage` base classes and their user-facing surfaces
7
+ (:func:`~reactor_runtime.interface.model.decorators.event`,
8
+ :class:`~reactor_runtime.interface.pipeline.input_state.InputState`,
9
+ :class:`~reactor_runtime.interface.events.messages.MessageField`)
10
+ share to validate user-supplied defaults.
11
+
12
+ Exports :class:`FieldInfo`, :func:`InputField`, :func:`validate_field`,
13
+ :func:`raise_if_default_invalid`, :func:`raise_if_default_not_static`,
14
+ and :func:`field_info_to_json_schema`. Re-exported from
15
+ ``reactor_runtime.interface`` and ``reactor_runtime`` as part of the
16
+ public API.
17
+
18
+ Lives one level above ``interface.model`` so the base classes in
19
+ ``interface.events`` can consume it without cycling back through
20
+ ``interface.model.__init__``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Any, Dict, List, Optional, Tuple, Union
27
+
28
+ _NO_DEFAULT = object()
29
+
30
+ _MUTABLE_DEFAULT_TYPES = (list, dict, set)
31
+
32
+
33
+ # ---------------------------------------------------------------------
34
+ # FieldInfo + InputField — the user-facing declaration of a field's
35
+ # default value and validation constraints.
36
+ # ---------------------------------------------------------------------
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class FieldInfo:
41
+ """Validation constraints for a single input field.
42
+
43
+ Create instances via :func:`InputField` rather than directly.
44
+ When no default is provided, ``default`` is :data:`_NO_DEFAULT`.
45
+ """
46
+
47
+ default: Any = _NO_DEFAULT
48
+ description: Optional[str] = None
49
+ ge: Optional[Union[int, float]] = None
50
+ le: Optional[Union[int, float]] = None
51
+ min_length: Optional[int] = None
52
+ max_length: Optional[int] = None
53
+ choices: Optional[List[Any]] = None
54
+
55
+
56
+ def InputField(
57
+ default: Any = _NO_DEFAULT,
58
+ *,
59
+ default_factory: Any = None,
60
+ description: Optional[str] = None,
61
+ ge: Optional[Union[int, float]] = None,
62
+ le: Optional[Union[int, float]] = None,
63
+ min_length: Optional[int] = None,
64
+ max_length: Optional[int] = None,
65
+ choices: Optional[List[Any]] = None,
66
+ ) -> Any:
67
+ """Declare a default value and validation constraints for a field.
68
+
69
+ Use as the default value for ``@event`` handler parameters or
70
+ :class:`~reactor_runtime.interface.pipeline.input_state.InputState`
71
+ fields. Values that violate constraints are rejected before
72
+ reaching the handler (logged at debug level, the event is dropped).
73
+
74
+ Args:
75
+ default: Default value for the field.
76
+ default_factory: Not supported; raises ``TypeError``. Defaults
77
+ must be statically representable — use a literal ``default=...``.
78
+ description: Human-readable description (exposed in capabilities).
79
+ ge: Minimum allowed value (inclusive).
80
+ le: Maximum allowed value (inclusive).
81
+ min_length: Minimum length for string/sequence values.
82
+ max_length: Maximum length for string/sequence values.
83
+ choices: Exhaustive list of allowed values.
84
+ """
85
+ if default_factory is not None:
86
+ raise TypeError(
87
+ "InputField(default_factory=...) is not supported. "
88
+ "Defaults must be statically representable — use a literal `default=...`."
89
+ )
90
+ return FieldInfo(
91
+ default=default,
92
+ description=description,
93
+ ge=ge,
94
+ le=le,
95
+ min_length=min_length,
96
+ max_length=max_length,
97
+ choices=choices,
98
+ )
99
+
100
+
101
+ # ---------------------------------------------------------------------
102
+ # Static-default check — rejects mutable containers, the one guardrail
103
+ # every entry point funnels through.
104
+ # ---------------------------------------------------------------------
105
+
106
+
107
+ def raise_if_default_not_static(owner: str, field_name: str, default: Any) -> None:
108
+ """Raise ``TypeError`` when *default* is a mutable container.
109
+
110
+ Defaults must be statically representable. Mutable containers
111
+ (``list`` / ``dict`` / ``set``) would be shared across every
112
+ instance and produce surprising cross-session state leaks — the
113
+ same reason Python's ``dataclasses`` forbids ``field(default=[])``.
114
+ Users who need a per-instance container should use ``default=None``
115
+ and initialise inside the handler, or an immutable alternative like
116
+ ``tuple`` / ``frozenset``.
117
+
118
+ ``None`` is the canonical "unset" sentinel for Optional fields and
119
+ is always allowed.
120
+ """
121
+ if default is None:
122
+ return
123
+ if isinstance(default, _MUTABLE_DEFAULT_TYPES):
124
+ raise TypeError(
125
+ f"{owner}: default for '{field_name}' is a mutable "
126
+ f"{type(default).__name__} which can't be used as a static default. "
127
+ "Defaults must be statically representable — use `default=None` and "
128
+ "initialise inside the handler, or an immutable alternative "
129
+ "(tuple, frozenset)."
130
+ )
131
+
132
+
133
+ # ---------------------------------------------------------------------
134
+ # FieldInfo-aware validation — constraints + static-default check.
135
+ # ---------------------------------------------------------------------
136
+
137
+
138
+ def validate_field(name: str, value: Any, info: FieldInfo) -> Tuple[bool, str]:
139
+ """Validate *value* against its :class:`FieldInfo` constraints.
140
+
141
+ Returns ``(True, "")`` if valid, ``(False, reason)`` if not.
142
+ """
143
+ if info.choices is not None and value not in info.choices:
144
+ return False, f"{name}: {value!r} not in choices {info.choices}"
145
+
146
+ if info.ge is not None:
147
+ try:
148
+ if value < info.ge:
149
+ return False, f"{name}: {value} < ge({info.ge})"
150
+ except TypeError:
151
+ return False, f"{name}: {value!r} is not comparable to ge({info.ge})"
152
+
153
+ if info.le is not None:
154
+ try:
155
+ if value > info.le:
156
+ return False, f"{name}: {value} > le({info.le})"
157
+ except TypeError:
158
+ return False, f"{name}: {value!r} is not comparable to le({info.le})"
159
+
160
+ if info.min_length is not None:
161
+ try:
162
+ if len(value) < info.min_length:
163
+ return (
164
+ False,
165
+ f"{name}: length {len(value)} < min_length({info.min_length})",
166
+ )
167
+ except TypeError:
168
+ return (
169
+ False,
170
+ f"{name}: {value!r} has no length for min_length({info.min_length})",
171
+ )
172
+
173
+ if info.max_length is not None:
174
+ try:
175
+ if len(value) > info.max_length:
176
+ return (
177
+ False,
178
+ f"{name}: length {len(value)} > max_length({info.max_length})",
179
+ )
180
+ except TypeError:
181
+ return (
182
+ False,
183
+ f"{name}: {value!r} has no length for max_length({info.max_length})",
184
+ )
185
+
186
+ return True, ""
187
+
188
+
189
+ def raise_if_default_invalid(
190
+ owner: str, field_name: str, default: Any, info: FieldInfo
191
+ ) -> None:
192
+ """Raise ``TypeError`` when *default* is unusable for *field_name*.
193
+
194
+ Single source of truth for decoration-time / class-creation-time
195
+ checks on ``InputField`` defaults. Runs two checks in order:
196
+
197
+ 1. The default must be statically representable (no mutable
198
+ containers) — delegated to :func:`raise_if_default_not_static`.
199
+ 2. The default must satisfy its own ``info`` constraints
200
+ (``choices`` / ``ge`` / ``le`` / ``min_length`` / ``max_length``).
201
+
202
+ Bypasses both checks for the ``_NO_DEFAULT`` sentinel and for
203
+ explicit ``None`` (the "unset" value for Optional fields).
204
+ """
205
+ if default is _NO_DEFAULT or default is None:
206
+ return
207
+ raise_if_default_not_static(owner, field_name, default)
208
+ ok, reason = validate_field(field_name, default, info)
209
+ if not ok:
210
+ raise TypeError(
211
+ f"{owner}: default value for '{field_name}' violates "
212
+ f"its own InputField constraints ({reason})."
213
+ )
214
+
215
+
216
+ def field_info_to_json_schema(schema: Dict[str, Any], info: FieldInfo) -> None:
217
+ """Merge :class:`FieldInfo` constraints into a JSON Schema dict."""
218
+ if info.description:
219
+ schema["description"] = info.description
220
+ if info.ge is not None:
221
+ schema["minimum"] = info.ge
222
+ if info.le is not None:
223
+ schema["maximum"] = info.le
224
+ if info.min_length is not None:
225
+ schema["minLength"] = info.min_length
226
+ if info.max_length is not None:
227
+ schema["maxLength"] = info.max_length
228
+ if info.choices is not None:
229
+ schema["enum"] = info.choices
@@ -10,7 +10,7 @@ from typing import Any, Dict, Iterator, List, Optional, Type
10
10
  from reactor_runtime.interface.driver.step_result import StepResult
11
11
  from reactor_runtime.interface.events.messages import ModelMessage
12
12
  from reactor_runtime.interface.model.handlers import Handlers, build_dispatch_table
13
- from reactor_runtime.interface.model.input_fields import validate_field
13
+ from reactor_runtime.interface.defaults import validate_field
14
14
  from reactor_runtime.interface.pipeline.idle import Idle
15
15
  from reactor_runtime.interface.pipeline.reactor_pipeline import (
16
16
  GeneratorEnded,