nost-tools 2.0.1__tar.gz → 2.0.2__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.
Potentially problematic release.
This version of nost-tools might be problematic. Click here for more details.
- {nost_tools-2.0.1 → nost_tools-2.0.2}/PKG-INFO +1 -1
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/__init__.py +1 -1
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/application.py +124 -23
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/schemas.py +33 -7
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools.egg-info/PKG-INFO +1 -1
- {nost_tools-2.0.1 → nost_tools-2.0.2}/LICENSE +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/README.md +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/application_utils.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/configuration.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/entity.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/errors.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/logger_application.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/managed_application.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/manager.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/observer.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/publisher.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools/simulator.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools.egg-info/SOURCES.txt +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools.egg-info/dependency_links.txt +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools.egg-info/requires.txt +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/nost_tools.egg-info/top_level.txt +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/pyproject.toml +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/setup.cfg +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/tests/test_entity.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/tests/test_observer.py +0 -0
- {nost_tools-2.0.1 → nost_tools-2.0.2}/tests/test_simulator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nost_tools
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Tools for Novel Observing Strategies Testbed (NOS-T) Applications
|
|
5
5
|
Author-email: "Paul T. Grogan" <paul.grogan@asu.edu>, "Emmanuel M. Gonzalez" <emmanuelgonzalez@asu.edu>
|
|
6
6
|
License: BSD License
|
|
@@ -83,6 +83,7 @@ class Application:
|
|
|
83
83
|
self.refresh_token = None
|
|
84
84
|
self._token_refresh_thread = None
|
|
85
85
|
self.token_refresh_interval = None
|
|
86
|
+
self._reconnect_delay = None
|
|
86
87
|
|
|
87
88
|
def ready(self) -> None:
|
|
88
89
|
"""
|
|
@@ -158,14 +159,12 @@ class Application:
|
|
|
158
159
|
def start_token_refresh_thread(self):
|
|
159
160
|
"""
|
|
160
161
|
Starts a background thread to refresh the access token periodically.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
config (:obj:`ConnectionConfig`): connection configuration
|
|
164
162
|
"""
|
|
165
163
|
logger.debug("Starting refresh token thread.")
|
|
166
164
|
|
|
167
165
|
def refresh_token_periodically():
|
|
168
166
|
while not self._should_stop.wait(timeout=self.token_refresh_interval):
|
|
167
|
+
logger.debug("Token refresh thread is running.")
|
|
169
168
|
try:
|
|
170
169
|
access_token, refresh_token = self.new_access_token(
|
|
171
170
|
self.refresh_token
|
|
@@ -283,16 +282,17 @@ class Application:
|
|
|
283
282
|
# Set SSL options
|
|
284
283
|
parameters.ssl_options = pika.SSLOptions(context)
|
|
285
284
|
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
self.
|
|
290
|
-
|
|
285
|
+
# Save connection parameters for reconnection
|
|
286
|
+
self._connection_parameters = parameters
|
|
287
|
+
self._reconnect_delay = (
|
|
288
|
+
self.config.rc.server_configuration.servers.rabbitmq.reconnect_delay
|
|
289
|
+
)
|
|
290
|
+
logger.info(f"Reconnect delay: {self._reconnect_delay}")
|
|
291
291
|
|
|
292
292
|
# Establish non-blocking connection to RabbitMQ
|
|
293
293
|
self.connection = pika.SelectConnection(
|
|
294
294
|
parameters=parameters,
|
|
295
|
-
on_open_callback=on_connection_open,
|
|
295
|
+
on_open_callback=self.on_connection_open,
|
|
296
296
|
on_open_error_callback=self.on_connection_error,
|
|
297
297
|
on_close_callback=self.on_connection_closed,
|
|
298
298
|
)
|
|
@@ -327,11 +327,16 @@ class Application:
|
|
|
327
327
|
|
|
328
328
|
def _start_io_loop(self):
|
|
329
329
|
"""
|
|
330
|
-
Starts the I/O loop
|
|
330
|
+
Starts the I/O loop in a separate thread. This allows the application to
|
|
331
|
+
run in the background while still being able to process messages from RabbitMQ.
|
|
331
332
|
"""
|
|
332
333
|
self.stop_event = threading.Event()
|
|
333
334
|
while not self.stop_event.is_set():
|
|
334
|
-
|
|
335
|
+
try:
|
|
336
|
+
self.connection.ioloop.start()
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.error(f"I/O loop error: {e}")
|
|
339
|
+
break
|
|
335
340
|
|
|
336
341
|
def on_channel_open(self, channel):
|
|
337
342
|
"""
|
|
@@ -341,9 +346,46 @@ class Application:
|
|
|
341
346
|
channel (:obj:`pika.channel.Channel`): channel object
|
|
342
347
|
"""
|
|
343
348
|
self.channel = channel
|
|
349
|
+
self.add_on_channel_close_callback()
|
|
350
|
+
|
|
344
351
|
# Signal that connection is established
|
|
345
352
|
self._is_connected.set()
|
|
346
353
|
|
|
354
|
+
def add_on_channel_close_callback(self):
|
|
355
|
+
"""This method tells pika to call the on_channel_closed method if
|
|
356
|
+
RabbitMQ unexpectedly closes the channel.
|
|
357
|
+
"""
|
|
358
|
+
logger.info("Adding channel close callback")
|
|
359
|
+
self.channel.add_on_close_callback(self.on_channel_closed)
|
|
360
|
+
|
|
361
|
+
def on_channel_closed(self, channel, reason):
|
|
362
|
+
"""
|
|
363
|
+
Invoked by pika when RabbitMQ unexpectedly closes the channel.
|
|
364
|
+
Channels are usually closed if you attempt to do something that
|
|
365
|
+
violates the protocol, such as re-declare an exchange or queue with
|
|
366
|
+
different parameters. In this case, we'll close the connection
|
|
367
|
+
to shutdown the object.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
channel (:obj:`pika.channel.Channel`): channel object
|
|
371
|
+
reason (Exception): exception representing reason for loss of connection
|
|
372
|
+
"""
|
|
373
|
+
logger.warning(f"Channel {channel} was closed: {reason}")
|
|
374
|
+
self.close_connection()
|
|
375
|
+
|
|
376
|
+
def close_connection(self):
|
|
377
|
+
"""
|
|
378
|
+
This method is invoked by pika when the connection to RabbitMQ is
|
|
379
|
+
closed. This method is called when the application is shutting down
|
|
380
|
+
or when the connection is closed unexpectedly.
|
|
381
|
+
"""
|
|
382
|
+
self._consuming = False
|
|
383
|
+
if self.connection.is_closing or self.connection.is_closed:
|
|
384
|
+
logger.info("Connection is closing or already closed")
|
|
385
|
+
else:
|
|
386
|
+
logger.info("Closing connection")
|
|
387
|
+
self.connection.close()
|
|
388
|
+
|
|
347
389
|
def on_connection_error(self, connection, error):
|
|
348
390
|
"""
|
|
349
391
|
Callback function for when a connection error occurs.
|
|
@@ -362,18 +404,61 @@ class Application:
|
|
|
362
404
|
RabbitMQ if it disconnects.
|
|
363
405
|
|
|
364
406
|
Args:
|
|
365
|
-
connection (:obj:`pika.connection.Connection`):
|
|
407
|
+
connection (:obj:`pika.connection.Connection`): connection object
|
|
366
408
|
reason (Exception): exception representing reason for loss of connection
|
|
367
409
|
"""
|
|
368
410
|
self.channel = None
|
|
369
411
|
if self._closing:
|
|
370
412
|
self.connection.ioloop.stop()
|
|
413
|
+
else:
|
|
414
|
+
logger.warning(
|
|
415
|
+
f"Connection closed, reconnecting in {self._reconnect_delay} seconds: {reason}"
|
|
416
|
+
)
|
|
417
|
+
self.connection.ioloop.call_later(self._reconnect_delay, self.reconnect)
|
|
418
|
+
|
|
419
|
+
def on_connection_open(self, connection):
|
|
420
|
+
"""
|
|
421
|
+
This method is invoked by pika when the connection to RabbitMQ has
|
|
422
|
+
been established. At this point we can create a channel and start
|
|
423
|
+
consuming messages.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
connection (:obj:`pika.connection.Connection`): connection object
|
|
427
|
+
"""
|
|
428
|
+
self.connection = connection
|
|
429
|
+
self.connection.channel(on_open_callback=self.on_channel_open)
|
|
430
|
+
logger.info("Connection established successfully.")
|
|
431
|
+
|
|
432
|
+
def reconnect(self):
|
|
433
|
+
"""
|
|
434
|
+
Reconnect to RabbitMQ by reinitializing the connection.
|
|
435
|
+
"""
|
|
436
|
+
if not self._closing:
|
|
437
|
+
try:
|
|
438
|
+
logger.info("Attempting to reconnect to RabbitMQ...")
|
|
439
|
+
self.connection = pika.SelectConnection(
|
|
440
|
+
parameters=self._connection_parameters,
|
|
441
|
+
on_open_callback=self.on_connection_open,
|
|
442
|
+
on_open_error_callback=self.on_connection_error,
|
|
443
|
+
on_close_callback=self.on_connection_closed,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Start the I/O loop in a separate thread
|
|
447
|
+
self._io_thread = threading.Thread(target=self._start_io_loop)
|
|
448
|
+
self._io_thread.start()
|
|
449
|
+
self._is_connected.wait()
|
|
450
|
+
logger.info(
|
|
451
|
+
"Attempting to reconnect to RabbitMQ completed successfully."
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
except Exception as e:
|
|
455
|
+
logger.error(f"Reconnection attempt failed: {e}")
|
|
456
|
+
self.connection.ioloop.call_later(self._reconnect_delay, self.reconnect)
|
|
371
457
|
|
|
372
458
|
def shut_down(self) -> None:
|
|
373
459
|
"""
|
|
374
460
|
Shuts down the application by stopping the background event loop and disconnecting from the broker.
|
|
375
461
|
"""
|
|
376
|
-
# self._should_stop.set()
|
|
377
462
|
if self._time_status_publisher is not None:
|
|
378
463
|
self.simulator.remove_observer(self._time_status_publisher)
|
|
379
464
|
self._time_status_publisher = None
|
|
@@ -381,6 +466,11 @@ class Application:
|
|
|
381
466
|
if self.connection:
|
|
382
467
|
self.stop_application()
|
|
383
468
|
self._consuming = False
|
|
469
|
+
|
|
470
|
+
# Stop the token refresh thread
|
|
471
|
+
if hasattr(self, "_should_stop"):
|
|
472
|
+
self._should_stop.set()
|
|
473
|
+
|
|
384
474
|
logger.info(f"Application {self.app_name} successfully shut down.")
|
|
385
475
|
|
|
386
476
|
def send_message(self, app_name, app_topics, payload: str) -> None:
|
|
@@ -466,9 +556,12 @@ class Application:
|
|
|
466
556
|
):
|
|
467
557
|
"""
|
|
468
558
|
Add callback for a topic, supporting wildcards (* and #) in routing keys.
|
|
559
|
+
(* matches exactly one word, # matches zero or more words)
|
|
469
560
|
|
|
470
|
-
|
|
471
|
-
|
|
561
|
+
Args:
|
|
562
|
+
app_name (str): application name
|
|
563
|
+
app_topic (str): topic name
|
|
564
|
+
user_callback (Callable): callback function to be called when a message is received
|
|
472
565
|
"""
|
|
473
566
|
self.was_consuming = True
|
|
474
567
|
self._consuming = True
|
|
@@ -521,6 +614,12 @@ class Application:
|
|
|
521
614
|
"""
|
|
522
615
|
Callback for handling messages received from RabbitMQ.
|
|
523
616
|
Supports both direct routing key matches and wildcard patterns.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
ch (:obj:`pika.channel.Channel`): channel object
|
|
620
|
+
method (:obj:`pika.spec.Basic.Deliver`): method frame
|
|
621
|
+
properties (:obj:`pika.spec.BasicProperties`): properties frame
|
|
622
|
+
body (str): message body
|
|
524
623
|
"""
|
|
525
624
|
routing_key = method.routing_key
|
|
526
625
|
logger.debug(f"Received message with routing key: {routing_key}")
|
|
@@ -571,8 +670,8 @@ class Application:
|
|
|
571
670
|
"""Acknowledge the message delivery from RabbitMQ by sending a
|
|
572
671
|
Basic.Ack RPC method for the delivery tag.
|
|
573
672
|
|
|
574
|
-
:
|
|
575
|
-
|
|
673
|
+
Args:
|
|
674
|
+
delivery_tag (str): The delivery tag of the message to acknowledge
|
|
576
675
|
"""
|
|
577
676
|
try:
|
|
578
677
|
logger.debug(f"Acknowledging message {delivery_tag}")
|
|
@@ -596,10 +695,10 @@ class Application:
|
|
|
596
695
|
) -> None:
|
|
597
696
|
"""
|
|
598
697
|
Declares and binds a queue to the exchange. The queue is bound to the exchange using the routing key. The routing key is created using the application name and topic.
|
|
698
|
+
|
|
599
699
|
Args:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
app_specific_extender (str): application specific extender, used to create a unique queue name for the application. If the app_specific_extender is not provided, the queue name is the same as the routing key.
|
|
700
|
+
routing_key (str): routing key
|
|
701
|
+
app_specific_extender (str): application-specific extender for the queue name
|
|
603
702
|
"""
|
|
604
703
|
try:
|
|
605
704
|
if app_specific_extender:
|
|
@@ -684,8 +783,10 @@ class Application:
|
|
|
684
783
|
cancellation of a consumer. At this point we will close the channel.
|
|
685
784
|
This will invoke the on_channel_closed method once the channel has been
|
|
686
785
|
closed, which will in-turn close the connection.
|
|
687
|
-
|
|
688
|
-
:
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
_unused_frame (:obj:`pika.frame.Method`): The Basic.CancelOk frame
|
|
789
|
+
userdata (str|unicode): Extra user data (consumer tag)
|
|
689
790
|
"""
|
|
690
791
|
self._consuming = False
|
|
691
792
|
logger.info(
|
|
@@ -758,7 +859,7 @@ class Application:
|
|
|
758
859
|
logger.info(f"Wallclock offset updated to {offset}.")
|
|
759
860
|
return
|
|
760
861
|
except ntplib.NTPException:
|
|
761
|
-
logger.
|
|
862
|
+
logger.warning(
|
|
762
863
|
f"Could not connect to {host}, attempt #{i+1}/{max_retry} in {retry_delay_s} s."
|
|
763
864
|
)
|
|
764
865
|
time.sleep(retry_delay_s)
|
|
@@ -244,6 +244,7 @@ class RabbitMQConfig(BaseModel):
|
|
|
244
244
|
blocked_connection_timeout: int = Field(
|
|
245
245
|
None, description="Timeout for blocked connections."
|
|
246
246
|
)
|
|
247
|
+
reconnect_delay: int = Field(10, description="Reconnect delay, in seconds.")
|
|
247
248
|
|
|
248
249
|
|
|
249
250
|
class KeycloakConfig(BaseModel):
|
|
@@ -258,7 +259,25 @@ class KeycloakConfig(BaseModel):
|
|
|
258
259
|
|
|
259
260
|
class ServersConfig(BaseModel):
|
|
260
261
|
rabbitmq: RabbitMQConfig = Field(..., description="RabbitMQ configuration.")
|
|
261
|
-
keycloak: KeycloakConfig = Field(
|
|
262
|
+
keycloak: Optional[KeycloakConfig] = Field(
|
|
263
|
+
None, description="Keycloak configuration."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@model_validator(mode="before")
|
|
267
|
+
def validate_keycloak_authentication(cls, values):
|
|
268
|
+
rabbitmq_config = values.get("rabbitmq")
|
|
269
|
+
keycloak_config = values.get("keycloak")
|
|
270
|
+
|
|
271
|
+
# Check if rabbitmq_config is a dictionary and validate the keycloak_authentication key
|
|
272
|
+
if (
|
|
273
|
+
isinstance(rabbitmq_config, dict)
|
|
274
|
+
and rabbitmq_config.get("keycloak_authentication", False)
|
|
275
|
+
and not keycloak_config
|
|
276
|
+
):
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"Keycloak authentication is enabled, but the Keycloak configuration is missing."
|
|
279
|
+
)
|
|
280
|
+
return values
|
|
262
281
|
|
|
263
282
|
|
|
264
283
|
class GeneralConfig(BaseModel):
|
|
@@ -301,8 +320,9 @@ class ManagerConfig(BaseModel):
|
|
|
301
320
|
if "time_status_step" in values:
|
|
302
321
|
time_status_step = values["time_status_step"]
|
|
303
322
|
if isinstance(time_status_step, str):
|
|
323
|
+
hours, minutes, seconds = map(int, time_status_step.split(":"))
|
|
304
324
|
time_status_step = timedelta(
|
|
305
|
-
seconds=
|
|
325
|
+
hours=hours, minutes=minutes, seconds=seconds
|
|
306
326
|
)
|
|
307
327
|
if isinstance(time_status_step, timedelta):
|
|
308
328
|
values["time_status_step"] = timedelta(
|
|
@@ -332,7 +352,8 @@ class ManagedApplicationConfig(BaseModel):
|
|
|
332
352
|
if "time_step" in values:
|
|
333
353
|
time_step = values["time_step"]
|
|
334
354
|
if isinstance(time_step, str):
|
|
335
|
-
|
|
355
|
+
hours, minutes, seconds = map(int, time_step.split(":"))
|
|
356
|
+
time_step = timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
|
336
357
|
if isinstance(time_step, timedelta):
|
|
337
358
|
values["time_step"] = timedelta(
|
|
338
359
|
seconds=time_step.total_seconds() * time_scale_factor
|
|
@@ -341,8 +362,9 @@ class ManagedApplicationConfig(BaseModel):
|
|
|
341
362
|
if "time_status_step" in values:
|
|
342
363
|
time_status_step = values["time_status_step"]
|
|
343
364
|
if isinstance(time_status_step, str):
|
|
365
|
+
hours, minutes, seconds = map(int, time_status_step.split(":"))
|
|
344
366
|
time_status_step = timedelta(
|
|
345
|
-
seconds=
|
|
367
|
+
hours=hours, minutes=minutes, seconds=seconds
|
|
346
368
|
)
|
|
347
369
|
if isinstance(time_status_step, timedelta):
|
|
348
370
|
values["time_status_step"] = timedelta(
|
|
@@ -374,9 +396,13 @@ class LoggerApplicationConfig(BaseModel):
|
|
|
374
396
|
|
|
375
397
|
class ExecConfig(BaseModel):
|
|
376
398
|
general: GeneralConfig
|
|
377
|
-
manager: Optional[ManagerConfig] = None
|
|
378
|
-
managed_application: Optional[ManagedApplicationConfig] =
|
|
379
|
-
|
|
399
|
+
manager: Optional[ManagerConfig] = Field(None, description="Manager configuration.")
|
|
400
|
+
managed_application: Optional[ManagedApplicationConfig] = Field(
|
|
401
|
+
None, description="Managed application configuration."
|
|
402
|
+
)
|
|
403
|
+
logger_application: Optional[LoggerApplicationConfig] = Field(
|
|
404
|
+
None, description="Logger application configuration."
|
|
405
|
+
)
|
|
380
406
|
|
|
381
407
|
|
|
382
408
|
class Config(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nost_tools
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Tools for Novel Observing Strategies Testbed (NOS-T) Applications
|
|
5
5
|
Author-email: "Paul T. Grogan" <paul.grogan@asu.edu>, "Emmanuel M. Gonzalez" <emmanuelgonzalez@asu.edu>
|
|
6
6
|
License: BSD License
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|