nost-tools 2.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 +29 -0
- nost_tools/application.py +793 -0
- nost_tools/application_utils.py +262 -0
- nost_tools/configuration.py +304 -0
- nost_tools/entity.py +73 -0
- nost_tools/errors.py +14 -0
- nost_tools/logger_application.py +192 -0
- nost_tools/managed_application.py +261 -0
- nost_tools/manager.py +472 -0
- nost_tools/observer.py +181 -0
- nost_tools/publisher.py +141 -0
- nost_tools/schemas.py +426 -0
- nost_tools/simulator.py +531 -0
- nost_tools-2.0.0.dist-info/METADATA +119 -0
- nost_tools-2.0.0.dist-info/RECORD +18 -0
- nost_tools-2.0.0.dist-info/WHEEL +5 -0
- nost_tools-2.0.0.dist-info/licenses/LICENSE +29 -0
- nost_tools-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +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
|
+
)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Settings.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from dotenv import find_dotenv, load_dotenv
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from .errors import ConfigAssertionError, ConfigurationError, EnvironmentVariableError
|
|
13
|
+
from .schemas import (
|
|
14
|
+
ChannelConfig,
|
|
15
|
+
Config,
|
|
16
|
+
Credentials,
|
|
17
|
+
ExchangeConfig,
|
|
18
|
+
ExecConfig,
|
|
19
|
+
KeycloakConfig,
|
|
20
|
+
RabbitMQConfig,
|
|
21
|
+
RuntimeConfig,
|
|
22
|
+
ServersConfig,
|
|
23
|
+
SimulationConfig,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
class ConnectionConfig:
|
|
29
|
+
"""Connection configuration.
|
|
30
|
+
|
|
31
|
+
The configuration settings to establish a connection to the broker, including authentication for the
|
|
32
|
+
user and identification of the server.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
username (str): client username, provided by NOS-T operator
|
|
36
|
+
password (str): client password, provided by NOS-T operator
|
|
37
|
+
host (str): broker hostname
|
|
38
|
+
rabbitmq_port (int): RabbitMQ broker port number
|
|
39
|
+
keycloak_port (int): Keycloak IAM port number
|
|
40
|
+
keycloak_realm (str): Keycloak realm name
|
|
41
|
+
client_id (str): Keycloak client ID
|
|
42
|
+
client_secret_key (str): Keycloak client secret key
|
|
43
|
+
virtual_host (str): RabbitMQ virtual host
|
|
44
|
+
is_tls (bool): True, if the connection uses Transport Layer Security (TLS)
|
|
45
|
+
yaml_file (str): Path to the YAML configuration file
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
username: str = None,
|
|
51
|
+
password: str = None,
|
|
52
|
+
rabbitmq_host: str = None,
|
|
53
|
+
rabbitmq_port: int = None,
|
|
54
|
+
keycloak_host: str = None,
|
|
55
|
+
keycloak_port: int = None,
|
|
56
|
+
keycloak_realm: str = None,
|
|
57
|
+
client_id: str = None,
|
|
58
|
+
client_secret_key: str = None,
|
|
59
|
+
virtual_host: str = None,
|
|
60
|
+
is_tls: bool = True,
|
|
61
|
+
yaml_file: str = None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Initializes a new connection configuration.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
username (str): client username, provided by NOS-T operator
|
|
68
|
+
password (str): client password, provided by NOS-T operator
|
|
69
|
+
host (str): broker hostname
|
|
70
|
+
rabbitmq_port (int): RabbitMQ broker port number
|
|
71
|
+
keycloak_port (int): Keycloak IAM port number
|
|
72
|
+
keycloak_realm (str): Keycloak realm name
|
|
73
|
+
client_id (str): Keycloak client ID
|
|
74
|
+
client_secret_key (str): Keycloak client secret key
|
|
75
|
+
virtual_host (str): RabbitMQ virtual host
|
|
76
|
+
is_tls (bool): True, if the connection uses TLS
|
|
77
|
+
yaml_file (str): Path to the YAML configuration file
|
|
78
|
+
"""
|
|
79
|
+
self.username = username
|
|
80
|
+
self.password = password
|
|
81
|
+
self.rabbitmq_host = rabbitmq_host
|
|
82
|
+
self.keycloak_host = keycloak_host
|
|
83
|
+
self.rabbitmq_port = rabbitmq_port
|
|
84
|
+
self.keycloak_port = keycloak_port
|
|
85
|
+
self.keycloak_realm = keycloak_realm
|
|
86
|
+
self.client_id = client_id
|
|
87
|
+
self.client_secret_key = client_secret_key
|
|
88
|
+
self.virtual_host = virtual_host
|
|
89
|
+
self.is_tls = is_tls
|
|
90
|
+
|
|
91
|
+
self.yaml_config = None
|
|
92
|
+
self.predefined_exchanges_queues = False
|
|
93
|
+
self.yaml_file = yaml_file
|
|
94
|
+
self.unique_exchanges = {}
|
|
95
|
+
self.channel_configs = []
|
|
96
|
+
|
|
97
|
+
self.create_connection_config()
|
|
98
|
+
|
|
99
|
+
def get_exchanges(self):
|
|
100
|
+
"""
|
|
101
|
+
Get exchanges from the YAML configuration file.
|
|
102
|
+
"""
|
|
103
|
+
for app, app_channels in self.yaml_config.channels.items():
|
|
104
|
+
for channel, details in app_channels.items():
|
|
105
|
+
bindings = details.get("bindings", {}).get("amqp", {})
|
|
106
|
+
exchange = bindings.get("exchange", {})
|
|
107
|
+
exchange_name = exchange.get("name")
|
|
108
|
+
if exchange_name:
|
|
109
|
+
exchange_config = ExchangeConfig(
|
|
110
|
+
name=exchange_name,
|
|
111
|
+
type=exchange.get("type", "topic"),
|
|
112
|
+
durable=exchange.get("durable", True),
|
|
113
|
+
auto_delete=exchange.get("autoDelete", False),
|
|
114
|
+
vhost=exchange.get("vhost", "/"),
|
|
115
|
+
)
|
|
116
|
+
if exchange_name in self.unique_exchanges:
|
|
117
|
+
if (
|
|
118
|
+
self.unique_exchanges[exchange_name]
|
|
119
|
+
!= exchange_config.model_dump()
|
|
120
|
+
):
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Conflicting configurations for exchange '{exchange_name}': {self.unique_exchanges[exchange_name]} vs {exchange_config.model_dump()}"
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
self.unique_exchanges[exchange_name] = (
|
|
126
|
+
exchange_config.model_dump()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_channels(self):
|
|
130
|
+
"""
|
|
131
|
+
Get channels from the YAML configuration file.
|
|
132
|
+
"""
|
|
133
|
+
for app, app_channels in self.yaml_config.channels.items():
|
|
134
|
+
for channel, details in app_channels.items():
|
|
135
|
+
bindings = details.get("bindings", {}).get("amqp", {})
|
|
136
|
+
exchange = bindings.get("exchange", {})
|
|
137
|
+
exchange_name = exchange.get("name")
|
|
138
|
+
address = details.get("address")
|
|
139
|
+
if address and bindings:
|
|
140
|
+
channel_config = ChannelConfig(
|
|
141
|
+
app=app,
|
|
142
|
+
address=address,
|
|
143
|
+
exchange=exchange_name or "default_exchange",
|
|
144
|
+
durable=exchange.get("durable", True),
|
|
145
|
+
auto_delete=exchange.get("autoDelete", False),
|
|
146
|
+
vhost=exchange.get("vhost", "/"),
|
|
147
|
+
)
|
|
148
|
+
self.channel_configs.append(channel_config.model_dump())
|
|
149
|
+
|
|
150
|
+
def get_exchanges_channels(self):
|
|
151
|
+
"""
|
|
152
|
+
Get exchanges and channels from the YAML configuration file.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
self.get_exchanges(), self.get_channels()
|
|
156
|
+
if self.unique_exchanges and self.channel_configs:
|
|
157
|
+
self.predefined_exchanges_queues = True
|
|
158
|
+
self.simulation_config = SimulationConfig(
|
|
159
|
+
exchanges=self.unique_exchanges,
|
|
160
|
+
queues=self.channel_configs,
|
|
161
|
+
execution_parameters=self.yaml_config.execution,
|
|
162
|
+
predefined_exchanges_queues=self.predefined_exchanges_queues,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def load_environment_variables(self):
|
|
166
|
+
"""
|
|
167
|
+
Loads an environment (.env) file and returns the parsed data.
|
|
168
|
+
"""
|
|
169
|
+
dotenv_path = find_dotenv(usecwd=True)
|
|
170
|
+
if dotenv_path:
|
|
171
|
+
logger.info(f"Checking for credentials in the .env file: {dotenv_path}.")
|
|
172
|
+
load_dotenv(dotenv_path, override=True)
|
|
173
|
+
else:
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Checking for credentials in the system environment variables."
|
|
176
|
+
)
|
|
177
|
+
if self.server_config.servers.rabbitmq.keycloak_authentication:
|
|
178
|
+
required_fields = [
|
|
179
|
+
"USERNAME",
|
|
180
|
+
"PASSWORD",
|
|
181
|
+
"CLIENT_ID",
|
|
182
|
+
"CLIENT_SECRET_KEY",
|
|
183
|
+
]
|
|
184
|
+
else:
|
|
185
|
+
required_fields = ["USERNAME", "PASSWORD"]
|
|
186
|
+
|
|
187
|
+
env_data = {field: os.getenv(field) for field in required_fields}
|
|
188
|
+
|
|
189
|
+
missing_fields = [field for field, value in env_data.items() if value is None]
|
|
190
|
+
if missing_fields:
|
|
191
|
+
raise EnvironmentVariableError(
|
|
192
|
+
f"Missing required fields in .env file: {', '.join(missing_fields)}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
if self.server_config.servers.rabbitmq.keycloak_authentication:
|
|
197
|
+
self.credentials_config = Credentials(
|
|
198
|
+
username=env_data["USERNAME"],
|
|
199
|
+
password=env_data["PASSWORD"],
|
|
200
|
+
client_id=env_data["CLIENT_ID"],
|
|
201
|
+
client_secret_key=env_data["CLIENT_SECRET_KEY"],
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
self.credentials_config = Credentials(
|
|
205
|
+
username=env_data["USERNAME"],
|
|
206
|
+
password=env_data["PASSWORD"],
|
|
207
|
+
)
|
|
208
|
+
except ValidationError as err:
|
|
209
|
+
raise EnvironmentVariableError(f"Invalid environment variables: {err}")
|
|
210
|
+
|
|
211
|
+
def load_yaml_config_file(self):
|
|
212
|
+
"""
|
|
213
|
+
Loads a YAML configuration file and returns the parsed data.
|
|
214
|
+
"""
|
|
215
|
+
if not os.path.exists(self.yaml_file):
|
|
216
|
+
raise ConfigurationError("Couldn't load config file (not found)")
|
|
217
|
+
|
|
218
|
+
with open(self.yaml_file, "r", encoding="utf-8") as f:
|
|
219
|
+
try:
|
|
220
|
+
yaml_data = yaml.safe_load(f) # Store the parsed YAML data
|
|
221
|
+
except yaml.YAMLError as err:
|
|
222
|
+
raise ConfigurationError(f"Invalid YAML configuration: {err}")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
self.yaml_config = Config(**yaml_data)
|
|
226
|
+
except ValidationError as err:
|
|
227
|
+
raise ConfigurationError(f"Invalid configuration: {err}")
|
|
228
|
+
|
|
229
|
+
def create_connection_config(self):
|
|
230
|
+
"""
|
|
231
|
+
Creates a connection configuration.
|
|
232
|
+
"""
|
|
233
|
+
if self.yaml_file:
|
|
234
|
+
try:
|
|
235
|
+
self.load_yaml_config_file()
|
|
236
|
+
except ConfigurationError as e:
|
|
237
|
+
raise ValueError(f"Configuration error: {e}")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
assert all(
|
|
241
|
+
item in self.yaml_config.execution.required_apps
|
|
242
|
+
for item in self.yaml_config.channels.keys()
|
|
243
|
+
), "Application names do not match the channels defined in the configuration file."
|
|
244
|
+
except ConfigAssertionError as e:
|
|
245
|
+
raise ValueError(f"Assertion error: {e}")
|
|
246
|
+
else:
|
|
247
|
+
try:
|
|
248
|
+
self.yaml_config = Config(
|
|
249
|
+
servers=ServersConfig(
|
|
250
|
+
rabbitmq=RabbitMQConfig(
|
|
251
|
+
host=self.rabbitmq_host,
|
|
252
|
+
port=self.rabbitmq_port,
|
|
253
|
+
virtual_host=self.virtual_host,
|
|
254
|
+
tls=self.is_tls,
|
|
255
|
+
),
|
|
256
|
+
keycloak=KeycloakConfig(
|
|
257
|
+
host=self.keycloak_host,
|
|
258
|
+
port=self.keycloak_port,
|
|
259
|
+
realm=self.keycloak_realm,
|
|
260
|
+
tls=self.is_tls,
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
channels={},
|
|
264
|
+
execution=ExecConfig(),
|
|
265
|
+
)
|
|
266
|
+
except:
|
|
267
|
+
self.yaml_config = Config(
|
|
268
|
+
servers=ServersConfig(
|
|
269
|
+
rabbitmq=RabbitMQConfig(), keycloak=KeycloakConfig()
|
|
270
|
+
),
|
|
271
|
+
channels={},
|
|
272
|
+
execution=ExecConfig(),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self.get_exchanges_channels()
|
|
276
|
+
|
|
277
|
+
server_config = self.yaml_config.copy()
|
|
278
|
+
if hasattr(server_config, "channels"):
|
|
279
|
+
del server_config.channels
|
|
280
|
+
if hasattr(server_config, "execution"):
|
|
281
|
+
del server_config.execution
|
|
282
|
+
self.server_config = server_config
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
self.username is not None
|
|
286
|
+
and self.password is not None
|
|
287
|
+
and self.client_id is not None
|
|
288
|
+
and self.client_secret_key is not None
|
|
289
|
+
):
|
|
290
|
+
logger.info("Using provided credentials.")
|
|
291
|
+
self.credentials_config = Credentials(
|
|
292
|
+
username=self.username,
|
|
293
|
+
password=self.password,
|
|
294
|
+
client_id=self.client_id,
|
|
295
|
+
client_secret_key=self.client_secret_key,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
self.load_environment_variables()
|
|
299
|
+
|
|
300
|
+
self.rc = RuntimeConfig(
|
|
301
|
+
credentials=self.credentials_config,
|
|
302
|
+
server_configuration=server_config,
|
|
303
|
+
simulation_configuration=self.simulation_config,
|
|
304
|
+
)
|
nost_tools/entity.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides a base class to maintain state variables during scenario execution.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
from .observer import Observable
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Entity(Observable):
|
|
14
|
+
"""
|
|
15
|
+
A base entity that maintains its own clock (time) during scenario execution.
|
|
16
|
+
|
|
17
|
+
Notifies observers of changes to one observable property
|
|
18
|
+
* `time`: current scenario time
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
name (str): The entity name (optional)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
PROPERTY_TIME = "time"
|
|
25
|
+
|
|
26
|
+
def __init__(self, name: str = None):
|
|
27
|
+
"""
|
|
28
|
+
Initializes a new entity.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name (str): name of the entity (default: None)
|
|
32
|
+
"""
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.name = name
|
|
35
|
+
self._init_time = self._time = self._next_time = None
|
|
36
|
+
|
|
37
|
+
def initialize(self, init_time: datetime) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initializes the entity at a designated initial scenario time.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
init_time (:obj:`datetime`): initial scenario time
|
|
43
|
+
"""
|
|
44
|
+
self._init_time = self._time = self._next_time = init_time
|
|
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
|
|
54
|
+
|
|
55
|
+
def tock(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Commits the state transition pre-computed in `tick` and notifies observers of changes.
|
|
58
|
+
"""
|
|
59
|
+
# update the time
|
|
60
|
+
if self._time != self._next_time:
|
|
61
|
+
prev_time = self._time
|
|
62
|
+
self._time = self._next_time
|
|
63
|
+
logger.debug(f"Entity {self.name} updated time to {self._time}.")
|
|
64
|
+
self.notify_observers(self.PROPERTY_TIME, prev_time, self._time)
|
|
65
|
+
|
|
66
|
+
def get_time(self) -> datetime:
|
|
67
|
+
"""
|
|
68
|
+
Retrieves the current scenario time.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
:obj:`datetime`: current scenario time
|
|
72
|
+
"""
|
|
73
|
+
return self._time
|
nost_tools/errors.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides object models for common data structures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(Exception):
|
|
6
|
+
"""Configuration error"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EnvironmentVariableError(Exception):
|
|
10
|
+
"""Environment variable error"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigAssertionError(Exception): # Renamed to avoid shadowing built-in
|
|
14
|
+
"""Assertion error for configuration validation"""
|