nost-tools 2.2.0__tar.gz → 2.4.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.

Potentially problematic release.


This version of nost-tools might be problematic. Click here for more details.

Files changed (26) hide show
  1. {nost_tools-2.2.0 → nost_tools-2.4.0}/PKG-INFO +1 -1
  2. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/__init__.py +1 -1
  3. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/application.py +191 -51
  4. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/configuration.py +9 -7
  5. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/managed_application.py +50 -41
  6. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/manager.py +78 -69
  7. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/schemas.py +73 -10
  8. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/simulator.py +1 -3
  9. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools.egg-info/PKG-INFO +1 -1
  10. {nost_tools-2.2.0 → nost_tools-2.4.0}/LICENSE +0 -0
  11. {nost_tools-2.2.0 → nost_tools-2.4.0}/README.md +0 -0
  12. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/application_utils.py +0 -0
  13. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/entity.py +0 -0
  14. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/errors.py +0 -0
  15. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/logger_application.py +0 -0
  16. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/observer.py +0 -0
  17. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools/publisher.py +0 -0
  18. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools.egg-info/SOURCES.txt +0 -0
  19. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools.egg-info/dependency_links.txt +0 -0
  20. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools.egg-info/requires.txt +0 -0
  21. {nost_tools-2.2.0 → nost_tools-2.4.0}/nost_tools.egg-info/top_level.txt +0 -0
  22. {nost_tools-2.2.0 → nost_tools-2.4.0}/pyproject.toml +0 -0
  23. {nost_tools-2.2.0 → nost_tools-2.4.0}/setup.cfg +0 -0
  24. {nost_tools-2.2.0 → nost_tools-2.4.0}/tests/test_entity.py +0 -0
  25. {nost_tools-2.2.0 → nost_tools-2.4.0}/tests/test_observer.py +0 -0
  26. {nost_tools-2.2.0 → nost_tools-2.4.0}/tests/test_simulator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nost_tools
3
- Version: 2.2.0
3
+ Version: 2.4.0
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-Expression: BSD-3-Clause
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.0"
1
+ __version__ = "2.4.0"
2
2
 
3
3
  from .application import Application
4
4
  from .application_utils import ConnectionConfig, ModeStatusObserver, TimeStatusPublisher
@@ -4,6 +4,7 @@ Provides a base application that publishes messages from a simulator to a broker
4
4
 
5
5
  import functools
6
6
  import logging
7
+ import logging.handlers
7
8
  import os
8
9
  import signal
9
10
  import ssl
@@ -48,13 +49,19 @@ class Application:
48
49
  time_status_init (:obj:`datetime`): Scenario time of first time status message
49
50
  """
50
51
 
51
- def __init__(self, app_name: str, app_description: str = None):
52
+ def __init__(
53
+ self,
54
+ app_name: str,
55
+ app_description: str = None,
56
+ setup_signal_handlers: bool = True,
57
+ ):
52
58
  """
53
59
  Initializes a new application.
54
60
 
55
61
  Args:
56
62
  app_name (str): application name
57
63
  app_description (str): application description (optional)
64
+ setup_signal_handlers (bool): whether to set up signal handlers (default: True)
58
65
  """
59
66
  self.simulator = Simulator()
60
67
  self.connection = None
@@ -85,8 +92,12 @@ class Application:
85
92
  self._token_refresh_thread = None
86
93
  self.token_refresh_interval = None
87
94
  self._reconnect_delay = None
95
+ # Offset
96
+ self._wallclock_refresh_thread = None
97
+ self.wallclock_offset_refresh_interval = None
88
98
  # Set up signal handlers for graceful shutdown
89
- self._setup_signal_handlers()
99
+ if setup_signal_handlers:
100
+ self._setup_signal_handlers()
90
101
 
91
102
  def _setup_signal_handlers(self):
92
103
  """
@@ -194,6 +205,42 @@ class Application:
194
205
  self._token_refresh_thread.start()
195
206
  logger.debug("Starting refresh token thread successfully completed.")
196
207
 
208
+ def start_wallclock_refresh_thread(self): # , interval=30, host="pool.ntp.org"):
209
+ """
210
+ Starts a background thread to refresh the wallclock offset periodically.
211
+
212
+ Args:
213
+ interval (int): Seconds between wallclock offset refreshes (default: 3600 seconds/1 hour)
214
+ host (str): NTP host to query (default: 'pool.ntp.org')
215
+ """
216
+ logger.debug("Starting wallclock offset refresh thread.")
217
+
218
+ def refresh_wallclock_periodically():
219
+ while not self._should_stop.wait(
220
+ timeout=self.config.rc.wallclock_offset_properties.wallclock_offset_refresh_interval
221
+ ):
222
+ logger.debug("Wallclock refresh thread is running.")
223
+ try:
224
+ logger.info(
225
+ f"Contacting {self.config.rc.wallclock_offset_properties.ntp_host} to retrieve wallclock offset."
226
+ )
227
+ response = ntplib.NTPClient().request(
228
+ self.config.rc.wallclock_offset_properties.ntp_host,
229
+ version=3,
230
+ timeout=2,
231
+ )
232
+ offset = timedelta(seconds=response.offset)
233
+ self.simulator.set_wallclock_offset(offset)
234
+ logger.info(f"Wallclock offset updated to {offset}.")
235
+ except Exception as e:
236
+ logger.debug(f"Failed to refresh wallclock offset: {e}")
237
+
238
+ self._wallclock_refresh_thread = threading.Thread(
239
+ target=refresh_wallclock_periodically
240
+ )
241
+ self._wallclock_refresh_thread.start()
242
+ logger.debug("Starting wallclock offset refresh thread successfully completed.")
243
+
197
244
  def update_connection_credentials(self, access_token):
