nost-tools 2.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.0"
1
+ __version__ = "2.3.0"
2
2
 
3
3
  from .application import Application
4
4
  from .application_utils import ConnectionConfig, ModeStatusObserver, TimeStatusPublisher
nost_tools/application.py CHANGED
@@ -48,13 +48,19 @@ class Application:
48
48
  time_status_init (:obj:`datetime`): Scenario time of first time status message
49
49
  """
50
50
 
51
- def __init__(self, app_name: str, app_description: str = None):
51
+ def __init__(
52
+ self,
53
+ app_name: str,
54
+ app_description: str = None,
55
+ setup_signal_handlers: bool = True,
56
+ ):
52
57
  """
53
58
  Initializes a new application.
54
59
 
55
60
  Args:
56
61
  app_name (str): application name
57
62
  app_description (str): application description (optional)
63
+ setup_signal_handlers (bool): whether to set up signal handlers (default: True)
58
64
  """
59
65
  self.simulator = Simulator()
60
66
  self.connection = None
@@ -85,8 +91,12 @@ class Application:
85
91
  self._token_refresh_thread = None
86
92
  self.token_refresh_interval = None
87
93
  self._reconnect_delay = None
94
+ # Offset
95
+ self._wallclock_refresh_thread = None
96
+ self.wallclock_offset_refresh_interval = None
88
97
  # Set up signal handlers for graceful shutdown
89
- self._setup_signal_handlers()
98
+ if setup_signal_handlers:
99
+ self._setup_signal_handlers()
90
100
 
91
101
  def _setup_signal_handlers(self):
92
102
  """
@@ -194,6 +204,42 @@ class Application:
194
204
  self._token_refresh_thread.start()
195
205
  logger.debug("Starting refresh token thread successfully completed.")
196
206
 
207
+ def start_wallclock_refresh_thread(self): # , interval=30, host="pool.ntp.org"):
208
+ """
209
+ Starts a background thread to refresh the wallclock offset periodically.
210
+
211
+ Args:
212
+ interval (int): Seconds between wallclock offset refreshes (default: 3600 seconds/1 hour)
213
+ host (str): NTP host to query (default: 'pool.ntp.org')
214
+ """
215
+ logger.debug("Starting wallclock offset refresh thread.")
216
+
217
+ def refresh_wallclock_periodically():
218
+ while not self._should_stop.wait(
219
+ timeout=self.config.rc.wallclock_offset_properties.wallclock_offset_refresh_interval
220
+ ):
221
+ logger.debug("Wallclock refresh thread is running.")
222
+ try:
223
+ logger.info(
224
+ f"Contacting {self.config.rc.wallclock_offset_properties.ntp_host} to retrieve wallclock offset."
225
+ )
226
+ response = ntplib.NTPClient().request(
227
+ self.config.rc.wallclock_offset_properties.ntp_host,
228
+ version=3,
229
+ timeout=2,
230
+ )
231
+ offset = timedelta(seconds=response.offset)
232
+ self.simulator.set_wallclock_offset(offset)
233
+ logger.info(f"Wallclock offset updated to {offset}.")
234
+ except Exception as e:
235
+ logger.debug(f"Failed to refresh wallclock offset: {e}")
236
+
237
+ self._wallclock_refresh_thread = threading.Thread(
238
+ target=refresh_wallclock_periodically
239
+ )
240
+ self._wallclock_refresh_thread.start()
241
+ logger.debug("Starting wallclock offset refresh thread successfully completed.")
242
+
197
243
  def update_connection_credentials(self, access_token):
198
244
  """
199
245
  Updates the connection credentials with the new access token.
@@ -203,14 +249,33 @@ class Application:
203
249
  """
204
250
  self.connection.update_secret(access_token, "secret")
205
251
 
