nost-tools 2.0.4__py3-none-any.whl → 2.1.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/entity.py CHANGED
@@ -43,13 +43,30 @@ class Entity(Observable):
43
43
  """
44
44
  self._init_time = self._time = self._next_time = init_time
45
45
 
46
+ # def tick(self, time_step: timedelta) -> None:
47
+ # """
48
+ # Computes the next state transition following an elapsed scenario duration (time step).
49
+
50
+ # Args:
51
+ # time_step (:obj:`timedelta`): elapsed scenario duration
52
+ # """
53
+ # self._next_time = self._time + time_step
46
54
  def tick(self, time_step: timedelta) -> None:
47
55
  """
48
56
  Computes the next state transition following an elapsed scenario duration (time step).
57
+ If the entity hasn't been initialized yet, the time_step will be stored but no
58
+ time advancement will occur until the entity is initialized.
49
59
 
50
60
  Args:
51
61
  time_step (:obj:`timedelta`): elapsed scenario duration
52
62
  """
63
+ if self._time is None:
64
+ logger.debug(
65
+ f"Entity {self.name} not yet initialized, waiting for initialization."
66
+ )
67
+ # Don't try to calculate next_time yet, just maintain the current None state
68
+ return
69
+
53
70
  self._next_time = self._time + time_step
54
71
 
55
72
  def tock(self) -> None:
@@ -196,7 +196,7 @@ class ManagedApplication(Application):
196
196
  self._sim_stop_time = params.sim_stop_time
197
197
  logger.info(f"Sim stop time: {params.sim_stop_time}")
198
198
 
199
- threading.Thread(
199
+ self._simulation_thread = threading.Thread(
200
200
  target=self.simulator.execute,
201
201
  kwargs={
202
202
  "init_time": self._sim_start_time,
@@ -205,7 +205,8 @@ class ManagedApplication(Application):
205
205
  "wallclock_epoch": params.start_time,
206
206
  "time_scale_factor": params.time_scaling_factor,
207
207
  },
208
- ).start()
208
+ )
209
+ self._simulation_thread.start()
209
210
 
210
211
  except Exception as e:
211
212
  logger.error(
nost_tools/manager.py CHANGED
@@ -94,10 +94,40 @@ class Manager(Application):
94
94
  self.channel.exchange_declare(
95
95
  exchange=self.prefix,
96
96
  exchange_type="topic",
97
- durable=False,
97
+ durable=True,
98
98
  auto_delete=True,
99
99
  )
100
100
 
101
+ def _sleep_with_heartbeat(self, total_seconds):
102
+ """
103
+ Sleep for a specified number of seconds while allowing connection heartbeats.
104
+ Works with SelectConnection by using short sleep intervals.
105
+
106
+ Args:
107
+ total_seconds (float): Total number of seconds to sleep
108
+ """
109
+ if total_seconds <= 0:
110
+ return
111
+
112
+ # Sleep in smaller chunks to allow heartbeats to pass through
113
+ check_interval = 30 # Check every 30 seconds at most
114
+ end_time = time.time() + total_seconds
115
+
116
+ logger.debug(f"Starting heartbeat-safe sleep for {total_seconds:.2f} seconds")
117
+
118
+ while time.time() < end_time:
119
+ # Calculate remaining time
120
+ remaining = end_time - time.time()
121
+
122
+ # Sleep for the shorter of check_interval or remaining time
123
+ sleep_time = min(check_interval, remaining)
124
+
125
+ if sleep_time > 0:
126
+ time.sleep(sleep_time)
127
+ logger.debug(
128
+ f"Heartbeat check: {remaining:.2f} seconds remaining in sleep"
129
+ )
130
+
101
131
  def execute_test_plan(self, *args, **kwargs) -> None:
102
132
  """
103
133
  Starts the test plan execution in a background thread.
@@ -147,6 +177,7 @@ class Manager(Application):
147
177
  init_retry_delay_s (float): number of seconds to wait between initialization commands while waiting for required applications
148
178
  init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
149
179
  """
180
+ # Initialize parameters from arguments or config
150
181
  if sim_start_time is not None and sim_stop_time is not None:
151
182
  self.sim_start_time = sim_start_time
152
183
  self.sim_stop_time = sim_stop_time
@@ -186,13 +217,10 @@ class Manager(Application):
186
217
  raise ValueError(
187
218
  "No configuration runtime. Please provide simulation start and stop times."
188
219
  )
189
- ####
220
+
190
221
  self.establish_exchange()
191
- # if self.predefined_exchanges_queues:
192
- # self.declare_exchange()
193
- # self.declare_bind_queue()
194
- ####
195
222
 
223
+ # Set up tracking of required applications
196
224
  self.required_apps_status = dict(
197
225
  zip(self.required_apps, [False] * len(self.required_apps))
198
226
  )
@@ -200,35 +228,37 @@ class Manager(Application):
200
228
  self.add_message_callback("*", "status.time", self.on_app_time_status)
201
229
 
202
230
  self._create_time_status_publisher(self.time_status_step, self.time_status_init)
231
+
232
+ # Initialize with retry logic
203
233
  for i in range(self.init_max_retry):
204
- # issue the init command
205
234
  self.init(self.sim_start_time, self.sim_stop_time, self.required_apps)
206
235
  next_try = self.simulator.get_wallclock_time() + timedelta(
207
236
  seconds=self.init_retry_delay_s
208
237
  )
209
- # wait until all required apps are ready
210
238
  while (
211
239
  not all([self.required_apps_status[app] for app in self.required_apps])
212
240
  and self.simulator.get_wallclock_time() < next_try
213
241
  ):
214
242
  time.sleep(0.001)
215
- # self.remove_message_callback("*", "status.ready")
216
- # self.remove_message_callback()
217
- # configure start time
243
+
244
+ # Configure start time if not provided
218
245
  if self.start_time is None:
219
246
  self.start_time = self.simulator.get_wallclock_time() + self.command_lead
220
- # sleep until the start command needs to be issued
221
- time.sleep(
222
- max(
223
- 0,
224
- (
225
- (self.start_time - self.simulator.get_wallclock_time())
226
- - self.command_lead
227
- )
228
- / timedelta(seconds=1),
247
+
248
+ # Sleep until start time using heartbeat-safe approach
249
+ sleep_seconds = max(
250
+ 0,
251
+ (
252
+ (self.start_time - self.simulator.get_wallclock_time())
253
+ - self.command_lead
229
254
  )
255
+ / timedelta(seconds=1),
230
256
  )
231
- # issue the start command
257
+
258
+ # Use our heartbeat-safe sleep
259
+ self._sleep_with_heartbeat(sleep_seconds)
260
+
261
+ # Issue the start command
232
262
  self.start(
233
263
  self.sim_start_time,
234
264
  self.sim_stop_time,
@@ -238,43 +268,51 @@ class Manager(Application):
238
268
  self.time_status_step,
239
269
  self.time_status_init,
240
270
  )
241
- # wait for simulation to start executing
271
+
272
+ # Wait for simulation to start executing
242
273
  while self.simulator.get_mode() != Mode.EXECUTING:
243
274
  time.sleep(0.001)
275
+
276
+ # Process time scale updates
244
277
  for update in self.time_scale_updates:
245
278
  update_time = self.simulator.get_wallclock_time_at_simulation_time(
246
279
  update.sim_update_time
247
280
  )
248
- # sleep until the update command needs to be issued
249
- time.sleep(
250
- max(
251
- 0,
252
- (
253
- (update_time - self.simulator.get_wallclock_time())
254
- - self.command_lead
255
- )
256
- / timedelta(seconds=1),
281
+ # Sleep until update time using heartbeat-safe approach
282
+ sleep_seconds = max(
283
+ 0,
284
+ (
285
+ (update_time - self.simulator.get_wallclock_time())
286
+ - self.command_lead
257
287
  )
288
+ / timedelta(seconds=1),
258
289
  )
259
- # issue the update command
260
- self.update(
261
- update.time_scale_factor, update.sim_update_time, self.required_apps
262
- )
263
- # wait until the update command takes effect
290
+
291
+ # Use our heartbeat-safe sleep
292
+ self._sleep_with_heartbeat(sleep_seconds)
293
+
294
+ # Issue the update command
295
+ self.update(update.time_scale_factor, update.sim_update_time)
296
+
297
+ # Wait until update takes effect
264
298
  while self.simulator.get_time_scale_factor() != update.time_scale_factor:
265
- time.sleep(self.command_lead / timedelta(seconds=1) / 100)
299
+ time.sleep(0.001)
300
+
266
301
  end_time = self.simulator.get_wallclock_time_at_simulation_time(
267
302
  self.simulator.get_end_time()
268
303
  )
269
- # sleep until the stop command should be issued
270
- time.sleep(
271
- max(
272
- 0,
273
- ((end_time - self.simulator.get_wallclock_time()) - self.command_lead)
274
- / timedelta(seconds=1),
275
- )
304
+
305
+ # Sleep until stop time using heartbeat-safe approach
306
+ sleep_seconds = max(
307
+ 0,
308
+ ((end_time - self.simulator.get_wallclock_time()) - self.command_lead)
309
+ / timedelta(seconds=1),
276
310
  )
277
- # issue the stop command
311
+
312
+ # Use our heartbeat-safe sleep
313
+ self._sleep_with_heartbeat(sleep_seconds)
314
+
315
+ # Issue the stop command
278
316
  self.stop(self.sim_stop_time)
279
317
 
280
318
  def on_app_ready_status(self, ch, method, properties, body) -> None:
nost_tools/observer.py CHANGED
@@ -3,8 +3,12 @@ Provides base classes that implement the observer pattern to loosely couple an o
3
3
  """
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from datetime import datetime, timezone
7
- from typing import List, Optional, Union
6
+ from collections.abc import Callable
7
+ from datetime import datetime, timedelta, timezone
8
+ from typing import TYPE_CHECKING, List, Optional, Union
9
+
10
+ if TYPE_CHECKING:
11
+ from nost_tools.simulator import Mode, Simulator
8
12
 
9
13
 
10
14
  class Observer(ABC):
@@ -179,3 +183,76 @@ class MessageObservable(Observable):
179
183
  """
180
184
  for observer in self._message_observers:
181
185
  observer.on_message(ch, method, properties, body)
186
+
187
+
188
+ class PropertyChangeCallback(Observer):
189
+ """
190
+ Triggers a provided callback basedwhen a named property changes.
191
+ """
192
+
193
+ def __init__(self, property_name: str, callback: Callable[[object, object], None]):
194
+ self.callback = callback
195
+ self.property_name = property_name
196
+
197
+ def on_change(
198
+ self, source: object, property_name: str, old_value: object, new_value: object
199
+ ) -> None:
200
+ if self.property_name == property_name:
201
+ self.callback(source, new_value)
202
+
203
+
204
+ class ScenarioTimeIntervalCallback(Observer):
205
+ """
206
+ Triggers a provided callback at a fixed interval in scenario time.
207
+ """
208
+
209
+ def __init__(
210
+ self, callback: Callable[[object, datetime], None], time_inteval: timedelta
211
+ ):
212
+ self.callback = callback
213
+ self.time_interval = time_inteval
214
+ self._next_time = None
215
+
216
+ def on_change(
217
+ self, source: object, property_name: str, old_value: object, new_value: object
218
+ ):
219
+ if property_name == source.PROPERTY_TIME:
220
+ if self._next_time is None:
221
+ self._next_time = old_value + self.time_interval
222
+ while self._next_time <= new_value:
223
+ self.callback(source, self._next_time)
224
+ self._next_time = self._next_time + self.time_interval
225
+
226
+
227
+ class WallclockTimeIntervalCallback(Observer):
228
+ """
229
+ Triggers a provided callback at a fixed interval in wallclock time.
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ simulator: "Simulator",
235
+ callback: Callable[[datetime], None],
236
+ time_inteval: timedelta,
237
+ time_init: timedelta = None,
238
+ ):
239
+ self.simulator = simulator
240
+ self.callback = callback
241
+ self.time_interval = time_inteval
242
+ self.time_init = time_init
243
+ self._next_time = None
244
+
245
+ def on_change(
246
+ self, source: object, property_name: str, old_value: object, new_value: object
247
+ ):
248
+ from nost_tools.simulator import Mode, Simulator
249
+
250
+ if property_name == Simulator.PROPERTY_MODE and new_value == Mode.INITIALIZED:
251
+ self._next_time = self.time_init
252
+ elif property_name == Simulator.PROPERTY_TIME:
253
+ wallclock_time = self.simulator.get_wallclock_time()
254
+ if self._next_time is None:
255
+ self._next_time = wallclock_time + self.time_interval
256
+ while self._next_time <= wallclock_time:
257
+ self.callback(self._next_time)
258
+ self._next_time = self._next_time + self.time_interval
nost_tools/schemas.py CHANGED
@@ -219,20 +219,48 @@ class RabbitMQConfig(BaseModel):
219
219
  keycloak_authentication: bool = Field(
220
220
  False, description="Keycloak authentication for RabbitMQ."
221
221
  )
222
- host: str = Field("localhost", description="RabbitMQ host.")
223
- port: int = Field(5672, description="RabbitMQ port.")
224
222
  tls: bool = Field(False, description="RabbitMQ TLS/SSL.")
225
- virtual_host: str = Field("/", description="RabbitMQ virtual host.")
226
- message_expiration: str = Field(
227
- None, description="RabbitMQ expiration, in milliseconds."
223
+ reconnect_delay: int = Field(10, description="Reconnection delay, in seconds.")
224
+ queue_max_size: int = Field(5000, description="Maximum size of the RabbitMQ queue.")
225
+ # BasicProperties
226
+ content_type: str = Field(
227
+ None,
228
+ description="RabbitMQ MIME content type (application/json, text/plain, etc.).",
229
+ )
230
+ content_encoding: str = Field(
231
+ None,
232
+ description="RabbitMQ MIME content encoding (gzip, deflate, etc.).",
233
+ )
234
+ headers: Dict[str, str] = Field(
235
+ None, description="RabbitMQ message headers (key-value pairs)."
228
236
  )
229
237
  delivery_mode: int = Field(
230
238
  None, description="RabbitMQ delivery mode (1: non-persistent, 2: durable)."
231
239
  )
232
- content_type: str = Field(
233
- None,
234
- description="RabbitMQ MIME content type (application/json, text/plain, etc.).",
240
+ priority: int = Field(None, description="RabbitMQ message priority (0-255).")
241
+ correlation_id: str = Field(
242
+ None, description="RabbitMQ correlation ID for message tracking."
243
+ )
244
+ reply_to: str = Field(
245
+ None, description="RabbitMQ reply-to queue for response messages."
246
+ )
247
+ message_expiration: str = Field(
248
+ None, description="RabbitMQ expiration, in milliseconds."
249
+ )
250
+ message_id: str = Field(None, description="RabbitMQ message ID for tracking.")
251
+ timestamp: datetime = Field(None, description="RabbitMQ message timestamp.")
252
+ type: str = Field(None, description="RabbitMQ message type (e.g., 'text', 'json').")
253
+ user_id: str = Field(None, description="RabbitMQ user ID for authentication.")
254
+ app_id: str = Field(None, description="RabbitMQ application ID for tracking.")
255
+ cluster_id: str = Field(None, description="RabbitMQ cluster ID for tracking.")
256
+ # ConnectionParameters
257
+ host: str = Field("localhost", description="RabbitMQ host.")
258
+ port: int = Field(5672, description="RabbitMQ port.")
259
+ virtual_host: str = Field("/", description="RabbitMQ virtual host.")
260
+ channel_max: int = Field(
261
+ 65535, description="RabbitMQ maximum number of channels per connection."
235
262
  )
263
+ frame_max: int = Field(131072, description="RabbitMQ maximum frame size in bytes.")
236
264
  heartbeat: int = Field(None, description="RabbitMQ heartbeat interval, in seconds.")
237
265
  connection_attempts: int = Field(
238
266
  1, description="RabbitMQ connection attempts before giving up."
@@ -244,7 +272,6 @@ class RabbitMQConfig(BaseModel):
244
272
  blocked_connection_timeout: int = Field(
245
273
  None, description="Timeout for blocked connections."
246
274
  )
247
- reconnect_delay: int = Field(10, description="Reconnect delay, in seconds.")
248
275
 
249
276
 
250
277
  class KeycloakConfig(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nost_tools
3
- Version: 2.0.4
3
+ Version: 2.1.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=lEj4-pG2wDOV6AHI8ROPB9X63nMnzSgzpyoyCSLDJns,870
2
+ nost_tools/application.py,sha256=bPuRMBifppJ-qkDKqOqp13rW_LOjbM7JaXIozMyxmlw,60914
3
+ nost_tools/application_utils.py,sha256=_R39D26FYxgaO4uyTON24KXc4UQ4zAEDBZfEkHbEw64,9386
4
+ nost_tools/configuration.py,sha256=ikNpZi8aofhZzJRbJf4x46afbAnp8r5C7Yr50Rnn1Nc,11639
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=QSOcFqA3xpa84AuxvXV6134PZp8iQndNCq1w3P4tHUU,21315
10
+ nost_tools/observer.py,sha256=D64V0KTvHRPEqbB8q3BosJhoAlpBah2vyBlVbxWQR44,8161
11
+ nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
12
+ nost_tools/schemas.py,sha256=Wlzo21D94wFbQ2jlIU-vdhLGvE-2gaEYWx04KCGmhF0,16730
13
+ nost_tools/simulator.py,sha256=ALnGDmnA_ga-1Lq-bVWi2vcrspgjS4vtuDE0jWsI7fE,20191
14
+ nost_tools-2.1.0.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
15
+ nost_tools-2.1.0.dist-info/METADATA,sha256=eQcmCcznZHJtDqOHgoO_ydMMRRfhyjqGlPG5l2tVsgk,4256
16
+ nost_tools-2.1.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
17
+ nost_tools-2.1.0.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
18
+ nost_tools-2.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- nost_tools/__init__.py,sha256=sG0Uwgsr3_fZsBiCWJ_Ff_kdhYNX3Xec6asi3T0yrck,873
2
- nost_tools/application.py,sha256=1mFCw6b5BCAlaof-ijzjFYCvVB-WCn9llpHHijxeIik,37027
3
- nost_tools/application_utils.py,sha256=_R39D26FYxgaO4uyTON24KXc4UQ4zAEDBZfEkHbEw64,9386
4
- nost_tools/configuration.py,sha256=ikNpZi8aofhZzJRbJf4x46afbAnp8r5C7Yr50Rnn1Nc,11639
5
- nost_tools/entity.py,sha256=AwbZMP3_H4RQuyU4voyQwYFkETxG0mfD-0BMHxrRFf8,2064
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=jjS-URl4D_-VwKNYnqwYg_myFugKgMd_Gy1aAxxw0SU,11514
9
- nost_tools/manager.py,sha256=0282l6h2KSP2-OfFjI8tRaXJAvnZDzwqC_7dPzFK7U0,20304
10
- nost_tools/observer.py,sha256=w66jZQ11Fr7XSCcvcc2f5ISce2n8Ba7cXqheSTuyrmw,5519
11
- nost_tools/publisher.py,sha256=omU8tb0AXnA6RfhYSh0vnXbJtrRo4ukx1J5ANl4bDLQ,5291
12
- nost_tools/schemas.py,sha256=m6Np7DGrBIgtswpnrTqSowTb_niC4NY59BTQFBYfkZc,15332
13
- nost_tools/simulator.py,sha256=ALnGDmnA_ga-1Lq-bVWi2vcrspgjS4vtuDE0jWsI7fE,20191
14
- nost_tools-2.0.4.dist-info/licenses/LICENSE,sha256=aAMU-mTHTKpWkBsg9QhkhCQpEm3Gri7J_fVuJov8s3s,1539
15
- nost_tools-2.0.4.dist-info/METADATA,sha256=diDeXkc0h1cthwamEVkSxIjjQv08EUjoV1dQ-6xiUkU,4256
16
- nost_tools-2.0.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
17
- nost_tools-2.0.4.dist-info/top_level.txt,sha256=LNChUgrv2-wiym12O0r61kY83COjTpTiJ2Ly1Ca58A8,11
18
- nost_tools-2.0.4.dist-info/RECORD,,