198
245
  """
199
246
  Updates the connection credentials with the new access token.
@@ -203,14 +250,33 @@ class Application:
203
250
  """
204
251
  self.connection.update_secret(access_token, "secret")
205
252
 
253
+ def _get_parameters_from_config(self):
254
+ """
255
+ Gets application parameters from configuration or returns None if not available.
256
+ This method can be overridden by subclasses to customize parameter retrieval.
257
+
258
+ Returns:
259
+ object: Configuration parameters object or None
260
+ """
261
+ if self.config and self.config.rc.yaml_file:
262
+ try:
263
+ return getattr(
264
+ self.config.rc.simulation_configuration.execution_parameters,
265
+ "application",
266
+ None,
267
+ )
268
+ except (AttributeError, KeyError):
269
+ return None
270
+ return None
271
+
206
272
  def start_up(
207
273
  self,
208
274
  prefix: str,
209
275
  config: ConnectionConfig,
210
- set_offset: bool = None, # True,
276
+ set_offset: bool = True,
211
277
  time_status_step: timedelta = None,
212
278
  time_status_init: datetime = None,
213
- shut_down_when_terminated: bool = None,
279
+ shut_down_when_terminated: bool = False,
214
280
  ) -> None:
215
281
  """
216
282
  Starts up the application to prepare for scenario execution.
@@ -225,31 +291,56 @@ class Application:
225
291
  time_status_init (:obj:`datetime`): scenario time for first time status message
226
292
  shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
227
293
  """
228
- if (
229
- set_offset is not None
230
- and time_status_step is not None
231
- and time_status_init is not None
232
- and shut_down_when_terminated is not None
233
- ):
294
+ self.config = config
295
+
296
+ if self.config.rc.yaml_file:
297
+ logger.info(
298
+ f"Collecting start up parameters from YAML configuration file: {self.config.rc.yaml_file}"
299
+ )
300
+ parameters = self._get_parameters_from_config()
301
+ if parameters:
302
+ self.set_offset = getattr(parameters, "set_offset", set_offset)
303
+ self.time_status_step = getattr(
304
+ parameters, "time_status_step", time_status_step
305
+ )
306
+ self.time_status_init = getattr(
307
+ parameters, "time_status_init", time_status_init
308
+ )
309
+ self.shut_down_when_terminated = getattr(
310
+ parameters, "shut_down_when_terminated", shut_down_when_terminated
311
+ )
312
+
313
+ # Configure file logging if requested
314
+ if getattr(parameters, "enable_file_logging", False):
315
+ self.configure_file_logging(
316
+ log_dir=getattr(parameters, "log_dir", None),
317
+ log_filename=getattr(parameters, "log_filename", None),
318
+ log_level=getattr(parameters, "log_level", None),
319
+ max_bytes=getattr(parameters, "max_bytes", None),
320
+ backup_count=getattr(parameters, "backup_count", None),
321
+ log_format=getattr(parameters, "log_format", None),
322
+ )
323
+ else:
324
+ logger.warning("No parameters found in configuration, using defaults")
325
+ self.set_offset = set_offset
326
+ self.time_status_step = time_status_step
327
+ self.time_status_init = time_status_init
328
+ self.shut_down_when_terminated = shut_down_when_terminated
329
+ else:
330
+ logger.info(
331
+ f"Collecting start up parameters from user input or default values."
332
+ )
234
333
  self.set_offset = set_offset
235
334
  self.time_status_step = time_status_step
236
335
  self.time_status_init = time_status_init
237
336
  self.shut_down_when_terminated = shut_down_when_terminated
238
- else:
239
- self.config = config
240
- parameters = getattr(
241
- self.config.rc.simulation_configuration.execution_parameters,
242
- "application",
243
- None,
244
- )
245
- self.set_offset = parameters.set_offset
246
- self.time_status_step = parameters.time_status_step
247
- self.time_status_init = parameters.time_status_init
248
- self.shut_down_when_terminated = parameters.shut_down_when_terminated
249
337
 
250
338
  if self.set_offset:
251
- # Set the system clock offset
252
- self.set_wallclock_offset()
339
+ # Start periodic wallclock offset updates instead of one-time call
340
+ logger.info(
341
+ f"Wallclock offset will be set every {self.config.rc.wallclock_offset_properties.wallclock_offset_refresh_interval} seconds using {self.config.rc.wallclock_offset_properties.ntp_host}."
342
+ )
343
+ self.start_wallclock_refresh_thread()
253
344
 