252
+ def _get_parameters_from_config(self):
253
+ """
254
+ Gets application parameters from configuration or returns None if not available.
255
+ This method can be overridden by subclasses to customize parameter retrieval.
256
+
257
+ Returns:
258
+ object: Configuration parameters object or None
259
+ """
260
+ if self.config and self.config.rc.yaml_file:
261
+ try:
262
+ return getattr(
263
+ self.config.rc.simulation_configuration.execution_parameters,
264
+ "application",
265
+ None,
266
+ )
267
+ except (AttributeError, KeyError):
268
+ return None
269
+ return None
270
+
206
271
  def start_up(
207
272
  self,
208
273
  prefix: str,
209
274
  config: ConnectionConfig,
210
- set_offset: bool = None, # True,
275
+ set_offset: bool = True,
211
276
  time_status_step: timedelta = None,
212
277
  time_status_init: datetime = None,
213
- shut_down_when_terminated: bool = None,
278
+ shut_down_when_terminated: bool = False,
214
279
  ) -> None:
215
280
  """
216
281
  Starts up the application to prepare for scenario execution.
@@ -225,31 +290,45 @@ class Application:
225
290
  time_status_init (:obj:`datetime`): scenario time for first time status message
226
291
  shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
227
292
  """
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
- ):
293
+ self.config = config
294
+
295
+ if self.config.rc.yaml_file:
296
+ logger.info(
297
+ f"Collecting start up parameters from YAML configuration file: {self.config.rc.yaml_file}"
298
+ )
299
+ parameters = self._get_parameters_from_config()
300
+ if parameters:
301
+ self.set_offset = getattr(parameters, "set_offset", set_offset)
302
+ self.time_status_step = getattr(
303
+ parameters, "time_status_step", time_status_step
304
+ )
305
+ self.time_status_init = getattr(
306
+ parameters, "time_status_init", time_status_init
307
+ )
308
+ self.shut_down_when_terminated = getattr(
309
+ parameters, "shut_down_when_terminated", shut_down_when_terminated
310
+ )
311
+ else:
312
+ logger.warning("No parameters found in configuration, using defaults")
313
+ self.set_offset = set_offset
314
+ self.time_status_step = time_status_step
315
+ self.time_status_init = time_status_init
316
+ self.shut_down_when_terminated = shut_down_when_terminated
317
+ else:
318
+ logger.info(
319
+ f"Collecting start up parameters from user input or default values."
320
+ )
234
321
  self.set_offset = set_offset
235
322
  self.time_status_step = time_status_step
236
323
  self.time_status_init = time_status_init
237
324
  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
325
 
250
326
  if self.set_offset:
251
- # Set the system clock offset
252
- self.set_wallclock_offset()
327
+ # Start periodic wallclock offset updates instead of one-time call
328
+ logger.info(
329
+ 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}."
330
+ )
331
+ self.start_wallclock_refresh_thread()
253
332
 
254
333
  # Set the prefix and configuration parameters
255
334
  self.prefix = prefix
@@ -290,7 +369,6 @@ class Application:
290
369
  locale=config.rc.server_configuration.servers.rabbitmq.locale,
291
370
  blocked_connection_timeout=config.rc.server_configuration.servers.rabbitmq.blocked_connection_timeout,
292
371
  )
293
- logger.info(parameters)
294
372
 
295
373
  # Configure transport layer security (TLS) if needed
296
374
  if self.config.rc.server_configuration.servers.rabbitmq.tls:
@@ -1348,34 +1426,26 @@ class Application:
1348
1426
  )
1349
1427
  else:
1350
1428
  logger.info("Closing token refresh thread completed successfully")
1351
-
1429
+ # Also stop wallclock refresh thread if it exists
1430
+ if (
1431
+ hasattr(self, "_wallclock_refresh_thread")
1432
+ and self._wallclock_refresh_thread
1433
+ and self._wallclock_refresh_thread.is_alive()
1434
+ ):
1435
+ logger.info("Closing wallclock refresh thread.")
1436
+ # Set a timeout to avoid hanging indefinitely
1437
+ self._wallclock_refresh_thread.join(timeout=60.0)
1438
+ # Check if it's still alive after timeout
1439
+ if self._wallclock_refresh_thread.is_alive():
1440
+ logger.warning(
1441
+ "Closing wallclock refresh thread timed out after 60 seconds. "
1442
+ )
1443
+ else:
1444
+ logger.info(
1445
+ "Closing wallclock refresh thread completed successfully"
1446
+ )
1352
1447
  logger.debug("Stop_application completed successfully.")
1353
1448
 
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
1449
  def _create_time_status_publisher(
1380
1450
  self, time_status_step: timedelta, time_status_init: datetime
1381
1451
  ) -> None:
@@ -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(
nost_tools/manager.py CHANGED
@@ -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
  """
nost_tools/schemas.py CHANGED
@@ -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):
@@ -352,6 +365,10 @@ class ManagerConfig(BaseModel):
352
365
  shut_down_when_terminated: bool = Field(
353
366
  False, description="Shut down when terminated."
354
367
  )
368
+ is_scenario_time_step: bool = Field(
369
+ True,
370
+ 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.",
371
+ )
355
372
  is_scenario_time_status_step: bool = Field(
356
373
  True,
357
374
  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 +378,16 @@ class ManagerConfig(BaseModel):
361
378
  def scale_time(cls, values):
362
379
  time_scale_factor = values.get("time_scale_factor", 1.0)
363
380
 
381
+ if "time_step" in values and not values.get("is_scenario_time_step", True):
382
+ time_step = values["time_step"]
383
+ if isinstance(time_step, str):
384
+ hours, minutes, seconds = map(int, time_step.split(":"))
385
+ time_step = timedelta(hours=hours, minutes=minutes, seconds=seconds)
386
+ if isinstance(time_step, timedelta):
387
+ values["time_step"] = timedelta(
388
+ seconds=time_step.total_seconds() * time_scale_factor
389
+ )
390
+
364
391
  if "time_status_step" in values and not values.get(
365
392
  "is_scenario_time_status_step", True
366
393
  ):
@@ -440,7 +467,7 @@ class LoggerApplicationConfig(BaseModel):
440
467
  timedelta(seconds=10), description="Time status step."
441
468
  )
442
469
  time_status_init: Optional[datetime] = Field(
443
- datetime(2019, 3, 1, 0, 0, 0), description="Time status init."
470
+ datetime.now(), description="Time status init."
444
471
  )
445
472
  shut_down_when_terminated: Optional[bool] = Field(
446
473
  False, description="Shut down when terminated."
@@ -471,7 +498,9 @@ class ApplicationConfig(BaseModel):
471
498
 
472
499
 
473
500
  class ExecConfig(BaseModel):
474
- general: GeneralConfig
501
+ general: Optional[GeneralConfig] = Field(
502
+ None, description="General configuration for the execution."
503
+ )
475
504
  manager: Optional[ManagerConfig] = Field(None, description="Manager configuration.")
476
505
  managed_applications: Optional[Dict[str, ManagedApplicationConfig]] = Field(
477
506
  default_factory=lambda: {"default": ManagedApplicationConfig()},
@@ -509,11 +538,11 @@ class ChannelConfig(BaseModel):
509
538
 
510
539
 
511
540
  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.")
541
+ username: Optional[str] = Field("admin", description="Username for authentication.")
542
+ password: Optional[str] = Field("admin", description="Password for authentication.")
543
+ client_id: Optional[str] = Field(None, description="Client ID for authentication.")
515
544
  client_secret_key: Optional[str] = Field(
516
- "", description="Client secret key for authentication."
545
+ None, description="Client secret key for authentication."
517
546
  )
518
547
 
519
548
 
@@ -529,6 +558,9 @@ class SimulationConfig(BaseModel):
529
558
 
530
559
 
531
560
  class RuntimeConfig(BaseModel):
561
+ wallclock_offset_properties: WallclockOffsetProperties = Field(
562
+ ..., description="Properties for wallclock offset."
563
+ )
532
564
  credentials: Credentials = Field(..., description="Credentials for authentication.")
533
565
  server_configuration: Config = (
534
566
  Field(..., description="Simulation configuration."),
@@ -539,3 +571,6 @@ class RuntimeConfig(BaseModel):
539
571
  application_configuration: Optional[Dict] = Field(
540
572
  None, description="Application-specific, user-provided configuration."
541
573
  )
574
+ yaml_file: Optional[str] = Field(
575
+ None, description="Path to the YAML file containing the configuration."
576
+ )
nost_tools/simulator.py CHANGED
@@ -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.3.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
@@ -0,0 +1,18 @@
1
+ nost_tools/__init__.py,sha256=5DeYQxWXibC9y__BvjwL8YC8K4EYMZbAV6E75k5nuL8,870
2
+ nost_tools/application.py,sha256=gh48fokVifuhKyAoJylZ2mF_WHW90G9L8j99Ct7j2Pg,64257
3
+ nost_tools/application_utils.py,sha256=jiMzuuP6-47UlUO64HhwNvbl6uKvVnsksYgOw7CmxL4,9327
4
+ nost_tools/configuration.py,sha256=4WLs1BrHMMvVhSIpJfjVZe-zw04WygAzjiLX2pVXibY,13146
5
+ nost_tools/entity.py,sha256=JrSN5rz7h-N9zIQsaJOQVBIYPDfygacedks55fsq_QQ,2802
6
+ nost_tools/errors.py,sha256=0JcDlMEkZAya3-5c0rRozLuxp8qF58StG4JgRsaxfKU,344
7
+ nost_tools/logger_application.py,sha256=rxPBfyA7Zym5b_EsoSJvT9JWNIVWZX1a-4czFwCqaQ4,7217
8
+ nost_tools/managed_application.py,sha256=Xa6qGsNVrr_XNnpJt_-L5PVnpeUy7GXIf0p2aEI4dSE,11673
9
+ nost_tools/manager.py,sha256=KqFyE-vv9dPRuVdz_r1SAsPeWt9LwKOTF-a2cJId0rY,24321
10
+ nost_tools/observer.py,sha256=D64V0KTvHRPEqbB8q3BosJhoAlpBah2vyBlVbxWQR44,8161
11
+ nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
12
+ nost_tools/schemas.py,sha256=RZH9LCWSZT8hGteI_Cc5a_w_TyFYx0Kh5mBM02SObrk,20575
13
+ nost_tools/simulator.py,sha256=pWfMSarMCuInQTlvlJ5l53w5ZZP6jjyUtY8uWOkbe-4,20062
14
+ nost_tools-2.3.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
15
+ nost_tools-2.3.0.dist-info/METADATA,sha256=6ovwTQnDSv0ODsIsMQcFI5-kN652WW8FYOYlwzg-SH8,4256
16
+ nost_tools-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ nost_tools-2.3.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
18
+ nost_tools-2.3.0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- nost_tools/__init__.py,sha256=4AGyjwpOcyyQSwtFNemFAQYyZyoRUJbH5dEkJ75xxCI,870
2
- nost_tools/application.py,sha256=1msGzjDbPNY5Bv5OPu0pUctBEpXVBCkfBzjpF8G6skg,60914
3
- nost_tools/application_utils.py,sha256=jiMzuuP6-47UlUO64HhwNvbl6uKvVnsksYgOw7CmxL4,9327
4
- nost_tools/configuration.py,sha256=DBsISmhtMiWUw2PC4CfvIcfPOh_ayujFt5gif_2PvOI,12852
5
- nost_tools/entity.py,sha256=JrSN5rz7h-N9zIQsaJOQVBIYPDfygacedks55fsq_QQ,2802
6
- nost_tools/errors.py,sha256=0JcDlMEkZAya3-5c0rRozLuxp8qF58StG4JgRsaxfKU,344
7
- nost_tools/logger_application.py,sha256=rxPBfyA7Zym5b_EsoSJvT9JWNIVWZX1a-4czFwCqaQ4,7217
8
- nost_tools/managed_application.py,sha256=7393kCF9FeXlf_pSP_C-tAeNSW9_-smQH6pQgyHC6Cw,11576
9
- nost_tools/manager.py,sha256=SrGu3TX8cQnvSPzJC5W5izZPJe9beQPQIKyT7PHwepQ,24485
10
- nost_tools/observer.py,sha256=D64V0KTvHRPEqbB8q3BosJhoAlpBah2vyBlVbxWQR44,8161
11
- nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
12
- nost_tools/schemas.py,sha256=VAwHuIxVVbf-Te_CWkZ6iNvJwRvr9PYJRRD2dt0nMJ4,19068
13
- nost_tools/simulator.py,sha256=ALnGDmnA_ga-1Lq-bVWi2vcrspgjS4vtuDE0jWsI7fE,20191
14
- nost_tools-2.2.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
15
- nost_tools-2.2.0.dist-info/METADATA,sha256=Mvx484RUws2yo8PdAZPnCIgx0l2ssODBeJkQucB-Xuk,4256
16
- nost_tools-2.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- nost_tools-2.2.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
18
- nost_tools-2.2.0.dist-info/RECORD,,