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.

@@ -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
+ )