254
345
  # Set the prefix and configuration parameters
255
346
  self.prefix = prefix
@@ -290,7 +381,6 @@ class Application:
290
381
  locale=config.rc.server_configuration.servers.rabbitmq.locale,
291
382
  blocked_connection_timeout=config.rc.server_configuration.servers.rabbitmq.blocked_connection_timeout,
292
383
  )
293
- logger.info(parameters)
294
384
 
295
385
  # Configure transport layer security (TLS) if needed
296
386
  if self.config.rc.server_configuration.servers.rabbitmq.tls:
@@ -1348,34 +1438,26 @@ class Application:
1348
1438
  )
1349
1439
  else:
1350
1440
  logger.info("Closing token refresh thread completed successfully")
1351
-
1441
+ # Also stop wallclock refresh thread if it exists
1442
+ if (
1443
+ hasattr(self, "_wallclock_refresh_thread")
1444
+ and self._wallclock_refresh_thread
1445
+ and self._wallclock_refresh_thread.is_alive()
1446
+ ):
1447
+ logger.info("Closing wallclock refresh thread.")
1448
+ # Set a timeout to avoid hanging indefinitely
1449
+ self._wallclock_refresh_thread.join(timeout=60.0)
1450
+ # Check if it's still alive after timeout
1451
+ if self._wallclock_refresh_thread.is_alive():
1452
+ logger.warning(
1453
+ "Closing wallclock refresh thread timed out after 60 seconds. "
1454
+ )
1455
+ else:
1456
+ logger.info(
1457
+ "Closing wallclock refresh thread completed successfully"
1458
+ )
1352
1459
  logger.debug("Stop_application completed successfully.")
1353
1460
 
1354
- def set_wallclock_offset(
1355
- self, host="pool.ntp.org", retry_delay_s: int = 5, max_retry: int = 5
1356
- ) -> None:
1357
- """
1358
- Issues a Network Time Protocol (NTP) request to determine the system clock offset.
1359
-
1360
- Args:
1361
- host (str): NTP host (default: 'pool.ntp.org')
1362
- retry_delay_s (int): number of seconds to wait before retrying
1363
- max_retry (int): maximum number of retries allowed
1364
- """
1365
- for i in range(max_retry):
1366
- try:
1367
- logger.info(f"Contacting {host} to retrieve wallclock offset.")
1368
- response = ntplib.NTPClient().request(host, version=3, timeout=2)
1369
- offset = timedelta(seconds=response.offset)
1370
- self.simulator.set_wallclock_offset(offset)
1371
- logger.info(f"Wallclock offset updated to {offset}.")
1372
- return
1373
- except ntplib.NTPException:
1374
- logger.warning(
1375
- f"Could not connect to {host}, attempt #{i+1}/{max_retry} in {retry_delay_s} s."
1376
- )
1377
- time.sleep(retry_delay_s)
1378
-
1379
1461
  def _create_time_status_publisher(
1380
1462
  self, time_status_step: timedelta, time_status_init: datetime
1381
1463
  ) -> None:
@@ -1405,9 +1487,67 @@ class Application:
1405
1487
 
1406
1488
  def _create_shut_down_observer(self) -> None:
1407
1489
  """
1408
- Creates an observer to shut down the application when the simulation is terminated.
1490
+ Creates a shut down observer to close the application when the simulator is terminated.
1409
1491
  """
1410
1492
  if self._shut_down_observer is not None:
1411
1493
  self.simulator.remove_observer(self._shut_down_observer)
1412
1494
  self._shut_down_observer = ShutDownObserver(self)
1413
1495
  self.simulator.add_observer(self._shut_down_observer)
1496
+
1497
+ def configure_file_logging(
1498
+ self,
1499
+ log_dir: str = None,
1500
+ log_filename: str = None,
1501
+ log_level: str = None,
1502
+ max_bytes: int = None,
1503
+ backup_count: int = None,
1504
+ log_format: str = None,
1505
+ ):
1506
+ """
1507
+ Configures file logging for the application.
1508
+
1509
+ Args:
1510
+ log_dir (str): Directory where log files will be stored
1511
+ log_filename (str): Name of the log file. If None, a timestamped filename will be used
1512
+ log_level (str): Logging level (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
1513
+ max_bytes (int): Maximum file size in bytes before rotating
1514
+ backup_count (int): Number of backup files to keep
1515
+ log_format (str): Log message format
1516
+ """
1517
+ try:
1518
+ if log_filename is None:
1519
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1520
+ log_filename = os.path.join(log_dir, f"{self.app_name}_{timestamp}.log")
1521
+ else:
1522
+ log_filename = os.path.join(log_dir, log_filename)
1523
+
1524
+ # Create log directory if it doesn't exist
1525
+ if log_dir and not os.path.exists(log_dir):
1526
+ os.makedirs(log_dir)
1527
+ logger.info(f"Log directory {log_dir} successfully created.")
1528
+
1529
+ # Configure rotating file handler
1530
+ handler = logging.handlers.RotatingFileHandler(
1531
+ log_filename, maxBytes=max_bytes, backupCount=backup_count
1532
+ )
1533
+
1534
+ # Set log level
1535
+ level = getattr(logging, log_level.upper(), logging.INFO)
1536
+ handler.setLevel(level)
1537
+
1538
+ # Set log format
1539
+ if log_format is not None:
1540
+ formatter = logging.Formatter(log_format)
1541
+ handler.setFormatter(formatter)
1542
+ else:
1543
+ # Default format
1544
+ formatter = logging.Formatter(
1545
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
1546
+ )
1547
+ handler.setFormatter(formatter)
1548
+
1549
+ # Add the handler to the root logger
1550
+ logging.getLogger().addHandler(handler)
1551
+ logger.info(f"File logging configured: {log_filename} (level: {log_level})")
1552
+ except Exception as e:
1553
+ logger.error(f"Error configuring file logging: {e}")
@@ -21,6 +21,7 @@ from .schemas import (
21
21
  RuntimeConfig,
22
22
  ServersConfig,
23
23
  SimulationConfig,
24
+ WallclockOffsetProperties,
24
25
  )
