gridappsd-python 2026.2.1a1__tar.gz → 2026.2.1a2__tar.gz
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.
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/PKG-INFO +1 -1
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/app_registration.py +35 -10
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/goss.py +47 -53
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/gridappsd.py +20 -13
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/simulation.py +23 -19
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/timeseries.py +16 -22
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/pyproject.toml +1 -1
- gridappsd_python-2026.2.1a2/tests/test_app_registration.py +63 -0
- gridappsd_python-2026.2.1a2/tests/test_goss.py +128 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_gridappsd.py +71 -2
- gridappsd_python-2026.2.1a2/tests/test_simulation_commands.py +93 -0
- gridappsd_python-2026.2.1a2/tests/test_timeseries.py +66 -0
- gridappsd_python-2026.2.1a1/tests/test_goss.py +0 -204
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/.gitignore +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/CHANGELOG.md +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/README.md +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/conf/entrypoint.sh +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/conf/run-gridappsd.sh +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/example.env +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/__init__.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/__main__.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/difference_builder.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/docker_handler.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/field_interface/__init__.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/houses.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/json_extension.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/loghandler.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/register_app.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/topics.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/utils.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd_python_testing.ipynb +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/info/CHANGELOG.md +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/info/VERSION +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/poetry.toml +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/conftest.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/run_gridappsd.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/simulation_fixtures/13_node_2_min_base.json +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_containers.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_difference_builder.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_docker_handler.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_houses.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_json.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_logging.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_logging_integration.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_simulation.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_topics.py +0 -0
- {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/unittest_test_template.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
from enum import Enum
|
|
4
5
|
from queue import Queue
|
|
5
6
|
import time
|
|
6
7
|
import subprocess
|
|
@@ -14,6 +15,30 @@ from . import utils, json_extension as json
|
|
|
14
15
|
|
|
15
16
|
_log = logging.getLogger(__name__)
|
|
16
17
|
|
|
18
|
+
GRIDAPPSD_APPLICATION_STATUS = "GRIDAPPSD_APPLICATION_STATUS"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApplicationStatusEnum(Enum):
|
|
22
|
+
"""Values this module writes to the GRIDAPPSD_APPLICATION_STATUS environment variable.
|
|
23
|
+
|
|
24
|
+
This is a distinct enum from gridappsd.utils.ProcessStatusEnum: this module's
|
|
25
|
+
STOPPED value has no equivalent in ProcessStatusEnum (which has CLOSED
|
|
26
|
+
instead), so reusing that enum here would either drop STOPPED or introduce
|
|
27
|
+
a mismatch between the value written and the value a reader expects.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
STARTING = "STARTING"
|
|
31
|
+
STOPPING = "STOPPING"
|
|
32
|
+
RUNNING = "RUNNING"
|
|
33
|
+
STOPPED = "STOPPED"
|
|
34
|
+
ERROR = "ERROR"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _set_application_status(status: ApplicationStatusEnum) -> None:
|
|
38
|
+
"""Write status to the GRIDAPPSD_APPLICATION_STATUS environment variable."""
|
|
39
|
+
os.environ[GRIDAPPSD_APPLICATION_STATUS] = status.value
|
|
40
|
+
|
|
41
|
+
|
|
17
42
|
# determine OS type
|
|
18
43
|
posix = False
|
|
19
44
|
if os.name == "posix":
|
|
@@ -35,20 +60,20 @@ class Job(threading.Thread):
|
|
|
35
60
|
def run(self):
|
|
36
61
|
try:
|
|
37
62
|
self.running = True
|
|
38
|
-
|
|
63
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
39
64
|
|
|
40
65
|
p = subprocess.Popen(args=self._args, shell=False, stdout=self._out, stderr=self._err)
|
|
41
66
|
|
|
42
67
|
# Loop while process is executing
|
|
43
68
|
while p.poll() is None and self.running:
|
|
44
|
-
|
|
69
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
45
70
|
time.sleep(1)
|
|
46
71
|
|
|
47
72
|
except Exception as e:
|
|
48
|
-
|
|
73
|
+
_set_application_status(ApplicationStatusEnum.ERROR)
|
|
49
74
|
_log.error(repr(e))
|
|
50
75
|
else:
|
|
51
|
-
|
|
76
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
52
77
|
|
|
53
78
|
|
|
54
79
|
class ApplicationController(object):
|
|
@@ -58,7 +83,7 @@ class ApplicationController(object):
|
|
|
58
83
|
if not isinstance(gridappsd, GridAPPSD):
|
|
59
84
|
raise ValueError("Invalid gridappsd instance passed.")
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
62
87
|
self._configDict = config.copy()
|
|
63
88
|
self._validate_config()
|
|
64
89
|
self._gapd = gridappsd
|
|
@@ -80,7 +105,7 @@ class ApplicationController(object):
|
|
|
80
105
|
self._end_callback = None
|
|
81
106
|
self._print_queue = Queue()
|
|
82
107
|
self._heartbeat_thread = None
|
|
83
|
-
|
|
108
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
84
109
|
|
|
85
110
|
if "type" not in self._configDict or self._configDict["type"] != "REMOTE":
|
|
86
111
|
_log.warning(
|
|
@@ -119,7 +144,7 @@ class ApplicationController(object):
|
|
|
119
144
|
self._stop_control_topic = response.get("stopControlTopic")
|
|
120
145
|
|
|
121
146
|
os.environ["GRIDAPPSD_APPLICATION_ID"] = self._application_id
|
|
122
|
-
|
|
147
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
123
148
|
|
|
124
149
|
self._gapd.subscribe(self._stop_control_topic, self.__handle_stop)
|
|
125
150
|
self._gapd.subscribe(self._start_control_topic, self.__handle_start)
|
|
@@ -160,7 +185,7 @@ class ApplicationController(object):
|
|
|
160
185
|
obj = json.loads(message)
|
|
161
186
|
else:
|
|
162
187
|
obj = message
|
|
163
|
-
|
|
188
|
+
_set_application_status(ApplicationStatusEnum.STARTING)
|
|
164
189
|
self._gapd.get_logger().debug("Handling Start: {}\ndict:\n{}".format(headers, obj))
|
|
165
190
|
|
|
166
191
|
if "command" not in obj:
|
|
@@ -176,12 +201,12 @@ class ApplicationController(object):
|
|
|
176
201
|
|
|
177
202
|
def __handle_stop(self, headers, message):
|
|
178
203
|
print("Handling Stop: {} {}".format(headers, message))
|
|
179
|
-
|
|
204
|
+
_set_application_status(ApplicationStatusEnum.STOPPING)
|
|
180
205
|
if self._thread:
|
|
181
206
|
self._thread.join()
|
|
182
207
|
if self._end_callback is not None:
|
|
183
208
|
self._end_callback()
|
|
184
|
-
|
|
209
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
185
210
|
|
|
186
211
|
def shutdown(self):
|
|
187
212
|
self._shutting_down = True
|
|
@@ -57,7 +57,6 @@ from enum import Enum
|
|
|
57
57
|
from logging import Logger
|
|
58
58
|
from queue import Queue
|
|
59
59
|
|
|
60
|
-
import stomp as _stomp_module
|
|
61
60
|
from stomp import Connection12 as Connection
|
|
62
61
|
from stomp.exception import NotConnectedException
|
|
63
62
|
from time import sleep
|
|
@@ -66,19 +65,41 @@ from gridappsd import json_extension as json
|
|
|
66
65
|
|
|
67
66
|
_log: Logger = logging.getLogger(inspect.getmodulename(__file__))
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
|
|
69
|
+
def _unpack_stomp_args(*args):
|
|
70
|
+
"""Return (headers, body) from a stomp listener callback's arguments.
|
|
71
|
+
|
|
72
|
+
stomp-py pre 8.x, and this module's own CallbackRouter dispatch, call
|
|
73
|
+
on_message/on_error with two positional arguments: headers and body.
|
|
74
|
+
stomp-py 8.x's raw listener protocol calls with a single Frame object
|
|
75
|
+
exposing .headers and .body instead. A listener reached through
|
|
76
|
+
CallbackRouter (any listener registered via subscribe()) always
|
|
77
|
+
receives the two argument shape, regardless of the installed stomp-py
|
|
78
|
+
version, because CallbackRouter.run_callbacks is the caller, not
|
|
79
|
+
stomp itself. Only a listener registered directly on a raw stomp
|
|
80
|
+
Connection (CallbackRouter itself, TokenResponseListener) is actually
|
|
81
|
+
invoked by stomp's own version dependent dispatch. Detecting the
|
|
82
|
+
argument shape at each call, instead of trusting a single global
|
|
83
|
+
version sniffed flag, is correct for both callers.
|
|
84
|
+
"""
|
|
85
|
+
if len(args) >= 2:
|
|
86
|
+
return args[0], args[1]
|
|
87
|
+
frame = args[0]
|
|
88
|
+
if hasattr(frame, "headers") and hasattr(frame, "body"):
|
|
89
|
+
return frame.headers, frame.body
|
|
90
|
+
headers, body = frame
|
|
91
|
+
return headers, body
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _serialize_message(message):
|
|
95
|
+
"""Return message ready to send on the wire.
|
|
96
|
+
|
|
97
|
+
A list or dict body is serialized to a JSON string; any other body
|
|
98
|
+
(already a string, bytes, etc.) is passed through unchanged.
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(message, (list, dict)):
|
|
101
|
+
return json.dumps(message)
|
|
102
|
+
return message
|
|
82
103
|
|
|
83
104
|
|
|
84
105
|
class GRIDAPPSD_ENV_ENUM(Enum):
|
|
@@ -168,8 +189,7 @@ class GOSS(object):
|
|
|
168
189
|
|
|
169
190
|
def send(self, topic, message):
|
|
170
191
|
self._make_connection()
|
|
171
|
-
|
|
172
|
-
message = json.dumps(message)
|
|
192
|
+
message = _serialize_message(message)
|
|
173
193
|
_log.debug("Sending topic: {} body: {}".format(topic, message))
|
|
174
194
|
self._conn.send(
|
|
175
195
|
body=message, destination=topic, headers={"GOSS_HAS_SUBJECT": True, "GOSS_SUBJECT": self.__token}
|
|
@@ -185,11 +205,8 @@ class GOSS(object):
|
|
|
185
205
|
if "resultFormat" in message:
|
|
186
206
|
self.result_format = message["resultFormat"]
|
|
187
207
|
|
|
188
|
-
# Change message to string if we have a dictionary.
|
|
189
|
-
|
|
190
|
-
message = json.dumps(message)
|
|
191
|
-
elif isinstance(message, list):
|
|
192
|
-
message = json.dumps(message)
|
|
208
|
+
# Change message to string if we have a dictionary or list.
|
|
209
|
+
message = _serialize_message(message)
|
|
193
210
|
|
|
194
211
|
class ResponseListener(object):
|
|
195
212
|
def __init__(self, topic, result_format):
|
|
@@ -198,11 +215,7 @@ class GOSS(object):
|
|
|
198
215
|
self.result_format = result_format
|
|
199
216
|
|
|
200
217
|
def on_message(self, *args):
|
|
201
|
-
|
|
202
|
-
frame = args[0]
|
|
203
|
-
header, message = frame.headers, frame.body
|
|
204
|
-
else:
|
|
205
|
-
header, message = args[0], args[1]
|
|
218
|
+
header, message = _unpack_stomp_args(*args)
|
|
206
219
|
_log.debug("Internal on message is: {} {}".format(header, message))
|
|
207
220
|
try:
|
|
208
221
|
if self.result_format == "JSON":
|
|
@@ -216,11 +229,7 @@ class GOSS(object):
|
|
|
216
229
|
self.response = dict(error="Invalid json returned", header=header, message=message)
|
|
217
230
|
|
|
218
231
|
def on_error(self, *args):
|
|
219
|
-
|
|
220
|
-
frame = args[0]
|
|
221
|
-
headers, message = frame.headers, frame.body
|
|
222
|
-
else:
|
|
223
|
-
headers, message = args[0], args[1]
|
|
232
|
+
headers, message = _unpack_stomp_args(*args)
|
|
224
233
|
_log.error("ERR: {}".format(headers))
|
|
225
234
|
_log.error("OUR ERROR: {}".format(message))
|
|
226
235
|
|
|
@@ -337,21 +346,13 @@ class GOSS(object):
|
|
|
337
346
|
return self.__token
|
|
338
347
|
|
|
339
348
|
def on_message(self, *args):
|
|
340
|
-
|
|
341
|
-
frame = args[0]
|
|
342
|
-
header, message = frame.headers, frame.body
|
|
343
|
-
else:
|
|
344
|
-
header, message = args[0], args[1]
|
|
349
|
+
header, message = _unpack_stomp_args(*args)
|
|
345
350
|
_log.debug("Internal on message is: {} {}".format(header, message))
|
|
346
351
|
|
|
347
352
|
self.__token = str(message)
|
|
348
353
|
|
|
349
354
|
def on_error(self, *args):
|
|
350
|
-
|
|
351
|
-
frame = args[0]
|
|
352
|
-
headers, message = frame.headers, frame.body
|
|
353
|
-
else:
|
|
354
|
-
headers, message = args[0], args[1]
|
|
355
|
+
headers, message = _unpack_stomp_args(*args)
|
|
355
356
|
_log.error("ERR: {}".format(headers))
|
|
356
357
|
_log.error("OUR ERROR: {}".format(message))
|
|
357
358
|
|
|
@@ -404,9 +405,10 @@ class CallbackRouter(object):
|
|
|
404
405
|
cb, hdrs, msg = self._queue_callerback.get()
|
|
405
406
|
try:
|
|
406
407
|
msg = json.loads(msg)
|
|
407
|
-
except:
|
|
408
|
+
except (TypeError, ValueError):
|
|
409
|
+
# msg was not JSON text (already a dict, or plain string body);
|
|
410
|
+
# pass it through unchanged rather than as a decode failure.
|
|
408
411
|
pass
|
|
409
|
-
# msg = message
|
|
410
412
|
|
|
411
413
|
for c in cb:
|
|
412
414
|
c(hdrs, msg)
|
|
@@ -429,11 +431,7 @@ class CallbackRouter(object):
|
|
|
429
431
|
pass
|
|
430
432
|
|
|
431
433
|
def on_message(self, *args):
|
|
432
|
-
|
|
433
|
-
frame = args[0]
|
|
434
|
-
headers, message = frame.headers, frame.body
|
|
435
|
-
else:
|
|
436
|
-
headers, message = args[0], args[1]
|
|
434
|
+
headers, message = _unpack_stomp_args(*args)
|
|
437
435
|
destination = headers["destination"]
|
|
438
436
|
# _log.debug("Topic map keys are: {keys}".format(keys=self._topics_callback_map.keys()))
|
|
439
437
|
if destination in self._topics_callback_map:
|
|
@@ -442,11 +440,7 @@ class CallbackRouter(object):
|
|
|
442
440
|
_log.error("INVALID DESTINATION {destination}".format(destination=destination))
|
|
443
441
|
|
|
444
442
|
def on_error(self, *args):
|
|
445
|
-
|
|
446
|
-
frame = args[0]
|
|
447
|
-
header, message = frame.headers, frame.body
|
|
448
|
-
else:
|
|
449
|
-
header, message = args[0], args[1]
|
|
443
|
+
header, message = _unpack_stomp_args(*args)
|
|
450
444
|
_log.error("Error in callback router")
|
|
451
445
|
_log.error(header)
|
|
452
446
|
_log.error(message)
|
|
@@ -116,29 +116,32 @@ class GridAPPSD(GOSS):
|
|
|
116
116
|
self._simulation_log_topic = t.simulation_log_topic(self._simulation_id)
|
|
117
117
|
return self._simulation_id
|
|
118
118
|
|
|
119
|
-
def
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
:param status:
|
|
119
|
+
def _set_status(self, status, kind):
|
|
120
|
+
"""Set self._process_status, warning instead of raising on an invalid value.
|
|
121
|
+
|
|
122
|
+
:param status: candidate value for ProcessStatusEnum
|
|
123
|
+
:param kind: human readable label for the warning message, e.g. "application"
|
|
123
124
|
"""
|
|
124
125
|
try:
|
|
125
126
|
self._process_status = ProcessStatusEnum(status)
|
|
126
127
|
except ValueError:
|
|
127
128
|
self.get_logger().warning(
|
|
128
|
-
"Unsuccessful change of
|
|
129
|
+
f"Unsuccessful change of {kind} status." + f"Valid statuses are {ProcessStatusEnum.__members__}."
|
|
129
130
|
)
|
|
130
131
|
|
|
132
|
+
def set_application_status(self, status):
|
|
133
|
+
"""
|
|
134
|
+
Set the application status.
|
|
135
|
+
:param status:
|
|
136
|
+
"""
|
|
137
|
+
self._set_status(status, "application")
|
|
138
|
+
|
|
131
139
|
def set_service_status(self, status):
|
|
132
140
|
"""
|
|
133
141
|
Set the service status.
|
|
134
142
|
:param status:
|
|
135
143
|
"""
|
|
136
|
-
|
|
137
|
-
self._process_status = ProcessStatusEnum(status)
|
|
138
|
-
except ValueError:
|
|
139
|
-
self.get_logger().warning(
|
|
140
|
-
"Unsuccessful change of service status." + f"Valid statuses are {ProcessStatusEnum.__members__}."
|
|
141
|
-
)
|
|
144
|
+
self._set_status(status, "service")
|
|
142
145
|
|
|
143
146
|
def set_simulation_id(self, simulation_id):
|
|
144
147
|
if simulation_id is None:
|
|
@@ -148,12 +151,16 @@ class GridAPPSD(GOSS):
|
|
|
148
151
|
self._simulation_id = simulation_id
|
|
149
152
|
self._simulation_log_topic = t.simulation_log_topic(self._simulation_id)
|
|
150
153
|
|
|
154
|
+
def _get_status(self):
|
|
155
|
+
"""Return self._process_status as its plain string value."""
|
|
156
|
+
return self._process_status.value
|
|
157
|
+
|
|
151
158
|
def get_application_status(self):
|
|
152
159
|
"""
|
|
153
160
|
Return the application status
|
|
154
161
|
:return:
|
|
155
162
|
"""
|
|
156
|
-
return self.
|
|
163
|
+
return self._get_status()
|
|
157
164
|
|
|
158
165
|
def get_application_id(self):
|
|
159
166
|
return utils.get_gridappsd_application_id()
|
|
@@ -163,7 +170,7 @@ class GridAPPSD(GOSS):
|
|
|
163
170
|
Return the service status
|
|
164
171
|
:return:
|
|
165
172
|
"""
|
|
166
|
-
return self.
|
|
173
|
+
return self._get_status()
|
|
167
174
|
|
|
168
175
|
def query_object_types(self, model_id=None):
|
|
169
176
|
"""Allows the caller to query the different object types.
|
|
@@ -58,8 +58,8 @@ class ModelCreationConfig(ConfigBase):
|
|
|
58
58
|
class SimulationArgs(ConfigBase):
|
|
59
59
|
start_time: int = field(default=1655321830)
|
|
60
60
|
duration: int = field(default=300)
|
|
61
|
-
publish_period: int = field(default=None)
|
|
62
|
-
interval: int = field(default=None)
|
|
61
|
+
publish_period: int | None = field(default=None)
|
|
62
|
+
interval: int | None = field(default=None)
|
|
63
63
|
run_realtime: bool = field(default=True)
|
|
64
64
|
pause_after_measurements: bool = field(default=False)
|
|
65
65
|
simulation_name: str = field(default="ieee13nodeckt")
|
|
@@ -75,8 +75,10 @@ class SimulationArgs(ConfigBase):
|
|
|
75
75
|
if not self.publish_period:
|
|
76
76
|
self.publish_period = 60
|
|
77
77
|
if self.publish_period < self.interval:
|
|
78
|
-
raise RuntimeError(
|
|
79
|
-
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
"A simulation's publishing_period cannot be less than the simulation's timestep "
|
|
80
|
+
"interval. please make the publishing_period >= interval!"
|
|
81
|
+
)
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
@dataclass
|
|
@@ -215,26 +217,30 @@ class Simulation:
|
|
|
215
217
|
for p in self.__on_start:
|
|
216
218
|
p(self)
|
|
217
219
|
|
|
218
|
-
def
|
|
219
|
-
"""
|
|
220
|
-
|
|
221
|
-
command
|
|
220
|
+
def _send_simulation_command(self, command_name, **command_input):
|
|
221
|
+
"""Send a command to this simulation's input topic.
|
|
222
|
+
|
|
223
|
+
:param command_name: value of the command field sent to the simulation
|
|
224
|
+
:param command_input: when given, nested under the command's "input" key
|
|
225
|
+
"""
|
|
226
|
+
_log.debug("Sending simulation command: {}".format(command_name))
|
|
227
|
+
command = dict(command=command_name)
|
|
228
|
+
if command_input:
|
|
229
|
+
command["input"] = command_input
|
|
222
230
|
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
223
231
|
self._running_or_paused = True
|
|
224
232
|
|
|
233
|
+
def pause(self):
|
|
234
|
+
"""Pause simulation"""
|
|
235
|
+
self._send_simulation_command("pause")
|
|
236
|
+
|
|
225
237
|
def stop(self):
|
|
226
238
|
"""Stop the simulation"""
|
|
227
|
-
|
|
228
|
-
command = dict(command="stop")
|
|
229
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
230
|
-
self._running_or_paused = True
|
|
239
|
+
self._send_simulation_command("stop")
|
|
231
240
|
|
|
232
241
|
def resume(self):
|
|
233
242
|
"""Resume the simulation"""
|
|
234
|
-
|
|
235
|
-
command = dict(command="resume")
|
|
236
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
237
|
-
self._running_or_paused = True
|
|
243
|
+
self._send_simulation_command("resume")
|
|
238
244
|
|
|
239
245
|
def run_loop(self):
|
|
240
246
|
"""Loop around the running of the simulation itself.
|
|
@@ -265,9 +271,7 @@ class Simulation:
|
|
|
265
271
|
:param pause_in: number of seconds to run before pausing the simulation
|
|
266
272
|
"""
|
|
267
273
|
_log.debug("Resuming simulation. Will pause after {} seconds".format(pause_in))
|
|
268
|
-
|
|
269
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
270
|
-
self._running_or_paused = True
|
|
274
|
+
self._send_simulation_command("resumePauseAt", pauseIn=pause_in)
|
|
271
275
|
|
|
272
276
|
def add_onmeasurement_callback(self, callback, device_filter=()):
|
|
273
277
|
"""registers an onmeasurment callback to be called when measurements have come through.
|
|
@@ -46,38 +46,37 @@ class Query:
|
|
|
46
46
|
self.last = n
|
|
47
47
|
return self
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
49
|
+
def _add_filter(self, operator, value):
|
|
50
|
+
"""Append a single {key, operator: value} filter for the current key.
|
|
51
51
|
|
|
52
|
-
:param
|
|
52
|
+
:param operator: one of "ge", "le", "eq"
|
|
53
|
+
:param value: value to filter on
|
|
53
54
|
"""
|
|
54
55
|
if self.key is None:
|
|
55
56
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
56
|
-
|
|
57
|
-
self.queryFilter.append(obj)
|
|
57
|
+
self.queryFilter.append({"key": self.key, operator: value})
|
|
58
58
|
return self
|
|
59
59
|
|
|
60
|
+
def ge(self, value):
|
|
61
|
+
"""Method to add 'value greater than or equal to' filter to a key.
|
|
62
|
+
|
|
63
|
+
:param value:
|
|
64
|
+
"""
|
|
65
|
+
return self._add_filter("ge", value)
|
|
66
|
+
|
|
60
67
|
def le(self, value):
|
|
61
68
|
"""Method to add 'value less than or equal to' filter to a key.
|
|
62
69
|
|
|
63
70
|
:param value:
|
|
64
71
|
"""
|
|
65
|
-
|
|
66
|
-
raise ValueError("Key is not specified. Call where_key first.")
|
|
67
|
-
obj = {"key": self.key, "le": value}
|
|
68
|
-
self.queryFilter.append(obj)
|
|
69
|
-
return self
|
|
72
|
+
return self._add_filter("le", value)
|
|
70
73
|
|
|
71
74
|
def eq(self, value):
|
|
72
75
|
"""Method to add 'value equal to' filter to a key.
|
|
73
76
|
|
|
74
77
|
:param value:
|
|
75
78
|
"""
|
|
76
|
-
|
|
77
|
-
raise ValueError("Key is not specified. Call where_key first.")
|
|
78
|
-
obj = {"key": self.key, "eq": value}
|
|
79
|
-
self.queryFilter.append(obj)
|
|
80
|
-
return self
|
|
79
|
+
return self._add_filter("eq", value)
|
|
81
80
|
|
|
82
81
|
def between(self, value1, value2):
|
|
83
82
|
"""Method to add 'value between' value1 and value2 filter to a key.
|
|
@@ -85,13 +84,8 @@ class Query:
|
|
|
85
84
|
:param value1: defines 'greater than equal to' filter for key's value
|
|
86
85
|
:param value2: defines 'less than equal to' filter for key's value
|
|
87
86
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
obj = {"key": self.key, "ge": value1}
|
|
91
|
-
self.queryFilter.append(obj)
|
|
92
|
-
obj = {"key": self.key, "le": value2}
|
|
93
|
-
self.queryFilter.append(obj)
|
|
94
|
-
return self
|
|
87
|
+
self._add_filter("ge", value1)
|
|
88
|
+
return self._add_filter("le", value2)
|
|
95
89
|
|
|
96
90
|
def where_key(self, key):
|
|
97
91
|
self.key = key
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unit tests for gridappsd.app_registration status handling.
|
|
2
|
+
|
|
3
|
+
_set_application_status writes ApplicationStatusEnum values to the
|
|
4
|
+
GRIDAPPSD_APPLICATION_STATUS environment variable. These tests assert
|
|
5
|
+
the exact string value written for each status, and that Job.run()
|
|
6
|
+
drives the environment variable through the expected RUNNING then
|
|
7
|
+
STOPPED transition for a command that exits cleanly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from gridappsd.app_registration import (
|
|
15
|
+
GRIDAPPSD_APPLICATION_STATUS,
|
|
16
|
+
ApplicationStatusEnum,
|
|
17
|
+
Job,
|
|
18
|
+
_set_application_status,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def _clear_status_env(monkeypatch):
|
|
24
|
+
monkeypatch.delenv(GRIDAPPSD_APPLICATION_STATUS, raising=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestSetApplicationStatus:
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
"status",
|
|
30
|
+
[
|
|
31
|
+
ApplicationStatusEnum.STARTING,
|
|
32
|
+
ApplicationStatusEnum.STOPPING,
|
|
33
|
+
ApplicationStatusEnum.RUNNING,
|
|
34
|
+
ApplicationStatusEnum.STOPPED,
|
|
35
|
+
ApplicationStatusEnum.ERROR,
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
def test_writes_exact_status_value_to_environment(self, status):
|
|
39
|
+
_set_application_status(status)
|
|
40
|
+
|
|
41
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == status.value
|
|
42
|
+
|
|
43
|
+
def test_overwrites_previous_value(self):
|
|
44
|
+
_set_application_status(ApplicationStatusEnum.STARTING)
|
|
45
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
46
|
+
|
|
47
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == "RUNNING"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestJobRunSetsApplicationStatus:
|
|
51
|
+
def test_successful_job_ends_in_stopped_status(self):
|
|
52
|
+
job = Job(args=["true"])
|
|
53
|
+
|
|
54
|
+
job.run()
|
|
55
|
+
|
|
56
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == ApplicationStatusEnum.STOPPED.value
|
|
57
|
+
|
|
58
|
+
def test_failing_job_ends_in_error_status(self):
|
|
59
|
+
job = Job(args=["/nonexistent-command-for-gadp-044-test"])
|
|
60
|
+
|
|
61
|
+
job.run()
|
|
62
|
+
|
|
63
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == ApplicationStatusEnum.ERROR.value
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Unit tests for gridappsd.goss.
|
|
2
|
+
|
|
3
|
+
These tests exercise the pure argument-shape and serialization helpers
|
|
4
|
+
directly. They do not require a live stomp broker: GOSS is constructed
|
|
5
|
+
with attempt_connection=False where a GOSS/GridAPPSD instance is needed
|
|
6
|
+
at all.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from gridappsd.goss import _serialize_message, _unpack_stomp_args
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _FakeFrame:
|
|
15
|
+
"""Stand in for stomp-py 8.x's Frame object.
|
|
16
|
+
|
|
17
|
+
stomp-py 8.x calls a raw listener's on_message/on_error with a single
|
|
18
|
+
object exposing .headers and .body, instead of two positional
|
|
19
|
+
arguments. This fake reproduces that shape without depending on the
|
|
20
|
+
installed stomp-py version actually being 8.x.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, headers, body):
|
|
24
|
+
self.headers = headers
|
|
25
|
+
self.body = body
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestUnpackStompArgs:
|
|
29
|
+
"""Parametrized coverage of both stomp callback argument shapes."""
|
|
30
|
+
|
|
31
|
+
def test_two_positional_args_stomp_pre_8(self):
|
|
32
|
+
headers = {"destination": "/topic/foo", "reply-to": "/temp-queue/bar"}
|
|
33
|
+
body = "hello world"
|
|
34
|
+
|
|
35
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(headers, body)
|
|
36
|
+
|
|
37
|
+
assert unpacked_headers == headers
|
|
38
|
+
assert unpacked_body == body
|
|
39
|
+
|
|
40
|
+
def test_two_positional_args_preserves_dict_body(self):
|
|
41
|
+
headers = {"destination": "/topic/foo"}
|
|
42
|
+
body = {"key": "value"}
|
|
43
|
+
|
|
44
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(headers, body)
|
|
45
|
+
|
|
46
|
+
assert unpacked_headers is headers
|
|
47
|
+
assert unpacked_body is body
|
|
48
|
+
|
|
49
|
+
def test_single_frame_object_stomp_8(self):
|
|
50
|
+
headers = {"destination": "/topic/foo", "reply-to": "/temp-queue/bar"}
|
|
51
|
+
body = "hello world"
|
|
52
|
+
frame = _FakeFrame(headers, body)
|
|
53
|
+
|
|
54
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(frame)
|
|
55
|
+
|
|
56
|
+
assert unpacked_headers == headers
|
|
57
|
+
assert unpacked_body == body
|
|
58
|
+
|
|
59
|
+
def test_single_frame_object_preserves_dict_body(self):
|
|
60
|
+
headers = {"destination": "/topic/foo"}
|
|
61
|
+
body = {"key": "value"}
|
|
62
|
+
frame = _FakeFrame(headers, body)
|
|
63
|
+
|
|
64
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(frame)
|
|
65
|
+
|
|
66
|
+
assert unpacked_headers is headers
|
|
67
|
+
assert unpacked_body is body
|
|
68
|
+
|
|
69
|
+
def test_single_tuple_falls_back_to_unpacking(self):
|
|
70
|
+
headers = {"destination": "/topic/foo"}
|
|
71
|
+
body = "plain body"
|
|
72
|
+
|
|
73
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args((headers, body))
|
|
74
|
+
|
|
75
|
+
assert unpacked_headers == headers
|
|
76
|
+
assert unpacked_body == body
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize(
|
|
79
|
+
"args,expected_headers,expected_body",
|
|
80
|
+
[
|
|
81
|
+
(({"destination": "/topic/a"}, "body-a"), {"destination": "/topic/a"}, "body-a"),
|
|
82
|
+
(({"destination": "/topic/b"}, {"nested": 1}), {"destination": "/topic/b"}, {"nested": 1}),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
def test_two_arg_shape_parametrized(self, args, expected_headers, expected_body):
|
|
86
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(*args)
|
|
87
|
+
|
|
88
|
+
assert unpacked_headers == expected_headers
|
|
89
|
+
assert unpacked_body == expected_body
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(
|
|
92
|
+
"headers,body",
|
|
93
|
+
[
|
|
94
|
+
({"destination": "/topic/c"}, "body-c"),
|
|
95
|
+
({"destination": "/topic/d"}, {"nested": 2}),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
def test_frame_shape_parametrized(self, headers, body):
|
|
99
|
+
frame = _FakeFrame(headers, body)
|
|
100
|
+
|
|
101
|
+
unpacked_headers, unpacked_body = _unpack_stomp_args(frame)
|
|
102
|
+
|
|
103
|
+
assert unpacked_headers == headers
|
|
104
|
+
assert unpacked_body == body
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestSerializeMessage:
|
|
108
|
+
def test_dict_is_serialized_to_json_string(self):
|
|
109
|
+
result = _serialize_message({"a": 1, "b": 2})
|
|
110
|
+
|
|
111
|
+
assert result == '{"a": 1, "b": 2}'
|
|
112
|
+
|
|
113
|
+
def test_list_is_serialized_to_json_string(self):
|
|
114
|
+
result = _serialize_message([1, 2, 3])
|
|
115
|
+
|
|
116
|
+
assert result == "[1, 2, 3]"
|
|
117
|
+
|
|
118
|
+
def test_string_passes_through_unchanged(self):
|
|
119
|
+
result = _serialize_message("already a string")
|
|
120
|
+
|
|
121
|
+
assert result == "already a string"
|
|
122
|
+
|
|
123
|
+
def test_bytes_pass_through_unchanged(self):
|
|
124
|
+
payload = b"already bytes"
|
|
125
|
+
|
|
126
|
+
result = _serialize_message(payload)
|
|
127
|
+
|
|
128
|
+
assert result is payload
|
|
@@ -5,9 +5,78 @@
|
|
|
5
5
|
|
|
6
6
|
# import mock
|
|
7
7
|
|
|
8
|
-
from gridappsd import GridAPPSD
|
|
8
|
+
from gridappsd import GridAPPSD, ProcessStatusEnum
|
|
9
|
+
|
|
10
|
+
#, topics as t
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_gappsd():
|
|
14
|
+
return GridAPPSD(attempt_connection=False, username="u", password="p")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestApplicationAndServiceStatusShareState:
|
|
18
|
+
"""Unit coverage for the semantics the commented out test_gridappsd_status
|
|
19
|
+
integration stub below documents: set_application_status and
|
|
20
|
+
set_service_status both mutate the same shared _process_status field,
|
|
21
|
+
and an invalid status string is silently ignored (with a warning), not
|
|
22
|
+
raised, leaving the old value in place. No live broker is required:
|
|
23
|
+
GridAPPSD is constructed with attempt_connection=False.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def test_starts_in_starting_status(self):
|
|
27
|
+
gappsd = _make_gappsd()
|
|
28
|
+
|
|
29
|
+
assert gappsd.get_application_status() == ProcessStatusEnum.STARTING.value
|
|
30
|
+
assert gappsd.get_service_status() == ProcessStatusEnum.STARTING.value
|
|
31
|
+
|
|
32
|
+
def test_set_application_status_is_visible_through_service_status(self):
|
|
33
|
+
gappsd = _make_gappsd()
|
|
34
|
+
|
|
35
|
+
gappsd.set_application_status("RUNNING")
|
|
36
|
+
|
|
37
|
+
assert gappsd.get_service_status() == ProcessStatusEnum.RUNNING.value
|
|
38
|
+
assert gappsd.get_application_status() == ProcessStatusEnum.RUNNING.value
|
|
39
|
+
|
|
40
|
+
def test_set_service_status_is_visible_through_application_status(self):
|
|
41
|
+
gappsd = _make_gappsd()
|
|
42
|
+
|
|
43
|
+
gappsd.set_service_status("COMPLETE")
|
|
44
|
+
|
|
45
|
+
assert gappsd.get_service_status() == ProcessStatusEnum.COMPLETE.value
|
|
46
|
+
assert gappsd.get_application_status() == ProcessStatusEnum.COMPLETE.value
|
|
47
|
+
|
|
48
|
+
def test_invalid_service_status_is_ignored_and_retains_old_value(self, monkeypatch):
|
|
49
|
+
# The except ValueError branch in _set_status logs a warning, and the
|
|
50
|
+
# logger reads GRIDAPPSD_APPLICATION_ID to build the log record. In
|
|
51
|
+
# production app_registration.py sets this before app code runs; here
|
|
52
|
+
# it must be set explicitly so the warning path itself does not raise.
|
|
53
|
+
monkeypatch.setenv("GRIDAPPSD_APPLICATION_ID", "test-app-id")
|
|
54
|
+
gappsd = _make_gappsd()
|
|
55
|
+
# GOSS.send() calls _make_connection() unconditionally, even though
|
|
56
|
+
# this instance was constructed with attempt_connection=False, so the
|
|
57
|
+
# logger's warning path (get_logger().warning -> Logger.log ->
|
|
58
|
+
# self._gaps.send) would otherwise open a real stomp socket. Stub the
|
|
59
|
+
# network boundary, not the status logic under test.
|
|
60
|
+
monkeypatch.setattr(gappsd, "send", lambda topic, message: None)
|
|
61
|
+
gappsd.set_service_status("COMPLETE")
|
|
62
|
+
|
|
63
|
+
gappsd.set_service_status("Foo")
|
|
64
|
+
|
|
65
|
+
assert gappsd.get_service_status() == ProcessStatusEnum.COMPLETE.value
|
|
66
|
+
assert gappsd.get_application_status() == ProcessStatusEnum.COMPLETE.value
|
|
67
|
+
|
|
68
|
+
def test_invalid_application_status_is_ignored_and_retains_old_value(self, monkeypatch):
|
|
69
|
+
monkeypatch.setenv("GRIDAPPSD_APPLICATION_ID", "test-app-id")
|
|
70
|
+
gappsd = _make_gappsd()
|
|
71
|
+
# See the comment in test_invalid_service_status_is_ignored_and_retains_old_value:
|
|
72
|
+
# the warning path on an invalid status sends a log message over stomp.
|
|
73
|
+
monkeypatch.setattr(gappsd, "send", lambda topic, message: None)
|
|
74
|
+
gappsd.set_application_status("RUNNING")
|
|
75
|
+
|
|
76
|
+
gappsd.set_application_status("NotAValidStatus")
|
|
77
|
+
|
|
78
|
+
assert gappsd.get_application_status() == ProcessStatusEnum.RUNNING.value
|
|
9
79
|
|
|
10
|
-
#, topics as t, ProcessStatusEnum
|
|
11
80
|
|
|
12
81
|
# def test_get_gridappsd_client(gridappsd_client: GridAPPSD):
|
|
13
82
|
# assert isinstance(gridappsd_client, GridAPPSD)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Unit tests for gridappsd.simulation.Simulation's command-sending methods.
|
|
2
|
+
|
|
3
|
+
These tests exercise _send_simulation_command and the pause/stop/resume/
|
|
4
|
+
resume_pause_at wrappers built on it, asserting the exact JSON payload sent
|
|
5
|
+
and the destination topic. No live broker or docker stack is required:
|
|
6
|
+
GridAPPSD is constructed with attempt_connection=False, and Simulation's
|
|
7
|
+
own gapps.send call is mocked directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
import gridappsd.topics as t
|
|
13
|
+
from gridappsd import GridAPPSD, json_extension as json
|
|
14
|
+
from gridappsd.simulation import Simulation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_simulation():
|
|
18
|
+
gapps = GridAPPSD(attempt_connection=False, username="u", password="p")
|
|
19
|
+
run_config = {"simulation_config": {"duration": 300}}
|
|
20
|
+
simulation = Simulation(gapps, run_config)
|
|
21
|
+
simulation.simulation_id = "12345"
|
|
22
|
+
return simulation
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestSendSimulationCommand:
|
|
26
|
+
def test_command_without_input_omits_input_key(self):
|
|
27
|
+
simulation = _make_simulation()
|
|
28
|
+
|
|
29
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
30
|
+
simulation._send_simulation_command("pause")
|
|
31
|
+
|
|
32
|
+
mock_send.assert_called_once()
|
|
33
|
+
destination, body = mock_send.call_args[0]
|
|
34
|
+
assert destination == t.simulation_input_topic("12345")
|
|
35
|
+
assert json.loads(body) == {"command": "pause"}
|
|
36
|
+
assert simulation._running_or_paused is True
|
|
37
|
+
|
|
38
|
+
def test_command_with_input_nests_it_under_input_key(self):
|
|
39
|
+
simulation = _make_simulation()
|
|
40
|
+
|
|
41
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
42
|
+
simulation._send_simulation_command("resumePauseAt", pauseIn=30)
|
|
43
|
+
|
|
44
|
+
destination, body = mock_send.call_args[0]
|
|
45
|
+
assert destination == t.simulation_input_topic("12345")
|
|
46
|
+
assert json.loads(body) == {"command": "resumePauseAt", "input": {"pauseIn": 30}}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestPauseStopResumeWrappers:
|
|
50
|
+
def test_pause_sends_bare_pause_command(self):
|
|
51
|
+
simulation = _make_simulation()
|
|
52
|
+
|
|
53
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
54
|
+
simulation.pause()
|
|
55
|
+
|
|
56
|
+
_, body = mock_send.call_args[0]
|
|
57
|
+
assert json.loads(body) == {"command": "pause"}
|
|
58
|
+
|
|
59
|
+
def test_stop_sends_bare_stop_command(self):
|
|
60
|
+
simulation = _make_simulation()
|
|
61
|
+
|
|
62
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
63
|
+
simulation.stop()
|
|
64
|
+
|
|
65
|
+
_, body = mock_send.call_args[0]
|
|
66
|
+
assert json.loads(body) == {"command": "stop"}
|
|
67
|
+
|
|
68
|
+
def test_resume_sends_bare_resume_command(self):
|
|
69
|
+
simulation = _make_simulation()
|
|
70
|
+
|
|
71
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
72
|
+
simulation.resume()
|
|
73
|
+
|
|
74
|
+
_, body = mock_send.call_args[0]
|
|
75
|
+
assert json.loads(body) == {"command": "resume"}
|
|
76
|
+
|
|
77
|
+
def test_resume_pause_at_sends_command_with_nested_pause_in(self):
|
|
78
|
+
simulation = _make_simulation()
|
|
79
|
+
|
|
80
|
+
with mock.patch.object(simulation._gapps, "send") as mock_send:
|
|
81
|
+
simulation.resume_pause_at(45)
|
|
82
|
+
|
|
83
|
+
_, body = mock_send.call_args[0]
|
|
84
|
+
assert json.loads(body) == {"command": "resumePauseAt", "input": {"pauseIn": 45}}
|
|
85
|
+
|
|
86
|
+
def test_all_wrappers_set_running_or_paused_true(self):
|
|
87
|
+
simulation = _make_simulation()
|
|
88
|
+
simulation._running_or_paused = False
|
|
89
|
+
|
|
90
|
+
with mock.patch.object(simulation._gapps, "send"):
|
|
91
|
+
simulation.pause()
|
|
92
|
+
|
|
93
|
+
assert simulation._running_or_paused is True
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Unit tests for gridappsd.timeseries.Query filter building.
|
|
2
|
+
|
|
3
|
+
ge/le/eq/between all delegate to the shared _add_filter helper. These
|
|
4
|
+
tests assert the exact queryFilter list contents and ordering produced,
|
|
5
|
+
and that a filter method called before where_key raises.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from gridappsd.timeseries import Query
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestAddFilter:
|
|
14
|
+
def test_ge_appends_ge_filter_for_current_key(self):
|
|
15
|
+
query = Query("measurement").where_key("value")
|
|
16
|
+
|
|
17
|
+
query.ge(10)
|
|
18
|
+
|
|
19
|
+
assert query.queryFilter == [{"key": "value", "ge": 10}]
|
|
20
|
+
|
|
21
|
+
def test_le_appends_le_filter_for_current_key(self):
|
|
22
|
+
query = Query("measurement").where_key("value")
|
|
23
|
+
|
|
24
|
+
query.le(20)
|
|
25
|
+
|
|
26
|
+
assert query.queryFilter == [{"key": "value", "le": 20}]
|
|
27
|
+
|
|
28
|
+
def test_eq_appends_eq_filter_for_current_key(self):
|
|
29
|
+
query = Query("measurement").where_key("value")
|
|
30
|
+
|
|
31
|
+
query.eq(15)
|
|
32
|
+
|
|
33
|
+
assert query.queryFilter == [{"key": "value", "eq": 15}]
|
|
34
|
+
|
|
35
|
+
def test_between_appends_ge_then_le_in_order(self):
|
|
36
|
+
query = Query("measurement").where_key("value")
|
|
37
|
+
|
|
38
|
+
query.between(10, 20)
|
|
39
|
+
|
|
40
|
+
assert query.queryFilter == [
|
|
41
|
+
{"key": "value", "ge": 10},
|
|
42
|
+
{"key": "value", "le": 20},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def test_chained_filters_on_different_keys_preserve_each_keys_filters(self):
|
|
46
|
+
query = Query("measurement")
|
|
47
|
+
query.where_key("value").ge(10)
|
|
48
|
+
query.where_key("timestamp").le(999)
|
|
49
|
+
|
|
50
|
+
assert query.queryFilter == [
|
|
51
|
+
{"key": "value", "ge": 10},
|
|
52
|
+
{"key": "timestamp", "le": 999},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def test_filter_without_where_key_raises_value_error(self):
|
|
56
|
+
query = Query("measurement")
|
|
57
|
+
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
query.ge(10)
|
|
60
|
+
|
|
61
|
+
def test_filter_methods_return_self_for_chaining(self):
|
|
62
|
+
query = Query("measurement").where_key("value")
|
|
63
|
+
|
|
64
|
+
result = query.ge(10)
|
|
65
|
+
|
|
66
|
+
assert result is query
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
# import json
|
|
2
|
-
# import logging
|
|
3
|
-
# import os
|
|
4
|
-
# import threading
|
|
5
|
-
# from queue import Queue
|
|
6
|
-
|
|
7
|
-
# import mock
|
|
8
|
-
# import pytest
|
|
9
|
-
# from time import sleep
|
|
10
|
-
|
|
11
|
-
# from gridappsd import GOSS
|
|
12
|
-
# from gridappsd.docker_handler import get_docker_in_docker
|
|
13
|
-
# from gridappsd.goss import GRIDAPPSD_ENV_ENUM
|
|
14
|
-
|
|
15
|
-
# _log = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
# def test_auth_raises_error_no_username_password(docker_dependencies):
|
|
18
|
-
# container = get_docker_in_docker()
|
|
19
|
-
# mockdict = {
|
|
20
|
-
# GRIDAPPSD_ENV_ENUM.GRIDAPPSD_USER.value: '',
|
|
21
|
-
# GRIDAPPSD_ENV_ENUM.GRIDAPPSD_PASSWORD.value: ''
|
|
22
|
-
# }
|
|
23
|
-
# if container:
|
|
24
|
-
# mockdict[GRIDAPPSD_ENV_ENUM.GRIDAPPSD_ADDRESS.value] = "gridappsd"
|
|
25
|
-
|
|
26
|
-
# with mock.patch.dict(os.environ, mockdict):
|
|
27
|
-
# with pytest.raises(ValueError) as ex:
|
|
28
|
-
# goss = GOSS()
|
|
29
|
-
|
|
30
|
-
# with pytest.raises(ValueError) as ex:
|
|
31
|
-
# goss = GOSS(username="foo")
|
|
32
|
-
|
|
33
|
-
# with pytest.raises(ValueError) as ex:
|
|
34
|
-
# goss = GOSS(password="bar")
|
|
35
|
-
|
|
36
|
-
# def test_get_response(caplog, goss_client):
|
|
37
|
-
# caplog.set_level(logging.DEBUG)
|
|
38
|
-
|
|
39
|
-
# def addem_callback(header, message):
|
|
40
|
-
# print("Addem callback")
|
|
41
|
-
# print("Threadid: {}".format(threading.current_thread().ident))
|
|
42
|
-
|
|
43
|
-
# if isinstance(message, str):
|
|
44
|
-
# item = json.loads(message)
|
|
45
|
-
# else:
|
|
46
|
-
# item = message
|
|
47
|
-
# total = 0
|
|
48
|
-
# for x in item:
|
|
49
|
-
# total += x
|
|
50
|
-
|
|
51
|
-
# reply_to = header['reply-to']
|
|
52
|
-
# response = dict(result=total)
|
|
53
|
-
# print("Sending back topic: {topic} {response}".format(topic=reply_to,
|
|
54
|
-
# response=response))
|
|
55
|
-
# goss_client.send(reply_to, json.dumps(response))
|
|
56
|
-
|
|
57
|
-
# gen_sub = []
|
|
58
|
-
|
|
59
|
-
# def generic_subscription(header, message):
|
|
60
|
-
# gen_sub.append((header, message))
|
|
61
|
-
|
|
62
|
-
# # Simulate an rpc call.
|
|
63
|
-
# goss_client.subscribe("/addem", addem_callback)
|
|
64
|
-
|
|
65
|
-
# goss_client.subscribe("foo", generic_subscription)
|
|
66
|
-
|
|
67
|
-
# # id_before = id(goss_client._conn)
|
|
68
|
-
# result = goss_client.get_response('/addem', [5, 6])
|
|
69
|
-
# assert result['result'] == 11
|
|
70
|
-
# # assert id_before == id(goss_client._conn)
|
|
71
|
-
|
|
72
|
-
# goss_client.send("foo", str(result['result']))
|
|
73
|
-
|
|
74
|
-
# count = 0
|
|
75
|
-
# while True:
|
|
76
|
-
# sleep(0.1)
|
|
77
|
-
# count += 1
|
|
78
|
-
# if len(gen_sub) > 0 or count > 10:
|
|
79
|
-
# break
|
|
80
|
-
|
|
81
|
-
# assert gen_sub
|
|
82
|
-
# assert len(gen_sub) == 1
|
|
83
|
-
# assert len(gen_sub[0]) == 2
|
|
84
|
-
# assert result['result'] == 11
|
|
85
|
-
|
|
86
|
-
# def test_send_receive(goss_client):
|
|
87
|
-
# message_queue = Queue()
|
|
88
|
-
|
|
89
|
-
# class MyListener(object):
|
|
90
|
-
# def on_message(self, headers, message):
|
|
91
|
-
# message_queue.put((headers, message))
|
|
92
|
-
|
|
93
|
-
# listener = MyListener()
|
|
94
|
-
# goss_client.subscribe('doah', listener)
|
|
95
|
-
# goss_client.send('doah', "I am a foo")
|
|
96
|
-
# sleep(0.5)
|
|
97
|
-
# assert message_queue.qsize() == 1
|
|
98
|
-
# header, message = message_queue.get()
|
|
99
|
-
# assert message == "I am a foo"
|
|
100
|
-
|
|
101
|
-
# def test_callback_function(goss_client):
|
|
102
|
-
# message_queue1 = Queue()
|
|
103
|
-
|
|
104
|
-
# def callback1(headers, message):
|
|
105
|
-
# message_queue1.put((headers, message))
|
|
106
|
-
|
|
107
|
-
# goss_client.subscribe('foo', callback1)
|
|
108
|
-
# goss_client.send('foo', "I am a foo")
|
|
109
|
-
# sleep(0.5)
|
|
110
|
-
# assert message_queue1.qsize() == 1
|
|
111
|
-
# header, message = message_queue1.get()
|
|
112
|
-
# assert message == "I am a foo"
|
|
113
|
-
|
|
114
|
-
# def test_multi_subscriptions(goss_client):
|
|
115
|
-
# message_queue1 = Queue()
|
|
116
|
-
# message_queue2 = Queue()
|
|
117
|
-
|
|
118
|
-
# def callback1(headers, message):
|
|
119
|
-
# print(f"mq1 {headers} {message}")
|
|
120
|
-
# message_queue1.put((headers, message))
|
|
121
|
-
|
|
122
|
-
# def callback2(headers, message):
|
|
123
|
-
# print(f"mq2 {headers} {message}")
|
|
124
|
-
# message_queue2.put((headers, message))
|
|
125
|
-
|
|
126
|
-
# goss_client.subscribe('bim', callback1)
|
|
127
|
-
# goss_client.subscribe('bar', callback2)
|
|
128
|
-
# sleep(0.5)
|
|
129
|
-
# goss_client.send('bim', "I am a foo")
|
|
130
|
-
# goss_client.send('bar', "I am a bar")
|
|
131
|
-
# sleep(0.5)
|
|
132
|
-
# assert message_queue1.qsize() == 1
|
|
133
|
-
# assert message_queue2.qsize() == 1
|
|
134
|
-
# header, message = message_queue1.get()
|
|
135
|
-
# assert message == "I am a foo"
|
|
136
|
-
# header, message = message_queue2.get()
|
|
137
|
-
# assert message == "I am a bar"
|
|
138
|
-
|
|
139
|
-
# def test_multi_subscriptions_same_topic(goss_client):
|
|
140
|
-
# # pytest.xfail("Multiple topics can't be subscribed to the same topic at present.")
|
|
141
|
-
|
|
142
|
-
# message_queue1 = Queue()
|
|
143
|
-
# message_queue2 = Queue()
|
|
144
|
-
|
|
145
|
-
# def callback1(headers, message):
|
|
146
|
-
# print(f"handling callback1 {message} ")
|
|
147
|
-
# message_queue1.put((headers, message))
|
|
148
|
-
|
|
149
|
-
# def callback2(headers, message):
|
|
150
|
-
# print(f"handling callback2 {message} ")
|
|
151
|
-
# message_queue2.put((headers, message))
|
|
152
|
-
|
|
153
|
-
# indx1 = goss_client.subscribe('bim', callback1)
|
|
154
|
-
# goss_client.subscribe('bim', callback2)
|
|
155
|
-
# sleep(0.5)
|
|
156
|
-
# goss_client.send('bim', "I am a foo")
|
|
157
|
-
# goss_client.send('bim', "I am a bar")
|
|
158
|
-
# sleep(0.5)
|
|
159
|
-
# assert message_queue1.qsize() == 2
|
|
160
|
-
# assert message_queue2.qsize() == 2
|
|
161
|
-
# header, message = message_queue1.get()
|
|
162
|
-
# assert message == "I am a foo"
|
|
163
|
-
# header, message = message_queue1.get()
|
|
164
|
-
# assert message == "I am a bar"
|
|
165
|
-
# header, message = message_queue2.get()
|
|
166
|
-
# assert message == "I am a foo"
|
|
167
|
-
# header, message = message_queue2.get()
|
|
168
|
-
# assert message == "I am a bar"
|
|
169
|
-
|
|
170
|
-
# def test_response_class(goss_client):
|
|
171
|
-
# message_queue = Queue()
|
|
172
|
-
|
|
173
|
-
# class SubListener:
|
|
174
|
-
# def on_message(self, header, message):
|
|
175
|
-
# message_queue.put((header, message))
|
|
176
|
-
|
|
177
|
-
# goss_client.subscribe("/topic/bar", SubListener())
|
|
178
|
-
# sleep(0.5)
|
|
179
|
-
# goss_client.send("/topic/bar", {"abc": "def"})
|
|
180
|
-
|
|
181
|
-
# result = message_queue.get()
|
|
182
|
-
|
|
183
|
-
# print(result)
|
|
184
|
-
# assert result
|
|
185
|
-
# assert 2 == len(result)
|
|
186
|
-
|
|
187
|
-
# assert dict(abc="def") == result[1]
|
|
188
|
-
|
|
189
|
-
# def test_replace_subscription(caplog, goss_client):
|
|
190
|
-
# caplog.set_level(logging.DEBUG)
|
|
191
|
-
# original_queue = Queue()
|
|
192
|
-
# after_queue = Queue()
|
|
193
|
-
|
|
194
|
-
# def original_callback(headers, message):
|
|
195
|
-
# original_queue.put((headers, message))
|
|
196
|
-
|
|
197
|
-
# def after_callback(headers, message):
|
|
198
|
-
# after_queue.put((headers, message))
|
|
199
|
-
|
|
200
|
-
# goss_client.subscribe("woot", original_callback)
|
|
201
|
-
# goss_client.send("woot", "This is a message")
|
|
202
|
-
# sleep(0.5)
|
|
203
|
-
|
|
204
|
-
# assert original_queue.qsize() == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/field_interface/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_difference_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_logging_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|