nost-tools 2.0.0__py3-none-any.whl → 2.0.1__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 +29 -29
- nost_tools/application.py +800 -793
- nost_tools/application_utils.py +262 -262
- nost_tools/configuration.py +304 -304
- nost_tools/entity.py +73 -73
- nost_tools/errors.py +14 -14
- nost_tools/logger_application.py +192 -192
- nost_tools/managed_application.py +261 -261
- nost_tools/manager.py +472 -472
- nost_tools/observer.py +181 -181
- nost_tools/publisher.py +141 -141
- nost_tools/schemas.py +432 -426
- nost_tools/simulator.py +531 -531
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/METADATA +118 -119
- nost_tools-2.0.1.dist-info/RECORD +18 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/licenses/LICENSE +29 -29
- nost_tools-2.0.0.dist-info/RECORD +0 -18
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/WHEEL +0 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/top_level.txt +0 -0
nost_tools/application_utils.py
CHANGED
|
@@ -1,262 +1,262 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides utility classes to help applications interact with the broker.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
import yaml
|
|
9
|
-
from dotenv import dotenv_values
|
|
10
|
-
from pydantic import ValidationError
|
|
11
|
-
|
|
12
|
-
from .observer import Observer
|
|
13
|
-
from .publisher import ScenarioTimeIntervalPublisher
|
|
14
|
-
from .schemas import Config, ModeStatus, TimeStatus
|
|
15
|
-
from .simulator import Mode, Simulator
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from .application import Application
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ConnectionConfig(object):
|
|
24
|
-
"""Connection configuration.
|
|
25
|
-
|
|
26
|
-
The configuration settings to establish a connection to the broker, including authentication for the
|
|
27
|
-
user and identification of the server.
|
|
28
|
-
|
|
29
|
-
Attributes:
|
|
30
|
-
username (str): client username, provided by NOS-T operator
|
|
31
|
-
password (str): client password, provided by NOS-T operator
|
|
32
|
-
host (str): broker hostname
|
|
33
|
-
rabbitmq_port (int): RabbitMQ broker port number
|
|
34
|
-
keycloak_port (int): Keycloak IAM port number
|
|
35
|
-
keycloak_realm (str): Keycloak realm name
|
|
36
|
-
client_id (str): Keycloak client ID
|
|
37
|
-
client_secret_key (str): Keycloak client secret key
|
|
38
|
-
virtual_host (str): RabbitMQ virtual host
|
|
39
|
-
is_tls (bool): True, if the connection uses Transport Layer Security (TLS)
|
|
40
|
-
env_file (str): Path to the .env file
|
|
41
|
-
yaml_file (str): Path to the YAML configuration file
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
username: str = None,
|
|
47
|
-
password: str = None,
|
|
48
|
-
host: str = None,
|
|
49
|
-
rabbitmq_port: int = None,
|
|
50
|
-
keycloak_port: int = None,
|
|
51
|
-
keycloak_realm: str = None,
|
|
52
|
-
client_id: str = None,
|
|
53
|
-
client_secret_key: str = None,
|
|
54
|
-
virtual_host: str = None,
|
|
55
|
-
is_tls: bool = True,
|
|
56
|
-
env_file: str = None,
|
|
57
|
-
yaml_file: str = None,
|
|
58
|
-
):
|
|
59
|
-
"""
|
|
60
|
-
Initializes a new connection configuration.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
username (str): client username, provided by NOS-T operator
|
|
64
|
-
password (str): client password, provided by NOS-T operator
|
|
65
|
-
host (str): broker hostname
|
|
66
|
-
rabbitmq_port (int): RabbitMQ broker port number
|
|
67
|
-
keycloak_port (int): Keycloak IAM port number
|
|
68
|
-
keycloak_realm (str): Keycloak realm name
|
|
69
|
-
client_id (str): Keycloak client ID
|
|
70
|
-
client_secret_key (str): Keycloak client secret key
|
|
71
|
-
virtual_host (str): RabbitMQ virtual host
|
|
72
|
-
is_tls (bool): True, if the connection uses TLS
|
|
73
|
-
env_file (str): Path to the .env file
|
|
74
|
-
yaml_file (str): Path to the YAML configuration file
|
|
75
|
-
"""
|
|
76
|
-
self.yaml_config = None # Initialize the attribute to store YAML data
|
|
77
|
-
self.yaml_mode = (
|
|
78
|
-
False # Initialize the attribute to store the mode of operation
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if env_file and yaml_file:
|
|
82
|
-
logger.info(f"Loading configuration from {env_file} and {yaml_file}.")
|
|
83
|
-
self.load_config_from_files(env_file, yaml_file)
|
|
84
|
-
self.yaml_mode = True
|
|
85
|
-
else:
|
|
86
|
-
logger.info("Loading configuration from arguments.")
|
|
87
|
-
self.username = username
|
|
88
|
-
self.password = password
|
|
89
|
-
self.host = host
|
|
90
|
-
self.rabbitmq_port = rabbitmq_port
|
|
91
|
-
self.keycloak_port = keycloak_port
|
|
92
|
-
self.keycloak_realm = keycloak_realm
|
|
93
|
-
self.client_id = client_id
|
|
94
|
-
self.client_secret_key = client_secret_key
|
|
95
|
-
self.virtual_host = virtual_host
|
|
96
|
-
self.is_tls = is_tls
|
|
97
|
-
|
|
98
|
-
def load_config_from_files(self, env_file: str, yaml_file: str):
|
|
99
|
-
"""
|
|
100
|
-
Loads configuration from .env and YAML files.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
env_file (str): Path to the .env file
|
|
104
|
-
yaml_file (str): Path to the YAML configuration file
|
|
105
|
-
"""
|
|
106
|
-
# Load .env file
|
|
107
|
-
credentials = dotenv_values(env_file)
|
|
108
|
-
self.username = credentials["USERNAME"]
|
|
109
|
-
self.password = credentials["PASSWORD"]
|
|
110
|
-
self.client_id = credentials["CLIENT_ID"]
|
|
111
|
-
self.client_secret_key = credentials["CLIENT_SECRET_KEY"]
|
|
112
|
-
|
|
113
|
-
# Load YAML file
|
|
114
|
-
with open(yaml_file, "r") as file:
|
|
115
|
-
yaml_data = yaml.safe_load(file)
|
|
116
|
-
|
|
117
|
-
try:
|
|
118
|
-
config = Config(**yaml_data)
|
|
119
|
-
self.yaml_config = yaml_data
|
|
120
|
-
except ValidationError as e:
|
|
121
|
-
raise ValueError(f"Invalid configuration: {e}")
|
|
122
|
-
|
|
123
|
-
self.host = config.servers.rabbitmq.host
|
|
124
|
-
self.rabbitmq_port = config.servers.rabbitmq.port
|
|
125
|
-
self.keycloak_port = config.servers.keycloak.port
|
|
126
|
-
self.keycloak_realm = config.servers.keycloak.realm
|
|
127
|
-
self.virtual_host = config.servers.rabbitmq.virtual_host
|
|
128
|
-
self.is_tls = config.servers.rabbitmq.tls and config.servers.keycloak.tls
|
|
129
|
-
|
|
130
|
-
if not self.is_tls:
|
|
131
|
-
raise ValueError("TLS must be enabled for both RabbitMQ and Keycloak.")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class ShutDownObserver(Observer):
|
|
135
|
-
"""
|
|
136
|
-
Observer that shuts down an application after scenario termination.
|
|
137
|
-
|
|
138
|
-
Attributes:
|
|
139
|
-
app (:obj:`Application`): application to be shut down after termination
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
def __init__(self, app: "Application"):
|
|
143
|
-
"""
|
|
144
|
-
Initializes a new shut down observer.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
app (:obj:`Application`): application to be shut down after termination
|
|
148
|
-
"""
|
|
149
|
-
self.app = app
|
|
150
|
-
|
|
151
|
-
def on_change(
|
|
152
|
-
self, source: object, property_name: str, old_value: object, new_value: object
|
|
153
|
-
) -> None:
|
|
154
|
-
"""
|
|
155
|
-
Shuts down the application in response to a transition to the TERMINATED mode.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
source (object): object that triggered a property change
|
|
159
|
-
property_name (str): name of the changed property
|
|
160
|
-
old_value (obj): old value of the named property
|
|
161
|
-
new_value (obj): new value of the named property
|
|
162
|
-
"""
|
|
163
|
-
if property_name == Simulator.PROPERTY_MODE and new_value == Mode.TERMINATED:
|
|
164
|
-
self.app.shut_down()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class TimeStatusPublisher(ScenarioTimeIntervalPublisher):
|
|
168
|
-
"""
|
|
169
|
-
Publishes time status messages for an application at a regular interval.
|
|
170
|
-
|
|
171
|
-
Attributes:
|
|
172
|
-
app (:obj:`Application`): application to publish time status messages
|
|
173
|
-
"""
|
|
174
|
-
|
|
175
|
-
def publish_message(self) -> None:
|
|
176
|
-
"""
|
|
177
|
-
Publishes a time status message.
|
|
178
|
-
"""
|
|
179
|
-
status = TimeStatus.model_validate(
|
|
180
|
-
{
|
|
181
|
-
"name": self.app.app_name,
|
|
182
|
-
"description": self.app.app_description,
|
|
183
|
-
"properties": {
|
|
184
|
-
"simTime": self.app.simulator.get_time(),
|
|
185
|
-
"time": self.app.simulator.get_wallclock_time(),
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
)
|
|
189
|
-
logger.info(
|
|
190
|
-
f"Sending time status {status.model_dump_json(by_alias=True,exclude_none=True)}."
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
self.app.send_message(
|
|
194
|
-
app_name=self.app.app_name,
|
|
195
|
-
app_topics="status.time",
|
|
196
|
-
payload=status.model_dump_json(by_alias=True, exclude_none=True),
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class ModeStatusObserver(Observer):
|
|
201
|
-
"""
|
|
202
|
-
Observer that publishes mode status messages for an application.
|
|
203
|
-
|
|
204
|
-
Attributes:
|
|
205
|
-
app (:obj:`Application`): application to publish mode status messages
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
def __init__(self, app: "Application"):
|
|
209
|
-
"""
|
|
210
|
-
Initializes a new mode status observer.
|
|
211
|
-
"""
|
|
212
|
-
self.app = app
|
|
213
|
-
|
|
214
|
-
def stop_application(self):
|
|
215
|
-
"""
|
|
216
|
-
Stops the application by closing the channel and connection if they are open.
|
|
217
|
-
"""
|
|
218
|
-
if self.app.channel.is_open:
|
|
219
|
-
self.app.channel.close()
|
|
220
|
-
logger.info(f"Channel closed for application {self.app.app_name}.")
|
|
221
|
-
|
|
222
|
-
if self.app.connection.is_open:
|
|
223
|
-
self.app.connection.close()
|
|
224
|
-
logger.info(f"Connection closed for application {self.app.app_name}.")
|
|
225
|
-
|
|
226
|
-
logger.info(f'Application "{self.app.app_name}" successfully stopped.')
|
|
227
|
-
|
|
228
|
-
def on_change(
|
|
229
|
-
self, source: object, property_name: str, old_value: object, new_value: object
|
|
230
|
-
) -> None:
|
|
231
|
-
"""
|
|
232
|
-
Publishes a mode status message in response to a mode transition.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
source (object): observable that triggered the change
|
|
236
|
-
property_name (str): name of the changed property
|
|
237
|
-
old_value (obj): old value of the named property
|
|
238
|
-
new_value (obj): new value of the named property
|
|
239
|
-
"""
|
|
240
|
-
if property_name == Simulator.PROPERTY_MODE:
|
|
241
|
-
status = ModeStatus.model_validate(
|
|
242
|
-
{
|
|
243
|
-
"name": self.app.app_name,
|
|
244
|
-
"description": self.app.app_description,
|
|
245
|
-
"properties": {"mode": self.app.simulator.get_mode()},
|
|
246
|
-
}
|
|
247
|
-
)
|
|
248
|
-
logger.info(
|
|
249
|
-
f"Sending mode status {status.model_dump_json(by_alias=True, exclude_none=True)}."
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
# Ensure self.prefix is a string
|
|
253
|
-
if not isinstance(self.app.prefix, str):
|
|
254
|
-
raise ValueError(f"Exchange ({self.app.prefix}) must be a string")
|
|
255
|
-
|
|
256
|
-
# Declare the topic exchange
|
|
257
|
-
if self.app.channel.is_open and self.app.connection.is_open:
|
|
258
|
-
self.app.send_message(
|
|
259
|
-
app_name=self.app.app_name,
|
|
260
|
-
app_topics="status.mode",
|
|
261
|
-
payload=status.model_dump_json(by_alias=True, exclude_none=True),
|
|
262
|
-
)
|
|
1
|
+
"""
|
|
2
|
+
Provides utility classes to help applications interact with the broker.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from dotenv import dotenv_values
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from .observer import Observer
|
|
13
|
+
from .publisher import ScenarioTimeIntervalPublisher
|
|
14
|
+
from .schemas import Config, ModeStatus, TimeStatus
|
|
15
|
+
from .simulator import Mode, Simulator
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .application import Application
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConnectionConfig(object):
|
|
24
|
+
"""Connection configuration.
|
|
25
|
+
|
|
26
|
+
The configuration settings to establish a connection to the broker, including authentication for the
|
|
27
|
+
user and identification of the server.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
username (str): client username, provided by NOS-T operator
|
|
31
|
+
password (str): client password, provided by NOS-T operator
|
|
32
|
+
host (str): broker hostname
|
|
33
|
+
rabbitmq_port (int): RabbitMQ broker port number
|
|
34
|
+
keycloak_port (int): Keycloak IAM port number
|
|
35
|
+
keycloak_realm (str): Keycloak realm name
|
|
36
|
+
client_id (str): Keycloak client ID
|
|
37
|
+
client_secret_key (str): Keycloak client secret key
|
|
38
|
+
virtual_host (str): RabbitMQ virtual host
|
|
39
|
+
is_tls (bool): True, if the connection uses Transport Layer Security (TLS)
|
|
40
|
+
env_file (str): Path to the .env file
|
|
41
|
+
yaml_file (str): Path to the YAML configuration file
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
username: str = None,
|
|
47
|
+
password: str = None,
|
|
48
|
+
host: str = None,
|
|
49
|
+
rabbitmq_port: int = None,
|
|
50
|
+
keycloak_port: int = None,
|
|
51
|
+
keycloak_realm: str = None,
|
|
52
|
+
client_id: str = None,
|
|
53
|
+
client_secret_key: str = None,
|
|
54
|
+
virtual_host: str = None,
|
|
55
|
+
is_tls: bool = True,
|
|
56
|
+
env_file: str = None,
|
|
57
|
+
yaml_file: str = None,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initializes a new connection configuration.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
username (str): client username, provided by NOS-T operator
|
|
64
|
+
password (str): client password, provided by NOS-T operator
|
|
65
|
+
host (str): broker hostname
|
|
66
|
+
rabbitmq_port (int): RabbitMQ broker port number
|
|
67
|
+
keycloak_port (int): Keycloak IAM port number
|
|
68
|
+
keycloak_realm (str): Keycloak realm name
|
|
69
|
+
client_id (str): Keycloak client ID
|
|
70
|
+
client_secret_key (str): Keycloak client secret key
|
|
71
|
+
virtual_host (str): RabbitMQ virtual host
|
|
72
|
+
is_tls (bool): True, if the connection uses TLS
|
|
73
|
+
env_file (str): Path to the .env file
|
|
74
|
+
yaml_file (str): Path to the YAML configuration file
|
|
75
|
+
"""
|
|
76
|
+
self.yaml_config = None # Initialize the attribute to store YAML data
|
|
77
|
+
self.yaml_mode = (
|
|
78
|
+
False # Initialize the attribute to store the mode of operation
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if env_file and yaml_file:
|
|
82
|
+
logger.info(f"Loading configuration from {env_file} and {yaml_file}.")
|
|
83
|
+
self.load_config_from_files(env_file, yaml_file)
|
|
84
|
+
self.yaml_mode = True
|
|
85
|
+
else:
|
|
86
|
+
logger.info("Loading configuration from arguments.")
|
|
87
|
+
self.username = username
|
|
88
|
+
self.password = password
|
|
89
|
+
self.host = host
|
|
90
|
+
self.rabbitmq_port = rabbitmq_port
|
|
91
|
+
self.keycloak_port = keycloak_port
|
|
92
|
+
self.keycloak_realm = keycloak_realm
|
|
93
|
+
self.client_id = client_id
|
|
94
|
+
self.client_secret_key = client_secret_key
|
|
95
|
+
self.virtual_host = virtual_host
|
|
96
|
+
self.is_tls = is_tls
|
|
97
|
+
|
|
98
|
+
def load_config_from_files(self, env_file: str, yaml_file: str):
|
|
99
|
+
"""
|
|
100
|
+
Loads configuration from .env and YAML files.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
env_file (str): Path to the .env file
|
|
104
|
+
yaml_file (str): Path to the YAML configuration file
|
|
105
|
+
"""
|
|
106
|
+
# Load .env file
|
|
107
|
+
credentials = dotenv_values(env_file)
|
|
108
|
+
self.username = credentials["USERNAME"]
|
|
109
|
+
self.password = credentials["PASSWORD"]
|
|
110
|
+
self.client_id = credentials["CLIENT_ID"]
|
|
111
|
+
self.client_secret_key = credentials["CLIENT_SECRET_KEY"]
|
|
112
|
+
|
|
113
|
+
# Load YAML file
|
|
114
|
+
with open(yaml_file, "r") as file:
|
|
115
|
+
yaml_data = yaml.safe_load(file)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
config = Config(**yaml_data)
|
|
119
|
+
self.yaml_config = yaml_data
|
|
120
|
+
except ValidationError as e:
|
|
121
|
+
raise ValueError(f"Invalid configuration: {e}")
|
|
122
|
+
|
|
123
|
+
self.host = config.servers.rabbitmq.host
|
|
124
|
+
self.rabbitmq_port = config.servers.rabbitmq.port
|
|
125
|
+
self.keycloak_port = config.servers.keycloak.port
|
|
126
|
+
self.keycloak_realm = config.servers.keycloak.realm
|
|
127
|
+
self.virtual_host = config.servers.rabbitmq.virtual_host
|
|
128
|
+
self.is_tls = config.servers.rabbitmq.tls and config.servers.keycloak.tls
|
|
129
|
+
|
|
130
|
+
if not self.is_tls:
|
|
131
|
+
raise ValueError("TLS must be enabled for both RabbitMQ and Keycloak.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ShutDownObserver(Observer):
|
|
135
|
+
"""
|
|
136
|
+
Observer that shuts down an application after scenario termination.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
app (:obj:`Application`): application to be shut down after termination
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, app: "Application"):
|
|
143
|
+
"""
|
|
144
|
+
Initializes a new shut down observer.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
app (:obj:`Application`): application to be shut down after termination
|
|
148
|
+
"""
|
|
149
|
+
self.app = app
|
|
150
|
+
|
|
151
|
+
def on_change(
|
|
152
|
+
self, source: object, property_name: str, old_value: object, new_value: object
|
|
153
|
+
) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Shuts down the application in response to a transition to the TERMINATED mode.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
source (object): object that triggered a property change
|
|
159
|
+
property_name (str): name of the changed property
|
|
160
|
+
old_value (obj): old value of the named property
|
|
161
|
+
new_value (obj): new value of the named property
|
|
162
|
+
"""
|
|
163
|
+
if property_name == Simulator.PROPERTY_MODE and new_value == Mode.TERMINATED:
|
|
164
|
+
self.app.shut_down()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TimeStatusPublisher(ScenarioTimeIntervalPublisher):
|
|
168
|
+
"""
|
|
169
|
+
Publishes time status messages for an application at a regular interval.
|
|
170
|
+
|
|
171
|
+
Attributes:
|
|
172
|
+
app (:obj:`Application`): application to publish time status messages
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def publish_message(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Publishes a time status message.
|
|
178
|
+
"""
|
|
179
|
+
status = TimeStatus.model_validate(
|
|
180
|
+
{
|
|
181
|
+
"name": self.app.app_name,
|
|
182
|
+
"description": self.app.app_description,
|
|
183
|
+
"properties": {
|
|
184
|
+
"simTime": self.app.simulator.get_time(),
|
|
185
|
+
"time": self.app.simulator.get_wallclock_time(),
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
logger.info(
|
|
190
|
+
f"Sending time status {status.model_dump_json(by_alias=True,exclude_none=True)}."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self.app.send_message(
|
|
194
|
+
app_name=self.app.app_name,
|
|
195
|
+
app_topics="status.time",
|
|
196
|
+
payload=status.model_dump_json(by_alias=True, exclude_none=True),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ModeStatusObserver(Observer):
|
|
201
|
+
"""
|
|
202
|
+
Observer that publishes mode status messages for an application.
|
|
203
|
+
|
|
204
|
+
Attributes:
|
|
205
|
+
app (:obj:`Application`): application to publish mode status messages
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, app: "Application"):
|
|
209
|
+
"""
|
|
210
|
+
Initializes a new mode status observer.
|
|
211
|
+
"""
|
|
212
|
+
self.app = app
|
|
213
|
+
|
|
214
|
+
def stop_application(self):
|
|
215
|
+
"""
|
|
216
|
+
Stops the application by closing the channel and connection if they are open.
|
|
217
|
+
"""
|
|
218
|
+
if self.app.channel.is_open:
|
|
219
|
+
self.app.channel.close()
|
|
220
|
+
logger.info(f"Channel closed for application {self.app.app_name}.")
|
|
221
|
+
|
|
222
|
+
if self.app.connection.is_open:
|
|
223
|
+
self.app.connection.close()
|
|
224
|
+
logger.info(f"Connection closed for application {self.app.app_name}.")
|
|
225
|
+
|
|
226
|
+
logger.info(f'Application "{self.app.app_name}" successfully stopped.')
|
|
227
|
+
|
|
228
|
+
def on_change(
|
|
229
|
+
self, source: object, property_name: str, old_value: object, new_value: object
|
|
230
|
+
) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Publishes a mode status message in response to a mode transition.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
source (object): observable that triggered the change
|
|
236
|
+
property_name (str): name of the changed property
|
|
237
|
+
old_value (obj): old value of the named property
|
|
238
|
+
new_value (obj): new value of the named property
|
|
239
|
+
"""
|
|
240
|
+
if property_name == Simulator.PROPERTY_MODE:
|
|
241
|
+
status = ModeStatus.model_validate(
|
|
242
|
+
{
|
|
243
|
+
"name": self.app.app_name,
|
|
244
|
+
"description": self.app.app_description,
|
|
245
|
+
"properties": {"mode": self.app.simulator.get_mode()},
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
logger.info(
|
|
249
|
+
f"Sending mode status {status.model_dump_json(by_alias=True, exclude_none=True)}."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Ensure self.prefix is a string
|
|
253
|
+
if not isinstance(self.app.prefix, str):
|
|
254
|
+
raise ValueError(f"Exchange ({self.app.prefix}) must be a string")
|
|
255
|
+
|
|
256
|
+
# Declare the topic exchange
|
|
257
|
+
if self.app.channel.is_open and self.app.connection.is_open:
|
|
258
|
+
self.app.send_message(
|
|
259
|
+
app_name=self.app.app_name,
|
|
260
|
+
app_topics="status.mode",
|
|
261
|
+
payload=status.model_dump_json(by_alias=True, exclude_none=True),
|
|
262
|
+
)
|