25
26
 
26
27
  logger = logging.getLogger(__name__)
@@ -52,6 +53,7 @@ class ConnectionConfig:
52
53
  password: str = None,
53
54
  rabbitmq_host: str = None,
54
55
  rabbitmq_port: int = None,
56
+ keycloak_authentication: bool = False,
55
57
  keycloak_host: str = None,
56
58
  keycloak_port: int = None,
57
59
  keycloak_realm: str = None,
@@ -70,6 +72,7 @@ class ConnectionConfig:
70
72
  password (str): client password, provided by NOS-T operator
71
73
  host (str): broker hostname
72
74
  rabbitmq_port (int): RabbitMQ broker port number
75
+ keycloak_authentication (bool): True, if Keycloak IAM authentication is used
73
76
  keycloak_port (int): Keycloak IAM port number
74
77
  keycloak_realm (str): Keycloak realm name
75
78
  client_id (str): Keycloak client ID
@@ -84,6 +87,7 @@ class ConnectionConfig:
84
87
  self.rabbitmq_host = rabbitmq_host
85
88
  self.keycloak_host = keycloak_host
86
89
  self.rabbitmq_port = rabbitmq_port
90
+ self.keycloak_authentication = keycloak_authentication
87
91
  self.keycloak_port = keycloak_port
88
92
  self.keycloak_realm = keycloak_realm
89
93
  self.client_id = client_id
@@ -283,6 +287,7 @@ class ConnectionConfig:
283
287
  port=self.rabbitmq_port,
284
288
  virtual_host=self.virtual_host,
285
289
  tls=self.is_tls,
290
+ keycloak_authentication=self.keycloak_authentication,
286
291
  ),
