@@ -21,95 +21,71 @@ def _get_config(self) -> Dict[str, Any]:
21
21
return self .config_manager .get_rest_config ()
22
22
23
23
def __init__ (self , config_manager : ConfigManager ):
24
- """Initialize REST adapter with configuration.
25
-
26
- Args:
27
- config_manager: Configuration manager instance
28
- """
24
+ """Initialize REST adapter with configuration."""
29
25
super ().__init__ (config_manager )
30
- self .app = Quart (__name__ )
31
- self ._setup_routes ()
32
- self .server = None
33
26
self ._active_streams = {} # Store active streams by client_id
34
- # Main event loop
35
27
self ._loop : Optional [asyncio .AbstractEventLoop ] = None
36
28
self ._running = False
29
+ self .app = self ._create_app ()
37
30
logger .debug ("REST - Adapter initialized with config: host=%s, port=%s" ,
38
31
self .config ['host' ], self .config ['port' ])
39
32
40
- def _setup_routes (self ) -> None :
41
- """Set up the streaming endpoint."""
42
- self .app .post (self .config ['endpoint' ])(self ._handle_streaming_message )
43
-
44
- async def _handle_streaming_message (self ) -> Response :
45
- """Handle incoming messages with streaming response.
33
+ def _create_app (self ) -> Quart :
34
+ """Factory method to create and configure the Quart app."""
35
+ app = Quart ("simulation_rest_adapter" )
46
36
47
- Returns:
48
- Response: Streaming response with simulation results
49
- """
50
- content_type = request .headers .get ('content-type' , '' )
51
- body = await request .get_data ()
37
+ @app .post (self .config ['endpoint' ])
38
+ async def handle_streaming_message () -> Response :
39
+ content_type = request .headers .get ('content-type' , '' )
40
+ body = await request .get_data ()
52
41
53
- try :
54
- message = self ._parse_message (body , content_type )
55
- except Exception as e :
56
- logger .error ("REST - Error parsing message: %s" , e )
57
- return Response (
58
- response = json .dumps ({"error" : str (e )}),
59
- status = 400 ,
60
- content_type = 'application/json'
42
+ try :
43
+ message = self ._parse_message (body , content_type )
44
+ except Exception as e :
45
+ logger .error ("REST - Error parsing message: %s" , e )
46
+ return Response (
47
+ response = json .dumps ({"error" : str (e )}),
48
+ status = 400 ,
49
+ content_type = 'application/json'
50
+ )
51
+
52
+ if not isinstance (message , dict ):
53
+ return Response (
54
+ response = json .dumps ({"error" : "Message is not a dictionary" }),
55
+ status = 400 ,
56
+ content_type = 'application/json'
57
+ )
58
+
59
+ simulation = message .get ('simulation' , {})
60
+ producer = simulation .get ('client_id' , 'unknown' )
61
+ consumer = simulation .get ('simulator' , 'unknown' )
62
+
63
+ message ['bridge_meta' ] = {
64
+ 'protocol' : 'rest' ,
65
+ 'producer' : producer ,
66
+ 'consumer' : consumer
67
+ }
68
+
69
+ signal ('message_received_input_rest' ).send (
70
+ message = message ,
71
+ producer = producer ,
72
+ consumer = consumer ,
73
+ protocol = 'rest'
61
74
)
62
75
63
- if not isinstance (message , dict ):
64
- logger .error ("REST - Message is not a dictionary" )
76
+ queue = asyncio .Queue ()
77
+ self ._active_streams [producer ] = queue
78
+
65
79
return Response (
66
- response = json . dumps ({ "error" : "Message is not a dictionary" } ),
67
- status = 400 ,
68
- content_type = 'application/json'
80
+ self . _generate_response ( producer , queue ),
81
+ content_type = 'application/x-ndjson' ,
82
+ status = 200
69
83
)
70
84
71
- simulation = message .get ('simulation' , {})
72
- producer = simulation .get ('client_id' , 'unknown' )
73
- consumer = simulation .get ('simulator' , 'unknown' )
74
-
75
- # Add bridge metadata
76
- message ['bridge_meta' ] = {
77
- 'protocol' : 'rest' ,
78
- 'producer' : producer ,
79
- 'consumer' : consumer
80
- }
81
-
82
- logger .debug (
83
- "REST - Processing message from producer: %s, simulator: %s" ,
84
- producer , consumer )
85
- # Use SignalManager to send the signal
86
- signal ('message_received_input_rest' ).send (
87
- message = message ,
88
- producer = producer ,
89
- consumer = consumer ,
90
- protocol = 'rest'
91
- )
92
-
93
- # Create a queue for this client's messages
94
- queue = asyncio .Queue ()
95
- self ._active_streams [producer ] = queue
96
-
97
- return Response (
98
- self ._generate_response (producer , queue ),
99
- content_type = 'application/x-ndjson' ,
100
- status = 200
101
- )
85
+ return app
102
86
103
87
def _parse_message (self , body : bytes , content_type : str ) -> Dict [str , Any ]:
104
- """Parse message body based on content type.
105
-
106
- Args:
107
- body: Raw message body
108
- content_type: Content type header
109
-
110
- Returns:
111
- Dict[str, Any]: Parsed message
112
- """
88
+ """Parse message body based on content type."""
113
89
if 'yaml' in content_type :
114
90
logger .debug ("REST - Attempting to parse message as YAML" )
115
91
return yaml .safe_load (body )
@@ -119,13 +95,11 @@ def _parse_message(self, body: bytes, content_type: str) -> Dict[str, Any]:
119
95
120
96
# Fallback: try YAML, then JSON, then raw text
121
97
try :
122
- logger .debug (
123
- "REST - Attempting to parse message as YAML (fallback)" )
98
+ logger .debug ("REST - Attempting to parse message as YAML (fallback)" )
124
99
return yaml .safe_load (body )
125
100
except Exception :
126
101
try :
127
- logger .debug (
128
- "REST - Attempting to parse message as JSON (fallback)" )
102
+ logger .debug ("REST - Attempting to parse message as JSON (fallback)" )
129
103
return json .loads (body )
130
104
except Exception :
131
105
logger .debug ("REST - Parsing as raw text (fallback)" )
@@ -135,20 +109,11 @@ def _parse_message(self, body: bytes, content_type: str) -> Dict[str, Any]:
135
109
}
136
110
137
111
async def _generate_response (
138
- self , producer : str , queue : asyncio .Queue ) -> AsyncGenerator [str , None ]:
139
- """Generate streaming response.
140
-
141
- Args:
142
- producer: Client ID
143
- queue: Message queue for this client
144
-
145
- Yields:
146
- str: JSON-encoded messages
147
- """
112
+ self , producer : str , queue : asyncio .Queue
113
+ ) -> AsyncGenerator [str , None ]:
114
+ """Generate streaming response."""
148
115
try :
149
- # Send initial acknowledgment
150
116
yield json .dumps ({"status" : "processing" }) + "\n "
151
- # Keep the connection open and wait for results
152
117
while True :
153
118
try :
154
119
result = await asyncio .wait_for (queue .get (), timeout = 600 )
@@ -161,45 +126,36 @@ async def _generate_response(
161
126
yield json .dumps ({"status" : "error" , "error" : str (e )}) + "\n "
162
127
break
163
128
finally :
164
- # Clean up when the stream ends
165
- if producer in self ._active_streams :
166
- del self ._active_streams [producer ]
129
+ self ._active_streams .pop (producer , None )
167
130
168
131
async def send_result (self , producer : str , result : Dict [str , Any ]) -> None :
169
- """Send a result message to a specific client.
170
-
171
- Args:
172
- producer: Client ID
173
- result: Result message to send
174
- """
132
+ """Send a result message to a specific client."""
175
133
if producer in self ._active_streams :
176
134
await self ._active_streams [producer ].put (result )
177
135
else :
178
- logger .warning (
179
- "REST - No active stream found for producer: %s" , producer )
136
+ logger .warning ("REST - No active stream found for producer: %s" , producer )
180
137
181
138
async def _start_server (self ) -> None :
182
139
"""Start the Hypercorn server."""
183
- self ._loop = asyncio .get_running_loop () # Save main event loop
184
-
140
+ self ._loop = asyncio .get_running_loop ()
185
141
config = HyperConfig ()
186
- config .errorlog = logger # Use the main logger for error logs
187
- config .accesslog = logger # Use the main logger for access logs
188
- config .bind = ["%s:%s" % ( self .config ['host' ], self .config ['port' ]) ]
142
+ config .errorlog = logger
143
+ config .accesslog = logger
144
+ config .bind = [f" { self .config ['host' ]} : { self .config ['port' ]} " ]
189
145
config .use_reloader = False
190
146
config .worker_class = "asyncio"
191
147
config .alpn_protocols = ["h2" , "http/1.1" ]
192
148
193
149
if self .config .get ('certfile' ) and self .config .get ('keyfile' ):
194
150
config .certfile = self .config ['certfile' ]
195
151
config .keyfile = self .config ['keyfile' ]
152
+
196
153
await serve (self .app , config )
197
154
198
155
def start (self ) -> None :
199
156
"""Start the REST server."""
200
- logger .debug (
201
- "REST - Starting adapter on %s:%s" ,
202
- self .config ['host' ], self .config ['port' ])
157
+ logger .debug ("REST - Starting adapter on %s:%s" ,
158
+ self .config ['host' ], self .config ['port' ])
203
159
try :
204
160
asyncio .run (self ._start_server ())
205
161
self ._running = True
@@ -208,28 +164,18 @@ def start(self) -> None:
208
164
raise
209
165
210
166
def send_result_sync (self , producer : str , result : Dict [str , Any ]) -> None :
211
- """Synchronous wrapper for sending result messages.
212
-
213
- Args:
214
- producer: Client ID
215
- result: Result message to send
216
- """
167
+ """Synchronous wrapper for sending result messages."""
217
168
if producer not in self ._active_streams :
218
- logger .warning (
219
- "REST - No active stream found for producer: %s. "
220
- "Available streams: %s" ,
221
- producer , list (self ._active_streams .keys ())
222
- )
169
+ logger .warning ("REST - No active stream found for producer: %s. Available streams: %s" ,
170
+ producer , list (self ._active_streams .keys ()))
223
171
return
224
172
225
173
if self ._loop and self ._loop .is_running ():
226
- # Use run_coroutine_threadsafe to execute coroutine in main loop
227
174
future = asyncio .run_coroutine_threadsafe (
228
175
self .send_result (producer , result ),
229
176
self ._loop
230
177
)
231
178
try :
232
- # Optional: wait for result with short timeout
233
179
future .result (timeout = 5 )
234
180
except Exception as e :
235
181
logger .error ("REST - Error sending result: %s" , e )
@@ -240,32 +186,17 @@ def stop(self) -> None:
240
186
"""Stop the REST server."""
241
187
logger .debug ("REST - Stopping adapter" )
242
188
self ._running = False
243
- if self .server :
244
- self .server .close ()
245
189
246
190
def _handle_message (self , message : Dict [str , Any ]) -> None :
247
- """Handle incoming messages (required by ProtocolAdapter).
248
-
249
- Args:
250
- message: Message to handle
251
- """
252
- # For REST, this is handled by the Quart endpoint
191
+ """(Not used in REST; handled via route)."""
253
192
pass
254
193
255
- def publish_result_message_rest (self , sender , ** kwargs ): # pylint: disable=unused-argument
256
- """
257
- Publish result message via REST adapter.
258
-
259
- Args:
260
- message: Message payload to send
261
- destination: REST endpoint destination
262
- """
194
+ def publish_result_message_rest (self , sender , ** kwargs ):
195
+ """Publish result message via REST adapter."""
263
196
try :
264
197
message = kwargs .get ('message' , {})
265
198
destination = message .get ('destinations' , [])[0 ]
266
199
self .send_result_sync (destination , message )
267
- logger .debug (
268
- "Successfully scheduled result message for REST client: %s" ,
269
- destination )
200
+ logger .debug ("Successfully scheduled result message for REST client: %s" , destination )
270
201
except (ConnectionError , TimeoutError ) as e :
271
202
logger .error ("Error sending result message to REST client: %s" , e )
0 commit comments