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.
Files changed (47) hide show
  1. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/PKG-INFO +1 -1
  2. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/app_registration.py +35 -10
  3. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/goss.py +47 -53
  4. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/gridappsd.py +20 -13
  5. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/simulation.py +23 -19
  6. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/timeseries.py +16 -22
  7. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/pyproject.toml +1 -1
  8. gridappsd_python-2026.2.1a2/tests/test_app_registration.py +63 -0
  9. gridappsd_python-2026.2.1a2/tests/test_goss.py +128 -0
  10. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_gridappsd.py +71 -2
  11. gridappsd_python-2026.2.1a2/tests/test_simulation_commands.py +93 -0
  12. gridappsd_python-2026.2.1a2/tests/test_timeseries.py +66 -0
  13. gridappsd_python-2026.2.1a1/tests/test_goss.py +0 -204
  14. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/.gitignore +0 -0
  15. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/CHANGELOG.md +0 -0
  16. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/README.md +0 -0
  17. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/conf/entrypoint.sh +0 -0
  18. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/conf/run-gridappsd.sh +0 -0
  19. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/example.env +0 -0
  20. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/__init__.py +0 -0
  21. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/__main__.py +0 -0
  22. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/difference_builder.py +0 -0
  23. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/docker_handler.py +0 -0
  24. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/field_interface/__init__.py +0 -0
  25. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/houses.py +0 -0
  26. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/json_extension.py +0 -0
  27. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/loghandler.py +0 -0
  28. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/register_app.py +0 -0
  29. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/topics.py +0 -0
  30. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd/utils.py +0 -0
  31. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/gridappsd_python_testing.ipynb +0 -0
  32. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/info/CHANGELOG.md +0 -0
  33. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/info/VERSION +0 -0
  34. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/poetry.toml +0 -0
  35. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/conftest.py +0 -0
  36. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/run_gridappsd.py +0 -0
  37. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/simulation_fixtures/13_node_2_min_base.json +0 -0
  38. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_containers.py +0 -0
  39. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_difference_builder.py +0 -0
  40. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_docker_handler.py +0 -0
  41. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_houses.py +0 -0
  42. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_json.py +0 -0
  43. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_logging.py +0 -0
  44. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_logging_integration.py +0 -0
  45. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_simulation.py +0 -0
  46. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/test_topics.py +0 -0
  47. {gridappsd_python-2026.2.1a1 → gridappsd_python-2026.2.1a2}/tests/unittest_test_template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridappsd-python
3
- Version: 2026.2.1a1
3
+ Version: 2026.2.1a2
4
4
  Summary: A GridAPPS-D Python Adapter
5
5
  Project-URL: Repository, https://github.com/GRIDAPPSD/gridappsd-python
6
6
  Project-URL: Homepage, https://gridappsd.readthedocs.io
@@ -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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "RUNNING"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "RUNNING"
69
+ _set_application_status(ApplicationStatusEnum.RUNNING)
45
70
  time.sleep(1)
46
71
 
47
72
  except Exception as e:
48
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "ERROR"
73
+ _set_application_status(ApplicationStatusEnum.ERROR)
49
74
  _log.error(repr(e))
50
75
  else:
51
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPED"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPED"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPED"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPED"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STARTING"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPING"
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
- os.environ["GRIDAPPSD_APPLICATION_STATUS"] = "STOPPED"
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
- # stomp.py 8.x changed listener callbacks from (headers, body) to (frame)
70
- _stomp_major = (
71
- int(getattr(_stomp_module, "__version__", (0,))[0])
72
- if isinstance(getattr(_stomp_module, "__version__", None), tuple)
73
- else 0
74
- )
75
- try:
76
- from importlib.metadata import version as _pkg_version
77
-
78
- _stomp_major = int(_pkg_version("stomp-py").split(".")[0])
79
- except Exception:
80
- pass
81
- _STOMP_V8 = _stomp_major >= 8
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
- if isinstance(message, list) or isinstance(message, dict):
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
- if isinstance(message, dict):
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
- if _STOMP_V8:
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
- if _STOMP_V8:
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
- if _STOMP_V8:
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
- if _STOMP_V8:
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
- if _STOMP_V8:
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
- if _STOMP_V8:
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 set_application_status(self, status):
120
- """
121
- Set the application status.
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 application status." + f"Valid statuses are {ProcessStatusEnum.__members__}."
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
- try:
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._process_status.value
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._process_status.value
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("A simulation's publishing_period cannot be less than the simulation's timestep "
79
- "interval. please make the publishing_period >= interval!")
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 pause(self):
219
- """Pause simulation"""
220
- _log.debug("Pausing simulation")
221
- command = dict(command="pause")
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
- _log.debug("Stopping simulation")
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
- _log.debug("Resuming simulation")
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
- command = dict(command="resumePauseAt", input=dict(pauseIn=pause_in))
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 ge(self, value):
50
- """Method to add 'value greater than or equal to' filter to a key.
49
+ def _add_filter(self, operator, value):
50
+ """Append a single {key, operator: value} filter for the current key.
51
51
 
52
- :param value:
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
- obj = {"key": self.key, "ge": value}
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
- if self.key is None:
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
- if self.key is None:
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
- if self.key is None:
89
- raise ValueError("Key is not specified. Call where_key first.")
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gridappsd-python"
7
- version = "2026.02.1a1"
7
+ version = "2026.02.1a2"
8
8
  description = "A GridAPPS-D Python Adapter"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -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