287
292
  keycloak=KeycloakConfig(
288
293
  host=self.keycloak_host,
@@ -312,13 +317,8 @@ class ConnectionConfig:
312
317
  del server_config.execution
313
318
  self.server_config = server_config
314
319
 
315
- if (
316
- self.username is not None
317
- and self.password is not None
318
- and self.client_id is not None
319
- and self.client_secret_key is not None
320
- ):
321
- logger.info("Using provided credentials.")
320
+ if self.username is not None and self.password is not None:
321
+ logger.info("Using user-provided credentials.")
322
322
  self.credentials_config = Credentials(
323
323
  username=self.username,
324
324
  password=self.password,
@@ -329,8 +329,10 @@ class ConnectionConfig:
329
329
  self.load_environment_variables()
330
330
 
331
331
  self.rc = RuntimeConfig(
332
+ wallclock_offset_properties=WallclockOffsetProperties(),
332
333
  credentials=self.credentials_config,
333
334
  server_configuration=server_config,
334
335
  simulation_configuration=self.simulation_config,
335
336
  application_configuration=self.app_specific,
337
+ yaml_file=self.yaml_file,
336
338
  )
@@ -32,27 +32,57 @@ class ManagedApplication(Application):
32
32
  time_step (:obj:`timedelta`): scenario time step used in execution
33
33
  """
34
34
 
35
- def __init__(self, app_name: str, app_description: str = None):
35
+ def __init__(
36
+ self,
37
+ app_name: str,
38
+ app_description: str = None,
39
+ setup_signal_handlers: bool = True,
40
+ ):
36
41
  """
37
42
  Initializes a new managed application.
38
43
 
39
44
  Args:
40
45
  app_name (str): application name
41
46
  app_description (str): application description
47
+ setup_signal_handlers (bool): whether to set up signal handlers (default: True)
42
48
  """
43
- super().__init__(app_name, app_description)
49
+ super().__init__(
50
+ app_name, app_description, setup_signal_handlers=setup_signal_handlers
51
+ )
44
52
  self.time_step = None
45
53
  self._sim_start_time = None
46
54
  self._sim_stop_time = None
47
55
 
56
+ def _get_parameters_from_config(self):
57
+ """
58
+ Override to get parameters specific to managed applications
59
+
60
+ Returns:
61
+ object: Configuration parameters for this managed application
62
+ """
63
+ if self.config and self.config.rc.yaml_file:
64
+ try:
65
+ parameters = (
66
+ self.config.rc.simulation_configuration.execution_parameters.managed_applications
67
+ )
68
+ try:
69
+ # Try to get app-specific parameters
70
+ return parameters[self.app_name]
71
+ except KeyError:
72
+ # Fall back to default parameters
73
+ return parameters.get("default")
74
+ except (AttributeError, KeyError):
75
+ return None
76
+ return None
77
+
48
78
  def start_up(
49
79
  self,
50
80
  prefix: str,
51
81
  config: ConnectionConfig,
52
- set_offset: bool = None,
82
+ set_offset: bool = True,
53
83
  time_status_step: timedelta = None,
54
84
  time_status_init: datetime = None,
55
- shut_down_when_terminated: bool = None,
85
+ shut_down_when_terminated: bool = False,
56
86
  time_step: timedelta = None,
57
87
  manager_app_name: str = None,
58
88
  ) -> None:
@@ -70,48 +100,27 @@ class ManagedApplication(Application):
70
100
  time_step (:obj:`timedelta`): scenario time step used in execution (Default: 1 second)
71
101
  manager_app_name (str): manager application name (Default: manager)
72
102
  """
73
- if (
74
- set_offset is not None
75
- and time_status_step is not None
76
- and time_status_init is not None
77
- and shut_down_when_terminated is not None
78
- and time_step is not None
79
- and manager_app_name is not None
80
- ):
81
- self.set_offset = set_offset
82
- self.time_status_step = time_status_step
83
- self.time_status_init = time_status_init
84
- self.shut_down_when_terminated = shut_down_when_terminated
85
- self.time_step = time_step
86
- self.manager_app_name = manager_app_name
87
- else:
88
- self.config = config
89
- parameters = (
90
- self.config.rc.simulation_configuration.execution_parameters.managed_applications
91
- )
92
-
93
- try:
94
- parameters = parameters[self.app_name]
95
- except KeyError:
96
- parameters = parameters["default"]
97
- self.set_offset = parameters.set_offset
98
- self.time_status_step = parameters.time_status_step
99
- self.time_status_init = parameters.time_status_init
100
- self.shut_down_when_terminated = parameters.shut_down_when_terminated
101
- self.time_step = parameters.time_step
102
- self.manager_app_name = parameters.manager_app_name
103
+ self.config = config
103
104
 
104
- # start up base application
105
+ # Call base start_up to handle common parameters
105
106
  super().start_up(
106
107
  prefix,
107
108
  config,
108
- self.set_offset,
109
- self.time_status_step,
110
- self.time_status_init,
111
- self.shut_down_when_terminated,
109
+ set_offset,
110
+ time_status_step,
111
+ time_status_init,
112
+ shut_down_when_terminated,
112
113
  )
113
- self.time_step = self.time_step
114
- self.manager_app_name = self.manager_app_name
114
+
115
+ # Get additional parameters specific to managed applications
116
+ if self.config.rc.yaml_file:
117
+ parameters = self._get_parameters_from_config()
118
+ if parameters:
119
+ self.time_step = parameters.time_step
120
+ self.manager_app_name = parameters.manager_app_name
121
+ else:
122
+ self.time_step = time_step
123
+ self.manager_app_name = manager_app_name
115
124
 
116
125
  # Register callback functions
117
126
  self.add_message_callback(
@@ -67,12 +67,22 @@ class Manager(Application):
67
67
  required_apps_status (dict): Ready status for all required applications
68
68
  """
69
69
 
70
- def __init__(self):
70
+ def __init__(
71
+ self,
72
+ app_name: str = "manager",
73
+ app_description: str = None,
74
+ setup_signal_handlers: bool = True,
75
+ ):
71
76
  """
72
77
  Initializes a new manager.
78
+
79
+ Attributes:
80
+ setup_signal_handlers (bool): whether to set up signal handlers (default: True)
73
81
  """
74
82
  # call super class constructor
75
- super().__init__("manager")
83
+ super().__init__(
84
+ app_name, app_description, setup_signal_handlers=setup_signal_handlers
85
+ )
76
86
  self.required_apps_status = {}
77
87
 
78
88
  self.sim_start_time = None
@@ -129,16 +139,32 @@ class Manager(Application):
129
139
  f"Heartbeat check: {remaining:.2f} seconds remaining in sleep"
130
140
  )
131
141
 
142
+ def _get_parameters_from_config(self):
143
+ """
144
+ Override to get parameters specific to manager application
145
+
146
+ Returns:
147
+ object: Configuration parameters for the manager application
148
+ """
149
+ if self.config and self.config.rc.yaml_file:
150
+ try:
151
+ return getattr(
152
+ self.config.rc.simulation_configuration.execution_parameters,
153
+ "manager",
154
+ None,
155
+ )
156
+ except (AttributeError, KeyError):
157
+ return None
158
+ return None
159
+
132
160
  def start_up(
133
161
  self,
134
162
  prefix: str,
135
163
  config: ConnectionConfig,
136
- set_offset: bool = None,
164
+ set_offset: bool = True,
137
165
  time_status_step: timedelta = None,
138
166
  time_status_init: datetime = None,
139
- shut_down_when_terminated: bool = None,
140
- time_step: timedelta = None,
141
- manager_app_name: str = None,
167
+ shut_down_when_terminated: bool = False
142
168
  ) -> None:
143
169
  """
144
170
  Starts up the application by connecting to message broker, starting a background event loop,
@@ -151,45 +177,20 @@ class Manager(Application):
151
177
  time_status_step (:obj:`timedelta`): scenario duration between time status messages
152
178
  time_status_init (:obj:`datetime`): scenario time for first time status message
153
179
  shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
154
- time_step (:obj:`timedelta`): scenario time step used in execution (Default: 1 second)
155
- manager_app_name (str): manager application name (Default: manager)
156
180
  """
157
- if (
158
- set_offset is not None
159
- and time_status_step is not None
160
- and time_status_init is not None
161
- and shut_down_when_terminated is not None
162
- and time_step is not None
163
- and manager_app_name is not None
164
- ):
165
- self.set_offset = set_offset
166
- self.time_status_step = time_status_step
167
- self.time_status_init = time_status_init
168
- self.shut_down_when_terminated = shut_down_when_terminated
169
- self.time_step = time_step
170
- self.manager_app_name = manager_app_name
171
- else:
172
- self.config = config
173
- parameters = (
174
- self.config.rc.simulation_configuration.execution_parameters.manager
175
- )
176
- self.set_offset = parameters.set_offset
177
- self.time_status_step = parameters.time_status_step
178
- self.time_status_init = parameters.time_status_init
179
- self.shut_down_when_terminated = parameters.shut_down_when_terminated
180
- self.time_step = parameters.time_step
181
+ self.config = config
181
182
 
182
- # start up base application
183
+ # Call base start_up to handle common parameters
183
184
  super().start_up(
184
185
  prefix,
185
186
  config,
186
- self.set_offset,
187
- self.time_status_step,
188
- self.time_status_init,
189
- self.shut_down_when_terminated,
187
+ set_offset,
188
+ time_status_step,
189
+ time_status_init,
190
+ shut_down_when_terminated,
190
191
  )
191
192
 
192
- # Establish the exchange
193
+ # Additional manager-specific setup: establish the exchange
193
194
  self.establish_exchange()
194
195
 
195
196
  def execute_test_plan(self, *args, **kwargs) -> None:
@@ -241,8 +242,33 @@ class Manager(Application):
241
242
  init_retry_delay_s (float): number of seconds to wait between initialization commands while waiting for required applications
242
243
  init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
243
244
  """
244
- # Initialize parameters from arguments or config
245
- if sim_start_time is not None and sim_stop_time is not None:
245
+ if self.config.rc.yaml_file:
246
+ logger.info(
247
+ f"Collecting execution parameters from YAML configuration file: {self.config.rc.yaml_file}"
248
+ )
249
+ parameters = getattr(
250
+ self.config.rc.simulation_configuration.execution_parameters,
251
+ self.app_name,
252
+ None,
253
+ )
254
+ self.sim_start_time = parameters.sim_start_time
255
+ self.sim_stop_time = parameters.sim_stop_time
256
+ self.start_time = parameters.start_time
257
+ self.time_step = parameters.time_step
258
+ self.time_scale_factor = parameters.time_scale_factor
259
+ self.time_scale_updates = parameters.time_scale_updates
260
+ self.time_status_step = parameters.time_status_step
261
+ self.time_status_init = parameters.time_status_init
262
+ self.command_lead = parameters.command_lead
263
+ self.required_apps = [
264
+ app for app in parameters.required_apps if app != self.app_name
265
+ ]
266
+ self.init_retry_delay_s = parameters.init_retry_delay_s
267
+ self.init_max_retry = parameters.init_max_retry
268
+ else:
269
+ logger.info(
270
+ f"Collecting execution parameters from user input or default values."
271
+ )
246
272
  self.sim_start_time = sim_start_time
247
273
  self.sim_stop_time = sim_stop_time
248
274
  self.start_time = start_time
@@ -255,36 +281,10 @@ class Manager(Application):
255
281
  self.required_apps = required_apps
256
282
  self.init_retry_delay_s = init_retry_delay_s
257
283
  self.init_max_retry = init_max_retry
258
- else:
259
- if self.config.rc:
260
- logger.info("Retrieving execution parameters from YAML file.")
261
- parameters = getattr(
262
- self.config.rc.simulation_configuration.execution_parameters,
263
- self.app_name,
264
- None,
265
- )
266
- self.sim_start_time = parameters.sim_start_time
267
- self.sim_stop_time = parameters.sim_stop_time
268
- self.start_time = parameters.start_time
269
- self.time_step = parameters.time_step
270
- self.time_scale_factor = parameters.time_scale_factor
271
- self.time_scale_updates = parameters.time_scale_updates
272
- self.time_status_step = parameters.time_status_step
273
- self.time_status_init = parameters.time_status_init
274
- self.command_lead = parameters.command_lead
275
- self.required_apps = [
276
- app for app in parameters.required_apps if app != self.app_name
277
- ]
278
- self.init_retry_delay_s = parameters.init_retry_delay_s
279
- self.init_max_retry = parameters.init_max_retry
280
- else:
281
- raise ValueError(
282
- "No configuration runtime. Please provide simulation start and stop times."
283
- )
284
284
 
285
285
  # Convert TimeScaleUpdateSchema objects to TimeScaleUpdate objects
286
286
  converted_updates = []
287
- for update_schema in parameters.time_scale_updates:
287
+ for update_schema in self.time_scale_updates:
288
288
  converted_updates.append(
289
289
  TimeScaleUpdate(
290
290
  time_scale_factor=update_schema.time_scale_factor,
@@ -563,8 +563,17 @@ class Manager(Application):
563
563
  app_topics="stop",
564
564
  payload=command.model_dump_json(by_alias=True),
565
565
  )
566
- # update the execution end time
567
- self.simulator.set_end_time(sim_stop_time)
566
+
567
+ # Update the execution end time if simulator is in EXECUTING mode
568
+ if self.simulator.get_mode() == Mode.EXECUTING:
569
+ try:
570
+ self.simulator.set_end_time(sim_stop_time)
571
+ except RuntimeError as e:
572
+ logger.warning(f"Could not set simulator end time: {e}")
573
+ else:
574
+ logger.debug(
575
+ "Skipping setting simulator end time as simulator is not in EXECUTING mode"
576
+ )
568
577
 
569
578
  def update(self, time_scale_factor: float, sim_update_time: datetime) -> None:
570
579
  """
@@ -280,7 +280,7 @@ class KeycloakConfig(BaseModel):
280
280
  realm: str = Field("master", description="Keycloak realm.")
281
281
  tls: bool = Field(False, description="Keycloak TLS/SSL.")
282
282
  token_refresh_interval: int = Field(
283
- 60, description="Keycloak token refresh interval, in seconds."
283
+ 240, description="Keycloak token refresh interval, in seconds."
284
284
  )
285
285
 
286
286
 
@@ -307,8 +307,21 @@ class ServersConfig(BaseModel):
307
307
  return values
308
308
 
309
309
 
310
+ class WallclockOffsetProperties(BaseModel):
311
+ """
312
+ Properties to report wallclock offset.
313
+ """
314
+
315
+ wallclock_offset_refresh_interval: Optional[int] = Field(
316
+ 10800, description="Wallclock offset refresh interval, in seconds."
317
+ )
318
+ ntp_host: Optional[str] = Field(
319
+ "pool.ntp.org", description="NTP host for wallclock offset synchronization."
320
+ )
321
+
322
+
310
323
  class GeneralConfig(BaseModel):
311
- prefix: str = Field("nost", description="Execution prefix.")
324
+ prefix: Optional[str] = Field("nost", description="Execution prefix.")
312
325
 
313
326
 
314
327
  class TimeScaleUpdateSchema(BaseModel):
@@ -324,7 +337,35 @@ class TimeScaleUpdateSchema(BaseModel):
324
337
  )
325
338
 
326
339
 
327
- class ManagerConfig(BaseModel):
340
+ class LoggingConfig(BaseModel):
341
+ """
342
+ Configuration for logging.
343
+ """
344
+
345
+ enable_file_logging: Optional[bool] = Field(
346
+ False, description="Enable file logging."
347
+ )
348
+ log_dir: Optional[str] = Field(
349
+ "logs", description="Directory path for log files."
350
+ )
351
+ log_filename: Optional[str] = Field(None, description="Path to the log file.")
352
+ log_level: Optional[str] = Field(
353
+ "INFO", description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)."
354
+ )
355
+ max_bytes: Optional[int] = Field(
356
+ 10 * 1024 * 1024,
357
+ description="Maximum size of the log file in bytes. Default is 10MB.",
358
+ )
359
+ backup_count: Optional[int] = Field(
360
+ 5, description="Number of backup log files to keep."
361
+ )
362
+ log_format: Optional[str] = Field(
363
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
364
+ description="Format of the log messages.",
365
+ )
366
+
367
+
368
+ class ManagerConfig(LoggingConfig):
328
369
  sim_start_time: Optional[datetime] = Field(
329
370
  None, description="Simulation start time."
330
371
  )
@@ -352,6 +393,10 @@ class ManagerConfig(BaseModel):
352
393
  shut_down_when_terminated: bool = Field(
353
394
  False, description="Shut down when terminated."
354
395
  )
396
+ is_scenario_time_step: bool = Field(
397
+ True,
398
+ description="If True, time_step is in scenario time and won't be scaled. If False, time_step is in wallclock time and will be scaled by the time scale factor.",
399
+ )
355
400
  is_scenario_time_status_step: bool = Field(
356
401
  True,
357
402
  description="If True, time_status_step is in scenario time and won't be scaled. If False, time_status_step is in wallclock time and will be scaled by the time scale factor.",
@@ -361,6 +406,16 @@ class ManagerConfig(BaseModel):
361
406
  def scale_time(cls, values):
362
407
  time_scale_factor = values.get("time_scale_factor", 1.0)
363
408
 
409
+ if "time_step" in values and not values.get("is_scenario_time_step", True):
410
+ time_step = values["time_step"]
411
+ if isinstance(time_step, str):
412
+ hours, minutes, seconds = map(int, time_step.split(":"))
413
+ time_step = timedelta(hours=hours, minutes=minutes, seconds=seconds)
414
+ if isinstance(time_step, timedelta):
415
+ values["time_step"] = timedelta(
416
+ seconds=time_step.total_seconds() * time_scale_factor
417
+ )
418
+
364
419
  if "time_status_step" in values and not values.get(
365
420
  "is_scenario_time_status_step", True
366
421
  ):
@@ -378,7 +433,7 @@ class ManagerConfig(BaseModel):
378
433
  return values
379
434
 
380
435
 
381
- class ManagedApplicationConfig(BaseModel):
436
+ class ManagedApplicationConfig(LoggingConfig):
382
437
  time_scale_factor: float = Field(1.0, description="Time scale factor.")
383
438
  time_step: timedelta = Field(
384
439
  timedelta(seconds=1), description="Time step for swe_change."
@@ -440,7 +495,7 @@ class LoggerApplicationConfig(BaseModel):
440
495
  timedelta(seconds=10), description="Time status step."
441
496
  )
442
497
  time_status_init: Optional[datetime] = Field(
443
- datetime(2019, 3, 1, 0, 0, 0), description="Time status init."
498
+ datetime.now(), description="Time status init."
444
499
  )
445
500
  shut_down_when_terminated: Optional[bool] = Field(
446
501
  False, description="Shut down when terminated."
@@ -471,7 +526,9 @@ class ApplicationConfig(BaseModel):
471
526
 
472
527
 
473
528
  class ExecConfig(BaseModel):
474
- general: GeneralConfig
529
+ general: Optional[GeneralConfig] = Field(
530
+ None, description="General configuration for the execution."
531
+ )
475
532
  manager: Optional[ManagerConfig] = Field(None, description="Manager configuration.")
476
533
  managed_applications: Optional[Dict[str, ManagedApplicationConfig]] = Field(
477
534
  default_factory=lambda: {"default": ManagedApplicationConfig()},
@@ -509,11 +566,11 @@ class ChannelConfig(BaseModel):
509
566
 
510
567
 
511
568
  class Credentials(BaseModel):
512
- username: str = Field(..., description="Username for authentication.")
513
- password: str = Field(..., description="Password for authentication.")
514
- client_id: Optional[str] = Field("", description="Client ID for authentication.")
569
+ username: Optional[str] = Field("admin", description="Username for authentication.")
570
+ password: Optional[str] = Field("admin", description="Password for authentication.")
571
+ client_id: Optional[str] = Field(None, description="Client ID for authentication.")
515
572
  client_secret_key: Optional[str] = Field(
516
- "", description="Client secret key for authentication."
573
+ None, description="Client secret key for authentication."
517
574
  )
518
575
 
519
576
 
@@ -529,6 +586,9 @@ class SimulationConfig(BaseModel):
529
586
 
530
587
 
531
588
  class RuntimeConfig(BaseModel):
589
+ wallclock_offset_properties: WallclockOffsetProperties = Field(
590
+ ..., description="Properties for wallclock offset."
591
+ )
532
592
  credentials: Credentials = Field(..., description="Credentials for authentication.")
533
593
  server_configuration: Config = (
534
594
  Field(..., description="Simulation configuration."),
@@ -539,3 +599,6 @@ class RuntimeConfig(BaseModel):
539
599
  application_configuration: Optional[Dict] = Field(
540
600
  None, description="Application-specific, user-provided configuration."
541
601
  )
602
+ yaml_file: Optional[str] = Field(
603
+ None, description="Path to the YAML file containing the configuration."
604
+ )
@@ -516,9 +516,7 @@ class Simulator(Observable):
516
516
  Args:
517
517
  wallclock_offset(:obj:`timedelta`): difference between system clock and trusted wallclock source
518
518
  """
519
- if self._mode == Mode.EXECUTING:
520
- raise RuntimeError("Cannot set wallclock offset: simulator is executing")
521
- elif self._mode == Mode.TERMINATING:
519
+ if self._mode == Mode.TERMINATING:
522
520
  raise RuntimeError("Cannot set wallclock offset: simulator is terminating")
523
521
  self._wallclock_offset = wallclock_offset
524
522
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nost_tools
3
- Version: 2.2.0
3
+ Version: 2.4.0
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-Expression: BSD-3-Clause
File without changes
File without changes
File without changes
File without changes