nost-tools 2.3.0__tar.gz → 3.0.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.
- {nost_tools-2.3.0 → nost_tools-3.0.0}/PKG-INFO +1 -1
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/__init__.py +2 -2
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/application.py +77 -7
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/configuration.py +0 -2
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/managed_application.py +180 -6
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/manager.py +300 -86
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/observer.py +10 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/publisher.py +11 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/schemas.py +180 -24
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/simulator.py +52 -21
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools.egg-info/PKG-INFO +1 -1
- {nost_tools-2.3.0 → nost_tools-3.0.0}/LICENSE +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/README.md +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/application_utils.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/entity.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/errors.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools/logger_application.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools.egg-info/SOURCES.txt +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools.egg-info/dependency_links.txt +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools.egg-info/requires.txt +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/nost_tools.egg-info/top_level.txt +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/pyproject.toml +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/setup.cfg +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/tests/test_entity.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/tests/test_observer.py +0 -0
- {nost_tools-2.3.0 → nost_tools-3.0.0}/tests/test_simulator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nost_tools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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__ = "
|
|
1
|
+
__version__ = "3.0.0"
|
|
2
2
|
|
|
3
3
|
from .application import Application
|
|
4
4
|
from .application_utils import ConnectionConfig, ModeStatusObserver, TimeStatusPublisher
|
|
@@ -6,7 +6,7 @@ from .configuration import ConnectionConfig
|
|
|
6
6
|
from .entity import Entity
|
|
7
7
|
from .logger_application import LoggerApplication
|
|
8
8
|
from .managed_application import ManagedApplication
|
|
9
|
-
from .manager import Manager
|
|
9
|
+
from .manager import Manager
|
|
10
10
|
from .observer import Observable, Observer
|
|
11
11
|
from .publisher import ScenarioTimeIntervalPublisher, WallclockTimeIntervalPublisher
|
|
12
12
|
from .schemas import (
|
|
@@ -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
|
|
@@ -216,21 +217,21 @@ class Application:
|
|
|
216
217
|
|
|
217
218
|
def refresh_wallclock_periodically():
|
|
218
219
|
while not self._should_stop.wait(
|
|
219
|
-
timeout=self.config.rc.
|
|
220
|
+
timeout=self.config.rc.simulation_configuration.execution_parameters.general.wallclock_offset_refresh_interval
|
|
220
221
|
):
|
|
221
222
|
logger.debug("Wallclock refresh thread is running.")
|
|
222
223
|
try:
|
|
223
|
-
logger.
|
|
224
|
-
f"Contacting {self.config.rc.
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"Contacting {self.config.rc.simulation_configuration.execution_parameters.general.ntp_host} to retrieve wallclock offset."
|
|
225
226
|
)
|
|
226
227
|
response = ntplib.NTPClient().request(
|
|
227
|
-
self.config.rc.
|
|
228
|
+
self.config.rc.simulation_configuration.execution_parameters.general.ntp_host,
|
|
228
229
|
version=3,
|
|
229
230
|
timeout=2,
|
|
230
231
|
)
|
|
231
232
|
offset = timedelta(seconds=response.offset)
|
|
232
233
|
self.simulator.set_wallclock_offset(offset)
|
|
233
|
-
logger.
|
|
234
|
+
logger.debug(f"Wallclock offset updated to {offset}.")
|
|
234
235
|
except Exception as e:
|
|
235
236
|
logger.debug(f"Failed to refresh wallclock offset: {e}")
|
|
236
237
|
|
|
@@ -308,6 +309,17 @@ class Application:
|
|
|
308
309
|
self.shut_down_when_terminated = getattr(
|
|
309
310
|
parameters, "shut_down_when_terminated", shut_down_when_terminated
|
|
310
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
|
+
)
|
|
311
323
|
else:
|
|
312
324
|
logger.warning("No parameters found in configuration, using defaults")
|
|
313
325
|
self.set_offset = set_offset
|
|
@@ -326,7 +338,7 @@ class Application:
|
|
|
326
338
|
if self.set_offset:
|
|
327
339
|
# Start periodic wallclock offset updates instead of one-time call
|
|
328
340
|
logger.info(
|
|
329
|
-
f"Wallclock offset will be set every {self.config.rc.
|
|
341
|
+
f"Wallclock offset will be set every {self.config.rc.simulation_configuration.execution_parameters.general.wallclock_offset_refresh_interval} seconds using {self.config.rc.simulation_configuration.execution_parameters.general.ntp_host}."
|
|
330
342
|
)
|
|
331
343
|
self.start_wallclock_refresh_thread()
|
|
332
344
|
|
|
@@ -1475,9 +1487,67 @@ class Application:
|
|
|
1475
1487
|
|
|
1476
1488
|
def _create_shut_down_observer(self) -> None:
|
|
1477
1489
|
"""
|
|
1478
|
-
Creates
|
|
1490
|
+
Creates a shut down observer to close the application when the simulator is terminated.
|
|
1479
1491
|
"""
|
|
1480
1492
|
if self._shut_down_observer is not None:
|
|
1481
1493
|
self.simulator.remove_observer(self._shut_down_observer)
|
|
1482
1494
|
self._shut_down_observer = ShutDownObserver(self)
|
|
1483
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,7 +21,6 @@ from .schemas import (
|
|
|
21
21
|
RuntimeConfig,
|
|
22
22
|
ServersConfig,
|
|
23
23
|
SimulationConfig,
|
|
24
|
-
WallclockOffsetProperties,
|
|
25
24
|
)
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger(__name__)
|
|
@@ -329,7 +328,6 @@ class ConnectionConfig:
|
|
|
329
328
|
self.load_environment_variables()
|
|
330
329
|
|
|
331
330
|
self.rc = RuntimeConfig(
|
|
332
|
-
wallclock_offset_properties=WallclockOffsetProperties(),
|
|
333
331
|
credentials=self.credentials_config,
|
|
334
332
|
server_configuration=server_config,
|
|
335
333
|
simulation_configuration=self.simulation_config,
|
|
@@ -4,12 +4,25 @@ Provides a base application that manages communication between a simulator and b
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import threading
|
|
7
|
+
import time
|
|
7
8
|
import traceback
|
|
8
9
|
from datetime import datetime, timedelta
|
|
9
10
|
|
|
11
|
+
from nost_tools.simulator import Mode
|
|
12
|
+
|
|
10
13
|
from .application import Application
|
|
11
14
|
from .application_utils import ConnectionConfig
|
|
12
|
-
from .schemas import
|
|
15
|
+
from .schemas import (
|
|
16
|
+
FreezeCommand,
|
|
17
|
+
FreezeRequest,
|
|
18
|
+
InitCommand,
|
|
19
|
+
ResumeCommand,
|
|
20
|
+
ResumeRequest,
|
|
21
|
+
StartCommand,
|
|
22
|
+
StopCommand,
|
|
23
|
+
UpdateCommand,
|
|
24
|
+
UpdateRequest,
|
|
25
|
+
)
|
|
13
26
|
|
|
14
27
|
logger = logging.getLogger(__name__)
|
|
15
28
|
|
|
@@ -143,6 +156,17 @@ class ManagedApplication(Application):
|
|
|
143
156
|
app_topic="update",
|
|
144
157
|
user_callback=self.on_manager_update,
|
|
145
158
|
)
|
|
159
|
+
self.add_message_callback(
|
|
160
|
+
app_name=self.manager_app_name,
|
|
161
|
+
app_topic="freeze",
|
|
162
|
+
user_callback=self.on_manager_freeze,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.add_message_callback(
|
|
166
|
+
app_name=self.manager_app_name,
|
|
167
|
+
app_topic="resume",
|
|
168
|
+
user_callback=self.on_manager_resume,
|
|
169
|
+
)
|
|
146
170
|
|
|
147
171
|
def shut_down(self) -> None:
|
|
148
172
|
"""
|
|
@@ -259,16 +283,166 @@ class ManagedApplication(Application):
|
|
|
259
283
|
body (bytes): The actual message body sent, containing the message payload.
|
|
260
284
|
"""
|
|
261
285
|
try:
|
|
262
|
-
# Parse message payload
|
|
263
286
|
message = body.decode("utf-8")
|
|
264
|
-
|
|
287
|
+
update_cmd = UpdateCommand.model_validate_json(message)
|
|
288
|
+
params = update_cmd.tasking_parameters
|
|
289
|
+
tcf = params.time_scaling_factor
|
|
290
|
+
sim_epoch = params.sim_update_time
|
|
291
|
+
|
|
265
292
|
logger.info(f"Received update command {message}")
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
293
|
+
|
|
294
|
+
def _apply_when_executing():
|
|
295
|
+
while self.simulator.get_mode() != Mode.EXECUTING:
|
|
296
|
+
time.sleep(0.01)
|
|
297
|
+
# Apply update once executing
|
|
298
|
+
self.simulator.set_time_scale_factor(tcf, sim_epoch)
|
|
299
|
+
|
|
300
|
+
if self.simulator.get_mode() != Mode.EXECUTING:
|
|
301
|
+
logger.debug("Deferring time scale update until EXECUTING")
|
|
302
|
+
threading.Thread(target=_apply_when_executing, daemon=True).start()
|
|
303
|
+
else:
|
|
304
|
+
self.simulator.set_time_scale_factor(tcf, sim_epoch)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(
|
|
307
|
+
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
269
308
|
)
|
|
309
|
+
print(traceback.format_exc())
|
|
310
|
+
|
|
311
|
+
def on_manager_freeze(self, ch, method, properties, body) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Callback function for the managed application ('self') to respond to a freeze command sent from the manager.
|
|
314
|
+
Parses the freeze duration and simulation freeze time and updates the simulator.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
318
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
319
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
320
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
# Parse message payload
|
|
324
|
+
message = body.decode("utf-8")
|
|
325
|
+
params = FreezeCommand.model_validate_json(message).tasking_parameters
|
|
326
|
+
logger.info(f"Received freeze command {message}")
|
|
327
|
+
# freeze simulation time
|
|
328
|
+
self.simulator.pause()
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(
|
|
332
|
+
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
333
|
+
)
|
|
334
|
+
print(traceback.format_exc())
|
|
335
|
+
|
|
336
|
+
def on_manager_resume(self, ch, method, properties, body) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Callback function for the managed application ('self') to respond to a resume command sent from the manager.
|
|
339
|
+
Resumes the simulator execution.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
343
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
344
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
345
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
# Parse message payload
|
|
349
|
+
message = body.decode("utf-8")
|
|
350
|
+
params = ResumeCommand.model_validate_json(message).tasking_parameters
|
|
351
|
+
logger.info(f"Received resume command {message}")
|
|
352
|
+
# resume simulation time
|
|
353
|
+
self.simulator.resume()
|
|
270
354
|
except Exception as e:
|
|
271
355
|
logger.error(
|
|
272
356
|
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
273
357
|
)
|
|
274
358
|
print(traceback.format_exc())
|
|
359
|
+
|
|
360
|
+
def request_freeze(
|
|
361
|
+
self, freeze_duration: timedelta = None, sim_freeze_time: datetime = None
|
|
362
|
+
) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Request a freeze from the manager.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
freeze_duration (:obj:`timedelta`, optional): Duration for which to freeze execution.
|
|
368
|
+
If None, creates an indefinite freeze.
|
|
369
|
+
sim_freeze_time (:obj:`datetime`, optional): Scenario time at which to freeze execution.
|
|
370
|
+
If None, freezes immediately.
|
|
371
|
+
"""
|
|
372
|
+
# Publish a freeze request message
|
|
373
|
+
wallclock_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
374
|
+
sim_freeze_time
|
|
375
|
+
)
|
|
376
|
+
request_params = {
|
|
377
|
+
"simFreezeTime": sim_freeze_time,
|
|
378
|
+
"freezeTime": wallclock_time,
|
|
379
|
+
"requestingApp": self.app_name,
|
|
380
|
+
}
|
|
381
|
+
if freeze_duration is not None:
|
|
382
|
+
request_params["freezeDuration"] = freeze_duration
|
|
383
|
+
request_params["resumeTime"] = wallclock_time + freeze_duration
|
|
384
|
+
# Create the freeze request
|
|
385
|
+
request = FreezeRequest.model_validate({"taskingParameters": request_params})
|
|
386
|
+
freeze_type = (
|
|
387
|
+
"indefinite" if freeze_duration is None else f"timed ({freeze_duration})"
|
|
388
|
+
)
|
|
389
|
+
logger.info(
|
|
390
|
+
f"Requesting {freeze_type} freeze: {request.model_dump_json(by_alias=True)}"
|
|
391
|
+
)
|
|
392
|
+
# Send the request to the manager
|
|
393
|
+
self.send_message(
|
|
394
|
+
app_name=self.app_name,
|
|
395
|
+
app_topics="request.freeze",
|
|
396
|
+
payload=request.model_dump_json(by_alias=True),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def request_resume(self) -> None:
|
|
400
|
+
"""
|
|
401
|
+
Request a resume from the manager.
|
|
402
|
+
"""
|
|
403
|
+
# Create the resume request
|
|
404
|
+
request = ResumeRequest.model_validate(
|
|
405
|
+
{"taskingParameters": {"requestingApp": self.app_name}}
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
logger.info(f"Requesting resume: {request.model_dump_json(by_alias=True)}")
|
|
409
|
+
|
|
410
|
+
# Send the request to the manager
|
|
411
|
+
self.send_message(
|
|
412
|
+
app_name=self.app_name,
|
|
413
|
+
app_topics="request.resume",
|
|
414
|
+
payload=request.model_dump_json(by_alias=True),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def request_update(
|
|
418
|
+
self, time_scale_factor: float, sim_update_time: datetime = None
|
|
419
|
+
) -> None:
|
|
420
|
+
"""
|
|
421
|
+
Request a time scale factor update from the manager.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
time_scale_factor (float): scenario seconds per wallclock second
|
|
425
|
+
sim_update_time (:obj:`datetime`, optional): Scenario time at which to update.
|
|
426
|
+
If None, updates immediately.
|
|
427
|
+
"""
|
|
428
|
+
# Publish an update request message
|
|
429
|
+
request_params = {
|
|
430
|
+
"timeScalingFactor": time_scale_factor,
|
|
431
|
+
"requestingApp": self.app_name,
|
|
432
|
+
}
|
|
433
|
+
if sim_update_time is not None:
|
|
434
|
+
request_params["simUpdateTime"] = sim_update_time
|
|
435
|
+
|
|
436
|
+
# Create the update request
|
|
437
|
+
request = UpdateRequest.model_validate({"taskingParameters": request_params})
|
|
438
|
+
|
|
439
|
+
logger.info(
|
|
440
|
+
f"Requesting time scale factor update to {time_scale_factor}: {request.model_dump_json(by_alias=True)}"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Send the request to the manager
|
|
444
|
+
self.send_message(
|
|
445
|
+
app_name=self.app_name,
|
|
446
|
+
app_topics="request.update",
|
|
447
|
+
payload=request.model_dump_json(by_alias=True),
|
|
448
|
+
)
|