nost-tools 2.3.0__py3-none-any.whl → 3.0.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 +2 -2
- nost_tools/application.py +77 -7
- nost_tools/configuration.py +0 -2
- nost_tools/managed_application.py +180 -6
- nost_tools/manager.py +300 -86
- nost_tools/observer.py +10 -0
- nost_tools/publisher.py +11 -0
- nost_tools/schemas.py +180 -24
- nost_tools/simulator.py +52 -21
- {nost_tools-2.3.0.dist-info → nost_tools-3.0.0.dist-info}/METADATA +1 -1
- nost_tools-3.0.0.dist-info/RECORD +18 -0
- nost_tools-2.3.0.dist-info/RECORD +0 -18
- {nost_tools-2.3.0.dist-info → nost_tools-3.0.0.dist-info}/WHEEL +0 -0
- {nost_tools-2.3.0.dist-info → nost_tools-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {nost_tools-2.3.0.dist-info → nost_tools-3.0.0.dist-info}/top_level.txt +0 -0
nost_tools/__init__.py
CHANGED
|
@@ -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 (
|
nost_tools/application.py
CHANGED
|
@@ -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}")
|
nost_tools/configuration.py
CHANGED
|
@@ -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
|
+
)
|
nost_tools/manager.py
CHANGED
|
@@ -15,40 +15,23 @@ from pydantic import ValidationError
|
|
|
15
15
|
from .application import Application
|
|
16
16
|
from .application_utils import ConnectionConfig
|
|
17
17
|
from .schemas import (
|
|
18
|
+
FreezeCommand,
|
|
19
|
+
FreezeRequest,
|
|
18
20
|
InitCommand,
|
|
19
21
|
ReadyStatus,
|
|
22
|
+
ResumeCommand,
|
|
23
|
+
ResumeRequest,
|
|
20
24
|
StartCommand,
|
|
21
25
|
StopCommand,
|
|
22
26
|
TimeStatus,
|
|
23
27
|
UpdateCommand,
|
|
28
|
+
UpdateRequest,
|
|
24
29
|
)
|
|
25
30
|
from .simulator import Mode
|
|
26
31
|
|
|
27
32
|
logger = logging.getLogger(__name__)
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
class TimeScaleUpdate(object):
|
|
31
|
-
"""
|
|
32
|
-
Provides a scheduled update to the simulation time scale factor by sending a message at the designated sim_update_time
|
|
33
|
-
to change the time_scale_factor to the indicated value.
|
|
34
|
-
|
|
35
|
-
Attributes:
|
|
36
|
-
time_scale_factor (float): scenario seconds per wallclock second
|
|
37
|
-
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(self, time_scale_factor: float, sim_update_time: datetime):
|
|
41
|
-
"""
|
|
42
|
-
Instantiates a new time scale update.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
time_scale_factor (float): scenario seconds per wallclock second
|
|
46
|
-
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
47
|
-
"""
|
|
48
|
-
self.time_scale_factor = time_scale_factor
|
|
49
|
-
self.sim_update_time = sim_update_time
|
|
50
|
-
|
|
51
|
-
|
|
52
35
|
class Manager(Application):
|
|
53
36
|
"""
|
|
54
37
|
NOS-T Manager Application.
|
|
@@ -84,19 +67,17 @@ class Manager(Application):
|
|
|
84
67
|
app_name, app_description, setup_signal_handlers=setup_signal_handlers
|
|
85
68
|
)
|
|
86
69
|
self.required_apps_status = {}
|
|
87
|
-
|
|
88
70
|
self.sim_start_time = None
|
|
89
71
|
self.sim_stop_time = None
|
|
90
|
-
start_time = None
|
|
91
|
-
time_step = None
|
|
92
|
-
time_scale_factor = None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
init_max_retry = None
|
|
72
|
+
self.start_time = None
|
|
73
|
+
self.time_step = None
|
|
74
|
+
self.time_scale_factor = None
|
|
75
|
+
self.time_status_step = None
|
|
76
|
+
self.time_status_init = None
|
|
77
|
+
self.command_lead = None
|
|
78
|
+
self.required_apps = None
|
|
79
|
+
self.init_retry_delay_s = None
|
|
80
|
+
self.init_max_retry = None
|
|
100
81
|
|
|
101
82
|
def establish_exchange(self):
|
|
102
83
|
"""
|
|
@@ -190,8 +171,147 @@ class Manager(Application):
|
|
|
190
171
|
shut_down_when_terminated,
|
|
191
172
|
)
|
|
192
173
|
|
|
193
|
-
#
|
|
174
|
+
# Establish the RabbitMQ exchange
|
|
194
175
|
self.establish_exchange()
|
|
176
|
+
# Register callbacks for freeze, resume, and update requests from managed applications
|
|
177
|
+
self.add_message_callback("*", "request.freeze", self.on_freeze_request)
|
|
178
|
+
self.add_message_callback("*", "request.resume", self.on_resume_request)
|
|
179
|
+
self.add_message_callback("*", "request.update", self.on_update_request)
|
|
180
|
+
|
|
181
|
+
def on_freeze_request(self, ch, method, properties, body) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Callback to handle freeze requests from managed applications.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
187
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
188
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
189
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
# Parse the freeze request
|
|
193
|
+
message = body.decode("utf-8")
|
|
194
|
+
freeze_request = FreezeRequest.model_validate_json(message)
|
|
195
|
+
params = freeze_request.tasking_parameters
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
f"Received freeze request from {params.requesting_app}: {message}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Use a separate thread to handle the freeze to avoid blocking the callback
|
|
202
|
+
freeze_thread = threading.Thread(
|
|
203
|
+
target=self._handle_freeze_request,
|
|
204
|
+
args=(
|
|
205
|
+
params.freeze_duration,
|
|
206
|
+
params.sim_freeze_time,
|
|
207
|
+
params.resume_time,
|
|
208
|
+
),
|
|
209
|
+
daemon=True,
|
|
210
|
+
)
|
|
211
|
+
freeze_thread.start()
|
|
212
|
+
|
|
213
|
+
except ValidationError as e:
|
|
214
|
+
logger.error(f"Validation error in freeze request: {e}")
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(
|
|
217
|
+
f"Exception handling freeze request (topic: {method.routing_key}, payload: {message}): {e}"
|
|
218
|
+
)
|
|
219
|
+
print(traceback.format_exc())
|
|
220
|
+
|
|
221
|
+
def _handle_freeze_request(
|
|
222
|
+
self,
|
|
223
|
+
freeze_duration: timedelta = None,
|
|
224
|
+
sim_freeze_time: datetime = None,
|
|
225
|
+
resume_time: datetime = None,
|
|
226
|
+
) -> None:
|
|
227
|
+
try:
|
|
228
|
+
if freeze_duration is not None:
|
|
229
|
+
self.freeze(freeze_duration, sim_freeze_time, resume_time)
|
|
230
|
+
# Only resume if we are still paused and not terminating
|
|
231
|
+
if self.simulator.get_mode() == Mode.PAUSED:
|
|
232
|
+
self.resume()
|
|
233
|
+
else:
|
|
234
|
+
self.freeze(None, sim_freeze_time, None)
|
|
235
|
+
logger.info("Indefinite freeze requested - manual resume required")
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Error handling freeze request: {e}")
|
|
239
|
+
print(traceback.format_exc())
|
|
240
|
+
|
|
241
|
+
def on_resume_request(self, ch, method, properties, body) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Callback to handle resume requests from managed applications.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
247
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
248
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
249
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
# Parse the resume request
|
|
253
|
+
message = body.decode("utf-8")
|
|
254
|
+
resume_request = ResumeRequest.model_validate_json(message)
|
|
255
|
+
params = resume_request.tasking_parameters
|
|
256
|
+
logger.info(
|
|
257
|
+
f"Received resume request from {params.requesting_app}: {message}"
|
|
258
|
+
)
|
|
259
|
+
# Execute the resume command
|
|
260
|
+
self.resume()
|
|
261
|
+
|
|
262
|
+
except ValidationError as e:
|
|
263
|
+
logger.error(f"Validation error in resume request: {e}")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(
|
|
266
|
+
f"Exception handling resume request (topic: {method.routing_key}, payload: {message}): {e}"
|
|
267
|
+
)
|
|
268
|
+
print(traceback.format_exc())
|
|
269
|
+
|
|
270
|
+
def on_update_request(self, ch, method, properties, body) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Callback to handle update requests from managed applications.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
276
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
277
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
278
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
message = body.decode("utf-8")
|
|
282
|
+
update_request = UpdateRequest.model_validate_json(message)
|
|
283
|
+
params = update_request.tasking_parameters
|
|
284
|
+
logger.info(
|
|
285
|
+
f"Received update request from {params.requesting_app}: {message}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def _apply_update_when_executing():
|
|
289
|
+
while self.simulator.get_mode() != Mode.EXECUTING:
|
|
290
|
+
self._sleep_with_heartbeat(0.01)
|
|
291
|
+
self.update(
|
|
292
|
+
params.time_scale_factor,
|
|
293
|
+
params.sim_update_time or self.simulator.get_time(),
|
|
294
|
+
)
|
|
295
|
+
# Wait until update takes effect
|
|
296
|
+
while (
|
|
297
|
+
self.simulator.get_time_scale_factor() != params.time_scale_factor
|
|
298
|
+
):
|
|
299
|
+
self._sleep_with_heartbeat(0.01)
|
|
300
|
+
|
|
301
|
+
# Defer if not executing yet
|
|
302
|
+
if self.simulator.get_mode() != Mode.EXECUTING:
|
|
303
|
+
threading.Thread(
|
|
304
|
+
target=_apply_update_when_executing, daemon=True
|
|
305
|
+
).start()
|
|
306
|
+
else:
|
|
307
|
+
_apply_update_when_executing()
|
|
308
|
+
except ValidationError as e:
|
|
309
|
+
logger.error(f"Validation error in update request: {e}")
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error(
|
|
312
|
+
f"Exception handling update request (topic: {method.routing_key}, payload: {message}): {e}"
|
|
313
|
+
)
|
|
314
|
+
print(traceback.format_exc())
|
|
195
315
|
|
|
196
316
|
def execute_test_plan(self, *args, **kwargs) -> None:
|
|
197
317
|
"""
|
|
@@ -214,7 +334,6 @@ class Manager(Application):
|
|
|
214
334
|
start_time: datetime = None,
|
|
215
335
|
time_step: timedelta = timedelta(seconds=1),
|
|
216
336
|
time_scale_factor: float = 1.0,
|
|
217
|
-
time_scale_updates: List[TimeScaleUpdate] = [],
|
|
218
337
|
time_status_step: timedelta = None,
|
|
219
338
|
time_status_init: datetime = None,
|
|
220
339
|
command_lead: timedelta = timedelta(seconds=0),
|
|
@@ -234,7 +353,6 @@ class Manager(Application):
|
|
|
234
353
|
start_time (:obj:`datetime`): wallclock time at which to start execution (default: now)
|
|
235
354
|
time_step (:obj:`timedelta`): scenario time step used in execution (default: 1 second)
|
|
236
355
|
time_scale_factor (float): scenario seconds per wallclock second (default: 1.0)
|
|
237
|
-
time_scale_updates (list(:obj:`TimeScaleUpdate`)): list of scheduled time scale updates (default: [])
|
|
238
356
|
time_status_step (:obj:`timedelta`): scenario duration between time status messages
|
|
239
357
|
time_status_init (:obj:`datetime`): scenario time of first time status message
|
|
240
358
|
command_lead (:obj:`timedelta`): wallclock lead time between command and action (default: 0 seconds)
|
|
@@ -243,7 +361,7 @@ class Manager(Application):
|
|
|
243
361
|
init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
|
|
244
362
|
"""
|
|
245
363
|
if self.config.rc.yaml_file:
|
|
246
|
-
logger.
|
|
364
|
+
logger.debug(
|
|
247
365
|
f"Collecting execution parameters from YAML configuration file: {self.config.rc.yaml_file}"
|
|
248
366
|
)
|
|
249
367
|
parameters = getattr(
|
|
@@ -256,7 +374,6 @@ class Manager(Application):
|
|
|
256
374
|
self.start_time = parameters.start_time
|
|
257
375
|
self.time_step = parameters.time_step
|
|
258
376
|
self.time_scale_factor = parameters.time_scale_factor
|
|
259
|
-
self.time_scale_updates = parameters.time_scale_updates
|
|
260
377
|
self.time_status_step = parameters.time_status_step
|
|
261
378
|
self.time_status_init = parameters.time_status_init
|
|
262
379
|
self.command_lead = parameters.command_lead
|
|
@@ -266,7 +383,7 @@ class Manager(Application):
|
|
|
266
383
|
self.init_retry_delay_s = parameters.init_retry_delay_s
|
|
267
384
|
self.init_max_retry = parameters.init_max_retry
|
|
268
385
|
else:
|
|
269
|
-
logger.
|
|
386
|
+
logger.debug(
|
|
270
387
|
f"Collecting execution parameters from user input or default values."
|
|
271
388
|
)
|
|
272
389
|
self.sim_start_time = sim_start_time
|
|
@@ -274,7 +391,6 @@ class Manager(Application):
|
|
|
274
391
|
self.start_time = start_time
|
|
275
392
|
self.time_step = time_step
|
|
276
393
|
self.time_scale_factor = time_scale_factor
|
|
277
|
-
self.time_scale_updates = time_scale_updates
|
|
278
394
|
self.time_status_step = time_status_step
|
|
279
395
|
self.time_status_init = time_status_init
|
|
280
396
|
self.command_lead = command_lead
|
|
@@ -282,17 +398,6 @@ class Manager(Application):
|
|
|
282
398
|
self.init_retry_delay_s = init_retry_delay_s
|
|
283
399
|
self.init_max_retry = init_max_retry
|
|
284
400
|
|
|
285
|
-
# Convert TimeScaleUpdateSchema objects to TimeScaleUpdate objects
|
|
286
|
-
converted_updates = []
|
|
287
|
-
for update_schema in self.time_scale_updates:
|
|
288
|
-
converted_updates.append(
|
|
289
|
-
TimeScaleUpdate(
|
|
290
|
-
time_scale_factor=update_schema.time_scale_factor,
|
|
291
|
-
sim_update_time=update_schema.sim_update_time,
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
self.time_scale_updates = converted_updates
|
|
295
|
-
|
|
296
401
|
# Set up tracking of required applications
|
|
297
402
|
self.required_apps_status = dict(
|
|
298
403
|
zip(self.required_apps, [False] * len(self.required_apps))
|
|
@@ -346,44 +451,22 @@ class Manager(Application):
|
|
|
346
451
|
while self.simulator.get_mode() != Mode.EXECUTING:
|
|
347
452
|
time.sleep(0.001)
|
|
348
453
|
|
|
349
|
-
#
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
# Sleep until update time using heartbeat-safe approach
|
|
355
|
-
sleep_seconds = max(
|
|
356
|
-
0,
|
|
357
|
-
(
|
|
358
|
-
(update_time - self.simulator.get_wallclock_time())
|
|
359
|
-
- self.command_lead
|
|
360
|
-
)
|
|
361
|
-
/ timedelta(seconds=1),
|
|
454
|
+
# Wait for stop time - simulator now handles freeze time internally
|
|
455
|
+
while True:
|
|
456
|
+
end_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
457
|
+
self.simulator.get_end_time()
|
|
362
458
|
)
|
|
459
|
+
current_time = self.simulator.get_wallclock_time()
|
|
460
|
+
time_until_stop = (
|
|
461
|
+
end_time - current_time - self.command_lead
|
|
462
|
+
).total_seconds()
|
|
363
463
|
|
|
364
|
-
|
|
365
|
-
|
|
464
|
+
if time_until_stop <= 0:
|
|
465
|
+
break
|
|
366
466
|
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
# Wait until update takes effect
|
|
371
|
-
while self.simulator.get_time_scale_factor() != update.time_scale_factor:
|
|
372
|
-
time.sleep(0.001)
|
|
373
|
-
|
|
374
|
-
end_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
375
|
-
self.simulator.get_end_time()
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
# Sleep until stop time using heartbeat-safe approach
|
|
379
|
-
sleep_seconds = max(
|
|
380
|
-
0,
|
|
381
|
-
((end_time - self.simulator.get_wallclock_time()) - self.command_lead)
|
|
382
|
-
/ timedelta(seconds=1),
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
# Use our heartbeat-safe sleep
|
|
386
|
-
self._sleep_with_heartbeat(sleep_seconds)
|
|
467
|
+
# Wait for timeout
|
|
468
|
+
timeout = min(30.0, time_until_stop)
|
|
469
|
+
time.sleep(timeout)
|
|
387
470
|
|
|
388
471
|
# Issue the stop command
|
|
389
472
|
self.stop(self.sim_stop_time)
|
|
@@ -601,3 +684,134 @@ class Manager(Application):
|
|
|
601
684
|
)
|
|
602
685
|
# update the execution time scale factor
|
|
603
686
|
self.simulator.set_time_scale_factor(time_scale_factor, sim_update_time)
|
|
687
|
+
|
|
688
|
+
def freeze(
|
|
689
|
+
self,
|
|
690
|
+
freeze_duration: timedelta = None,
|
|
691
|
+
sim_freeze_time: datetime = None,
|
|
692
|
+
resume_time: datetime = None,
|
|
693
|
+
) -> None:
|
|
694
|
+
"""
|
|
695
|
+
Command to freeze a test run execution by updating the execution freeze duration and publishing a freeze command.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
freeze_duration (:obj:`timedelta`, optional): Duration for which to freeze execution.
|
|
699
|
+
If None, creates an indefinite freeze.
|
|
700
|
+
sim_freeze_time (:obj:`datetime`, optional): Scenario time at which to freeze execution.
|
|
701
|
+
If None, freezes immediately.
|
|
702
|
+
"""
|
|
703
|
+
# publish a freeze command message
|
|
704
|
+
command_params = {"simFreezeTime": sim_freeze_time}
|
|
705
|
+
if freeze_duration is not None:
|
|
706
|
+
command_params["freezeDuration"] = freeze_duration
|
|
707
|
+
command = FreezeCommand.model_validate({"taskingParameters": command_params})
|
|
708
|
+
freeze_type = (
|
|
709
|
+
"indefinite" if freeze_duration is None else f"timed ({freeze_duration})"
|
|
710
|
+
)
|
|
711
|
+
logger.info(
|
|
712
|
+
f"Sending {freeze_type} freeze command {command.model_dump_json(by_alias=True)}."
|
|
713
|
+
)
|
|
714
|
+
self.send_message(
|
|
715
|
+
app_name=self.app_name,
|
|
716
|
+
app_topics="freeze",
|
|
717
|
+
payload=command.model_dump_json(by_alias=True),
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# If a future scenario freeze time is specified, wait until that time before pausing
|
|
721
|
+
if sim_freeze_time is not None:
|
|
722
|
+
try:
|
|
723
|
+
if self.simulator.get_time() < sim_freeze_time:
|
|
724
|
+
target_wc = self.simulator.get_wallclock_time_at_simulation_time(
|
|
725
|
+
sim_freeze_time
|
|
726
|
+
)
|
|
727
|
+
delay = (
|
|
728
|
+
target_wc - self.simulator.get_wallclock_time()
|
|
729
|
+
).total_seconds()
|
|
730
|
+
if delay > 0:
|
|
731
|
+
self._sleep_with_heartbeat(delay)
|
|
732
|
+
except Exception as e:
|
|
733
|
+
logger.warning(
|
|
734
|
+
f"Could not align to simFreezeTime={sim_freeze_time}: {e}"
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Freeze simulation time (skip if already paused/pausing)
|
|
738
|
+
if self.simulator.get_mode() not in (Mode.PAUSED, Mode.PAUSING):
|
|
739
|
+
self.simulator.pause()
|
|
740
|
+
# Wait until the simulator is paused to anchor the resume time
|
|
741
|
+
while self.simulator.get_mode() == Mode.PAUSING:
|
|
742
|
+
self._sleep_with_heartbeat(0.01)
|
|
743
|
+
while self.simulator.get_mode() != Mode.PAUSED:
|
|
744
|
+
if self.simulator.get_mode() in (Mode.TERMINATING, Mode.TERMINATED):
|
|
745
|
+
logger.info("Abort freeze wait due to termination")
|
|
746
|
+
return
|
|
747
|
+
self._sleep_with_heartbeat(0.01)
|
|
748
|
+
|
|
749
|
+
# Snapshot wallclock time at the moment of freeze
|
|
750
|
+
base = self.simulator.get_wallclock_time()
|
|
751
|
+
|
|
752
|
+
if freeze_duration is not None:
|
|
753
|
+
# Validate duration
|
|
754
|
+
if freeze_duration.total_seconds() <= 0:
|
|
755
|
+
logger.warning(
|
|
756
|
+
f"Ignoring non-positive freeze duration: {freeze_duration}"
|
|
757
|
+
)
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
# Compute authoritative resume time
|
|
761
|
+
target_resume_time = base + freeze_duration
|
|
762
|
+
# Optionally honor requested resume_time if it's later than base
|
|
763
|
+
if resume_time is not None and resume_time > base:
|
|
764
|
+
# Keep the earlier of the two if you want to minimize drift across nodes,
|
|
765
|
+
# or the later if you prefer never resuming before a requested time.
|
|
766
|
+
# Here we choose the later to avoid early resume vs. a peer's expectation.
|
|
767
|
+
target_resume_time = max(target_resume_time, resume_time)
|
|
768
|
+
|
|
769
|
+
logger.info(
|
|
770
|
+
f"Resume Time: requested={resume_time} calculated={target_resume_time} "
|
|
771
|
+
f"delta={abs((target_resume_time - (resume_time or target_resume_time)).total_seconds())}s"
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Poll until we reach the target, allowing early exit and heartbeats
|
|
775
|
+
while True:
|
|
776
|
+
mode = self.simulator.get_mode()
|
|
777
|
+
if mode in (
|
|
778
|
+
Mode.EXECUTING,
|
|
779
|
+
Mode.RESUMING,
|
|
780
|
+
Mode.TERMINATING,
|
|
781
|
+
Mode.TERMINATED,
|
|
782
|
+
):
|
|
783
|
+
break
|
|
784
|
+
remaining = (
|
|
785
|
+
target_resume_time - self.simulator.get_wallclock_time()
|
|
786
|
+
).total_seconds()
|
|
787
|
+
if remaining <= 0:
|
|
788
|
+
break
|
|
789
|
+
self._sleep_with_heartbeat(min(1.0, max(0.01, remaining)))
|
|
790
|
+
else:
|
|
791
|
+
logger.info("Indefinite freeze active. Call resume() to continue.")
|
|
792
|
+
while self.simulator.get_mode() not in (Mode.EXECUTING, Mode.RESUMING):
|
|
793
|
+
if self.simulator.get_mode() in (Mode.TERMINATING, Mode.TERMINATED):
|
|
794
|
+
break
|
|
795
|
+
self._sleep_with_heartbeat(0.01)
|
|
796
|
+
|
|
797
|
+
def resume(self) -> None:
|
|
798
|
+
"""
|
|
799
|
+
Command to resume a test run execution by unpausing the simulator.
|
|
800
|
+
"""
|
|
801
|
+
# resume the simulator execution
|
|
802
|
+
command = ResumeCommand.model_validate(
|
|
803
|
+
{
|
|
804
|
+
"taskingParameters": {
|
|
805
|
+
"resumeTime": self.simulator.get_wallclock_time(),
|
|
806
|
+
"simResumeTime": self.simulator.get_time(),
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
)
|
|
810
|
+
logger.info(f"Sending resume command {command.model_dump_json(by_alias=True)}.")
|
|
811
|
+
self.send_message(
|
|
812
|
+
app_name=self.app_name,
|
|
813
|
+
app_topics="resume",
|
|
814
|
+
payload=command.model_dump_json(by_alias=True),
|
|
815
|
+
)
|
|
816
|
+
# resume simulation time
|
|
817
|
+
self.simulator.resume()
|
nost_tools/observer.py
CHANGED
|
@@ -247,6 +247,16 @@ class WallclockTimeIntervalCallback(Observer):
|
|
|
247
247
|
):
|
|
248
248
|
from nost_tools.simulator import Mode, Simulator
|
|
249
249
|
|
|
250
|
+
# Reset timing when resuming
|
|
251
|
+
if (
|
|
252
|
+
property_name == Simulator.PROPERTY_MODE
|
|
253
|
+
and old_value == Mode.RESUMING
|
|
254
|
+
and new_value == Mode.EXECUTING
|
|
255
|
+
):
|
|
256
|
+
wallclock_now = self.simulator.get_wallclock_time()
|
|
257
|
+
self._next_time = wallclock_now + (self.time_interval if self.time_interval is not None else timedelta())
|
|
258
|
+
return
|
|
259
|
+
|
|
250
260
|
if property_name == Simulator.PROPERTY_MODE and new_value == Mode.INITIALIZED:
|
|
251
261
|
self._next_time = self.time_init
|
|
252
262
|
elif property_name == Simulator.PROPERTY_TIME:
|
nost_tools/publisher.py
CHANGED
|
@@ -125,6 +125,17 @@ class WallclockTimeIntervalPublisher(Observer, ABC):
|
|
|
125
125
|
old_value (obj): old value of the named property
|
|
126
126
|
new_value (obj): new value of the named property
|
|
127
127
|
"""
|
|
128
|
+
# Reset timing when resuming from pause
|
|
129
|
+
if (
|
|
130
|
+
property_name == Simulator.PROPERTY_MODE
|
|
131
|
+
and old_value == Mode.RESUMING
|
|
132
|
+
and new_value == Mode.EXECUTING
|
|
133
|
+
):
|
|
134
|
+
self._next_time_status = self.app.simulator.get_wallclock_time()
|
|
135
|
+
if self.time_status_step is not None:
|
|
136
|
+
self._next_time_status += self.time_status_step
|
|
137
|
+
return
|
|
138
|
+
|
|
128
139
|
if property_name == Simulator.PROPERTY_MODE and new_value == Mode.INITIALIZED:
|
|
129
140
|
if self.time_status_init is None:
|
|
130
141
|
self._next_time_status = self.app.simulator.get_wallclock_time()
|
nost_tools/schemas.py
CHANGED
|
@@ -132,6 +132,162 @@ class UpdateCommand(BaseModel):
|
|
|
132
132
|
)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
class FreezeTaskingParameters(BaseModel):
|
|
136
|
+
"""
|
|
137
|
+
Tasking parameters to freeze an execution.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
sim_freeze_time: datetime = Field(
|
|
141
|
+
...,
|
|
142
|
+
gt=0,
|
|
143
|
+
description="Scenario time at which to freeze execution.",
|
|
144
|
+
alias="simFreezeTime",
|
|
145
|
+
)
|
|
146
|
+
freeze_duration: Optional[timedelta] = Field(
|
|
147
|
+
None,
|
|
148
|
+
# timedelta(seconds=60),
|
|
149
|
+
description="Wallclock time duration for which to freeze execution.",
|
|
150
|
+
alias="freezeDuration",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class FreezeCommand(BaseModel):
|
|
155
|
+
"""
|
|
156
|
+
Command message to freeze an execution.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
tasking_parameters: FreezeTaskingParameters = Field(
|
|
160
|
+
...,
|
|
161
|
+
description="Tasking parameters for the freeze command.",
|
|
162
|
+
alias="taskingParameters",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ResumeTaskingParameters(BaseModel):
|
|
167
|
+
"""
|
|
168
|
+
Tasking parameters to resume an execution.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
resume_time: datetime = Field(
|
|
172
|
+
...,
|
|
173
|
+
gt=0,
|
|
174
|
+
description="Wallclock time at which to resume execution.",
|
|
175
|
+
alias="resumeTime",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
sim_resume_time: datetime = Field(
|
|
179
|
+
...,
|
|
180
|
+
gt=0,
|
|
181
|
+
description="Scenario time at which to resume execution.",
|
|
182
|
+
alias="simResumeTime",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ResumeCommand(BaseModel):
|
|
187
|
+
"""
|
|
188
|
+
Command message to resume an execution.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
tasking_parameters: ResumeTaskingParameters = Field(
|
|
192
|
+
...,
|
|
193
|
+
description="Tasking parameters for the resume command.",
|
|
194
|
+
alias="taskingParameters",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class FreezeRequestParameters(BaseModel):
|
|
199
|
+
"""
|
|
200
|
+
Parameters for requesting a freeze from a managed application.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
sim_freeze_time: datetime = Field(
|
|
204
|
+
...,
|
|
205
|
+
gt=0,
|
|
206
|
+
description="Scenario time at which to freeze execution.",
|
|
207
|
+
alias="simFreezeTime",
|
|
208
|
+
)
|
|
209
|
+
freezeTime: datetime = Field(
|
|
210
|
+
...,
|
|
211
|
+
description="Wallclock time at which to freeze execution.",
|
|
212
|
+
alias="freezeTime",
|
|
213
|
+
)
|
|
214
|
+
freeze_duration: Optional[timedelta] = Field(
|
|
215
|
+
None,
|
|
216
|
+
description="Wallclock time duration for which to freeze execution.",
|
|
217
|
+
alias="freezeDuration",
|
|
218
|
+
)
|
|
219
|
+
resume_time: Optional[datetime] = Field(
|
|
220
|
+
None,
|
|
221
|
+
description="Scenario time at which to resume execution.",
|
|
222
|
+
alias="resumeTime",
|
|
223
|
+
)
|
|
224
|
+
requesting_app: str = Field(
|
|
225
|
+
...,
|
|
226
|
+
description="Name of the application requesting the freeze.",
|
|
227
|
+
alias="requestingApp",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class FreezeRequest(BaseModel):
|
|
232
|
+
"""
|
|
233
|
+
Request message for a managed application to request a freeze.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
tasking_parameters: FreezeRequestParameters = Field(alias="taskingParameters")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ResumeRequestParameters(BaseModel):
|
|
240
|
+
"""
|
|
241
|
+
Parameters for requesting a resume from a managed application.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
requesting_app: str = Field(
|
|
245
|
+
...,
|
|
246
|
+
description="Name of the application requesting the freeze.",
|
|
247
|
+
alias="requestingApp",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ResumeRequest(BaseModel):
|
|
252
|
+
"""
|
|
253
|
+
Request message for a managed application to request a resume.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
tasking_parameters: ResumeRequestParameters = Field(alias="taskingParameters")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class UpdateRequestParameters(BaseModel):
|
|
260
|
+
"""
|
|
261
|
+
Parameters for requesting an update from a managed application.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
time_scale_factor: float = Field(
|
|
265
|
+
...,
|
|
266
|
+
gt=0,
|
|
267
|
+
description="Time scaling factor (scenario seconds per wallclock second).",
|
|
268
|
+
alias="timeScalingFactor",
|
|
269
|
+
)
|
|
270
|
+
sim_update_time: Optional[datetime] = Field(
|
|
271
|
+
# ...,
|
|
272
|
+
None,
|
|
273
|
+
description="Scenario time at which to update the time scaling factor.",
|
|
274
|
+
alias="simUpdateTime",
|
|
275
|
+
)
|
|
276
|
+
requesting_app: str = Field(
|
|
277
|
+
...,
|
|
278
|
+
description="Name of the application requesting the update.",
|
|
279
|
+
alias="requestingApp",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class UpdateRequest(BaseModel):
|
|
284
|
+
"""
|
|
285
|
+
Request message for a managed application to request an update.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
tasking_parameters: UpdateRequestParameters = Field(alias="taskingParameters")
|
|
289
|
+
|
|
290
|
+
|
|
135
291
|
class TimeStatusProperties(BaseModel):
|
|
136
292
|
"""
|
|
137
293
|
Properties to report time status.
|
|
@@ -280,7 +436,7 @@ class KeycloakConfig(BaseModel):
|
|
|
280
436
|
realm: str = Field("master", description="Keycloak realm.")
|
|
281
437
|
tls: bool = Field(False, description="Keycloak TLS/SSL.")
|
|
282
438
|
token_refresh_interval: int = Field(
|
|
283
|
-
|
|
439
|
+
240, description="Keycloak token refresh interval, in seconds."
|
|
284
440
|
)
|
|
285
441
|
|
|
286
442
|
|
|
@@ -307,11 +463,8 @@ class ServersConfig(BaseModel):
|
|
|
307
463
|
return values
|
|
308
464
|
|
|
309
465
|
|
|
310
|
-
class
|
|
311
|
-
"""
|
|
312
|
-
Properties to report wallclock offset.
|
|
313
|
-
"""
|
|
314
|
-
|
|
466
|
+
class GeneralConfig(BaseModel):
|
|
467
|
+
prefix: Optional[str] = Field("nost", description="Execution prefix.")
|
|
315
468
|
wallclock_offset_refresh_interval: Optional[int] = Field(
|
|
316
469
|
10800, description="Wallclock offset refresh interval, in seconds."
|
|
317
470
|
)
|
|
@@ -320,24 +473,33 @@ class WallclockOffsetProperties(BaseModel):
|
|
|
320
473
|
)
|
|
321
474
|
|
|
322
475
|
|
|
323
|
-
class
|
|
324
|
-
prefix: Optional[str] = Field("nost", description="Execution prefix.")
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
class TimeScaleUpdateSchema(BaseModel):
|
|
476
|
+
class LoggingConfig(BaseModel):
|
|
328
477
|
"""
|
|
329
|
-
|
|
478
|
+
Configuration for logging.
|
|
330
479
|
"""
|
|
331
480
|
|
|
332
|
-
|
|
333
|
-
|
|
481
|
+
enable_file_logging: Optional[bool] = Field(
|
|
482
|
+
False, description="Enable file logging."
|
|
334
483
|
)
|
|
335
|
-
|
|
336
|
-
|
|
484
|
+
log_dir: Optional[str] = Field("logs", description="Directory path for log files.")
|
|
485
|
+
log_filename: Optional[str] = Field(None, description="Path to the log file.")
|
|
486
|
+
log_level: Optional[str] = Field(
|
|
487
|
+
"INFO", description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)."
|
|
488
|
+
)
|
|
489
|
+
max_bytes: Optional[int] = Field(
|
|
490
|
+
10 * 1024 * 1024,
|
|
491
|
+
description="Maximum size of the log file in bytes. Default is 10MB.",
|
|
492
|
+
)
|
|
493
|
+
backup_count: Optional[int] = Field(
|
|
494
|
+
5, description="Number of backup log files to keep."
|
|
495
|
+
)
|
|
496
|
+
log_format: Optional[str] = Field(
|
|
497
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
498
|
+
description="Format of the log messages.",
|
|
337
499
|
)
|
|
338
500
|
|
|
339
501
|
|
|
340
|
-
class ManagerConfig(
|
|
502
|
+
class ManagerConfig(LoggingConfig):
|
|
341
503
|
sim_start_time: Optional[datetime] = Field(
|
|
342
504
|
None, description="Simulation start time."
|
|
343
505
|
)
|
|
@@ -348,9 +510,6 @@ class ManagerConfig(BaseModel):
|
|
|
348
510
|
description="Time step for the simulation.",
|
|
349
511
|
)
|
|
350
512
|
time_scale_factor: float = Field(1.0, description="Time scale factor.")
|
|
351
|
-
time_scale_updates: List[TimeScaleUpdateSchema] = Field(
|
|
352
|
-
default_factory=list, description="List of time scale updates."
|
|
353
|
-
)
|
|
354
513
|
time_status_step: Optional[timedelta] = Field(None, description="Time status step.")
|
|
355
514
|
time_status_init: Optional[datetime] = Field(None, description="Time status init.")
|
|
356
515
|
command_lead: timedelta = Field(
|
|
@@ -405,7 +564,7 @@ class ManagerConfig(BaseModel):
|
|
|
405
564
|
return values
|
|
406
565
|
|
|
407
566
|
|
|
408
|
-
class ManagedApplicationConfig(
|
|
567
|
+
class ManagedApplicationConfig(LoggingConfig):
|
|
409
568
|
time_scale_factor: float = Field(1.0, description="Time scale factor.")
|
|
410
569
|
time_step: timedelta = Field(
|
|
411
570
|
timedelta(seconds=1), description="Time step for swe_change."
|
|
@@ -558,9 +717,6 @@ class SimulationConfig(BaseModel):
|
|
|
558
717
|
|
|
559
718
|
|
|
560
719
|
class RuntimeConfig(BaseModel):
|
|
561
|
-
wallclock_offset_properties: WallclockOffsetProperties = Field(
|
|
562
|
-
..., description="Properties for wallclock offset."
|
|
563
|
-
)
|
|
564
720
|
credentials: Credentials = Field(..., description="Credentials for authentication.")
|
|
565
721
|
server_configuration: Config = (
|
|
566
722
|
Field(..., description="Simulation configuration."),
|
nost_tools/simulator.py
CHANGED
|
@@ -33,6 +33,9 @@ class Mode(str, Enum):
|
|
|
33
33
|
INITIALIZING = "INITIALIZING"
|
|
34
34
|
INITIALIZED = "INITIALIZED"
|
|
35
35
|
EXECUTING = "EXECUTING"
|
|
36
|
+
PAUSING = "PAUSING"
|
|
37
|
+
PAUSED = "PAUSED"
|
|
38
|
+
RESUMING = "RESUMING"
|
|
36
39
|
TERMINATING = "TERMINATING"
|
|
37
40
|
TERMINATED = "TERMINATED"
|
|
38
41
|
|
|
@@ -89,12 +92,10 @@ class Simulator(Observable):
|
|
|
89
92
|
Args:
|
|
90
93
|
entity (:obj:`Entity`): entity to be added
|
|
91
94
|
"""
|
|
92
|
-
if self._mode
|
|
93
|
-
raise RuntimeError(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
elif self._mode == Mode.TERMINATING:
|
|
97
|
-
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
95
|
+
if self._mode not in [Mode.UNDEFINED, Mode.INITIALIZED, Mode.TERMINATED]:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"Can only add entity from UNDEFINED, INITIALIZED, or TERMINATED modes."
|
|
98
|
+
)
|
|
98
99
|
self._set_mode(Mode.UNDEFINED)
|
|
99
100
|
self._entities.append(entity)
|
|
100
101
|
|
|
@@ -141,12 +142,10 @@ class Simulator(Observable):
|
|
|
141
142
|
Returns:
|
|
142
143
|
:obj:`Entity`: removed entity
|
|
143
144
|
"""
|
|
144
|
-
if self._mode
|
|
145
|
-
raise RuntimeError(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
elif self._mode == Mode.TERMINATING:
|
|
149
|
-
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
145
|
+
if self._mode not in [Mode.UNDEFINED, Mode.INITIALIZED, Mode.TERMINATED]:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"Can only remove entity from UNDEFINED, INITIALIZED, or TERMINATED modes."
|
|
148
|
+
)
|
|
150
149
|
if entity in self._entities:
|
|
151
150
|
self._set_mode(Mode.UNDEFINED)
|
|
152
151
|
return self._entities.remove(entity)
|
|
@@ -173,12 +172,10 @@ class Simulator(Observable):
|
|
|
173
172
|
initial scenario time, None uses the current wallclock time (default: None)
|
|
174
173
|
time_scale_factor (float): number of scenario seconds per wallclock second (default: 1)
|
|
175
174
|
"""
|
|
176
|
-
if self._mode
|
|
177
|
-
raise RuntimeError(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
elif self._mode == Mode.TERMINATING:
|
|
181
|
-
raise RuntimeError("Cannot initialize: simulator is terminating.")
|
|
175
|
+
if self._mode not in [Mode.UNDEFINED, Mode.INITIALIZED, Mode.TERMINATED]:
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
"Can only initialize from UNDEFINED, INITIALIZED, or TERMINATED modes."
|
|
178
|
+
)
|
|
182
179
|
self._set_mode(Mode.INITIALIZING)
|
|
183
180
|
logger.info(
|
|
184
181
|
f"Initializing simulator to time {init_time} (wallclock time {wallclock_epoch})"
|
|
@@ -219,6 +216,11 @@ class Simulator(Observable):
|
|
|
219
216
|
initial scenario time, None uses the current wallclock time (default: None)
|
|
220
217
|
time_scale_factor (float): number of scenario seconds per wallclock second (default value: 1)
|
|
221
218
|
"""
|
|
219
|
+
if self._mode not in [Mode.UNDEFINED, Mode.INITIALIZED, Mode.TERMINATED]:
|
|
220
|
+
raise RuntimeError(
|
|
221
|
+
f"Cannot execute: simulator is {self._mode}. Wait for TERMINATED or terminate the current run."
|
|
222
|
+
)
|
|
223
|
+
|
|
222
224
|
if self._mode != Mode.INITIALIZED:
|
|
223
225
|
self.initialize(init_time, wallclock_epoch, time_scale_factor)
|
|
224
226
|
|
|
@@ -233,9 +235,8 @@ class Simulator(Observable):
|
|
|
233
235
|
|
|
234
236
|
logger.info("Starting main simulation loop.")
|
|
235
237
|
while (
|
|
236
|
-
self._mode == Mode.EXECUTING
|
|
237
|
-
|
|
238
|
-
):
|
|
238
|
+
self._mode == Mode.EXECUTING or self._mode == Mode.PAUSING
|
|
239
|
+
) and self.get_time() < self.get_init_time() + self.get_duration():
|
|
239
240
|
# compute time step (last step may be shorter)
|
|
240
241
|
time_step = min(
|
|
241
242
|
self._time_step, self._init_time + self._duration - self._time
|
|
@@ -302,6 +303,17 @@ class Simulator(Observable):
|
|
|
302
303
|
"""
|
|
303
304
|
Waits until the wallclock time matches the next time step interval.
|
|
304
305
|
"""
|
|
306
|
+
if self._mode == Mode.PAUSING:
|
|
307
|
+
self._set_mode(Mode.PAUSED)
|
|
308
|
+
while self._mode == Mode.PAUSED:
|
|
309
|
+
time.sleep(0.01)
|
|
310
|
+
if self._mode == Mode.RESUMING:
|
|
311
|
+
# reset the wallclock and simulation epochs
|
|
312
|
+
self._wallclock_epoch = self.get_wallclock_time()
|
|
313
|
+
self._simulation_epoch = self._time
|
|
314
|
+
self._set_mode(Mode.EXECUTING)
|
|
315
|
+
# Return immediately after resume to avoid waiting
|
|
316
|
+
return
|
|
305
317
|
while (
|
|
306
318
|
self._mode == Mode.EXECUTING
|
|
307
319
|
and self.get_wallclock_time_at_simulation_time(self._next_time)
|
|
@@ -520,6 +532,25 @@ class Simulator(Observable):
|
|
|
520
532
|
raise RuntimeError("Cannot set wallclock offset: simulator is terminating")
|
|
521
533
|
self._wallclock_offset = wallclock_offset
|
|
522
534
|
|
|
535
|
+
def pause(self) -> None:
|
|
536
|
+
"""
|
|
537
|
+
Pauses the scenario execution. Requires that the simulator is in EXECUTING mode.
|
|
538
|
+
"""
|
|
539
|
+
logger.info("Pausing simulation execution.")
|
|
540
|
+
if self._mode != Mode.EXECUTING:
|
|
541
|
+
raise RuntimeError("Cannot pause: simulator is not executing.")
|
|
542
|
+
|
|
543
|
+
self._set_mode(Mode.PAUSING)
|
|
544
|
+
|
|
545
|
+
def resume(self) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Resumes the scenario execution. Requires that the simulator is in PAUSING or PAUSED mode.
|
|
548
|
+
"""
|
|
549
|
+
if self._mode not in [Mode.PAUSING, Mode.PAUSED]:
|
|
550
|
+
raise RuntimeError("Cannot resume: simulator is not pausing or paused.")
|
|
551
|
+
self._next_time = self._time
|
|
552
|
+
self._set_mode(Mode.RESUMING)
|
|
553
|
+
|
|
523
554
|
def terminate(self) -> None:
|
|
524
555
|
"""
|
|
525
556
|
Terminates the scenario execution. Requires that the simulator is in EXECUTING mode.
|
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
nost_tools/__init__.py,sha256=U8ylAZeztjYh1QXalVZU8-0BsY1JKRqTFAcnUBxEJro,853
|
|
2
|
+
nost_tools/application.py,sha256=C-19kvRTHUt-xXA_0hBh1aoIE32fvTETwCqtrFswYCQ,67406
|
|
3
|
+
nost_tools/application_utils.py,sha256=jiMzuuP6-47UlUO64HhwNvbl6uKvVnsksYgOw7CmxL4,9327
|
|
4
|
+
nost_tools/configuration.py,sha256=qHXfWK_IrLj5Z8xWfF3H5rqHOLL0bLlZ3Ls73rTDHUk,13046
|
|
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=0P3_UHSQfdLqgaAnxjEJetkYU4P6M7NAHkOcRTUXe9o,18631
|
|
9
|
+
nost_tools/manager.py,sha256=Xu200PlrNNMBhWMryRcuVT-_vE0oW5wrn7PtLLAPSIY,34148
|
|
10
|
+
nost_tools/observer.py,sha256=PmqxnVN422dIRgwH29RcvK331Q4vtH_eD7MP1eIe_vk,8564
|
|
11
|
+
nost_tools/publisher.py,sha256=-p5G9JLVZKhNNOSUGlvDKyWZSHlWikQV1TabsNIyLJA,5714
|
|
12
|
+
nost_tools/schemas.py,sha256=nuo0mdL1kouuuWXOROHTxA63lGJSvr15BDVygvl3PKU,24727
|
|
13
|
+
nost_tools/simulator.py,sha256=AtAHVl0BLAZuewqnpheNoLIDbObD-PWh9an_-d4Dx40,21148
|
|
14
|
+
nost_tools-3.0.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
|
|
15
|
+
nost_tools-3.0.0.dist-info/METADATA,sha256=OoZcUL8cm9QG7JNAyF4rVNPIntY1cX8Fsooq-U40dhw,4256
|
|
16
|
+
nost_tools-3.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
nost_tools-3.0.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
|
|
18
|
+
nost_tools-3.0.0.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|