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.
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/PKG-INFO +2 -1
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/pyproject.toml +2 -1
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/__init__.py +2 -2
- reactor_runtime-2.3.0/src/reactor_cli/commands/schema.py +146 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/main.py +2 -2
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/__init__.py +7 -2
- reactor_runtime-2.3.0/src/reactor_runtime/interface/defaults.py +229 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/pipeline_executor.py +1 -1
- reactor_runtime-2.3.0/src/reactor_runtime/interface/events/event.py +245 -0
- reactor_runtime-2.3.0/src/reactor_runtime/interface/events/messages.py +155 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/__init__.py +1 -4
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/decorators.py +27 -42
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/input_state.py +24 -3
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +6 -11
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtime_api.py +67 -67
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/headless_runtime.py +19 -7
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/http_runtime.py +11 -6
- reactor_runtime-2.3.0/src/reactor_runtime/schema.py +571 -0
- reactor_runtime-2.3.0/src/reactor_runtime/schema_validator.py +123 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/ice_connection.py +65 -32
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/client.py +98 -10
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +13 -1
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +2 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +88 -38
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +30 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/video.py +73 -12
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/settings.py +50 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/messages.py +2 -0
- reactor_runtime-2.3.0/src/reactor_runtime/utils/typing.py +22 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/PKG-INFO +2 -1
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/SOURCES.txt +5 -4
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/requires.txt +1 -0
- reactor_runtime-2.2.0/src/reactor_cli/commands/capabilities.py +0 -76
- reactor_runtime-2.2.0/src/reactor_runtime/capabilities.py +0 -324
- reactor_runtime-2.2.0/src/reactor_runtime/interface/events/event.py +0 -57
- reactor_runtime-2.2.0/src/reactor_runtime/interface/events/messages.py +0 -68
- reactor_runtime-2.2.0/src/reactor_runtime/interface/model/input_fields.py +0 -123
- reactor_runtime-2.2.0/src/reactor_runtime/utils/schema.py +0 -25
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/README.md +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/setup.cfg +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/api/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/init.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/commands/run.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/config.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/runtime.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_cli/utils/version.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/config.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/driver/step_result.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/connected.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/events/upload.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/handlers.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/interface/upload.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/model_state.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/base.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/file.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/helpers.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/profiler.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/profiling/singleton.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/config.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/config.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/runtimes/http/types.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/client.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/config.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/events.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/ice_uris.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/interface.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/media.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/transports/types.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/launch.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/loader.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime/utils/log.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/entry_points.txt +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/reactor_runtime.egg-info/top_level.txt +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/README.md +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/__init__.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/config.yml +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/model.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/pipeline.py +0 -0
- {reactor_runtime-2.2.0 → reactor_runtime-2.3.0}/src/template/reactor.yaml +0 -0
- {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.
|
|
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.
|
|
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 .
|
|
10
|
+
from .schema import SchemaCommand
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
13
|
"RunCommand",
|
|
14
14
|
"InitCommand",
|
|
15
|
-
"
|
|
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
|
-
|
|
13
|
+
SchemaCommand,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
RunCommand.register_subcommand(subparsers)
|
|
17
17
|
InitCommand.register_subcommand(subparsers)
|
|
18
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|