genie-python 15.1.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. genie_python/.pylintrc +539 -0
  2. genie_python/__init__.py +1 -0
  3. genie_python/block_names.py +123 -0
  4. genie_python/channel_access_exceptions.py +45 -0
  5. genie_python/genie.py +2462 -0
  6. genie_python/genie_advanced.py +418 -0
  7. genie_python/genie_alerts.py +195 -0
  8. genie_python/genie_api_setup.py +451 -0
  9. genie_python/genie_blockserver.py +64 -0
  10. genie_python/genie_cachannel_wrapper.py +545 -0
  11. genie_python/genie_change_cache.py +151 -0
  12. genie_python/genie_dae.py +2218 -0
  13. genie_python/genie_epics_api.py +906 -0
  14. genie_python/genie_experimental_data.py +186 -0
  15. genie_python/genie_logging.py +200 -0
  16. genie_python/genie_p4p_wrapper.py +203 -0
  17. genie_python/genie_plot.py +77 -0
  18. genie_python/genie_pre_post_cmd_manager.py +21 -0
  19. genie_python/genie_pv_connection_protocol.py +36 -0
  20. genie_python/genie_script_checker.py +507 -0
  21. genie_python/genie_script_generator.py +212 -0
  22. genie_python/genie_simulate.py +69 -0
  23. genie_python/genie_simulate_impl.py +1265 -0
  24. genie_python/genie_startup.py +29 -0
  25. genie_python/genie_toggle_settings.py +58 -0
  26. genie_python/genie_wait_for_move.py +154 -0
  27. genie_python/genie_waitfor.py +576 -0
  28. genie_python/matplotlib_backend/__init__.py +0 -0
  29. genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
  30. genie_python/mysql_abstraction_layer.py +272 -0
  31. genie_python/run_tests.py +56 -0
  32. genie_python/scanning_instrument_pylint_plugin.py +31 -0
  33. genie_python/typings/CaChannel/CaChannel.pyi +893 -0
  34. genie_python/typings/CaChannel/__init__.pyi +9 -0
  35. genie_python/typings/CaChannel/_version.pyi +6 -0
  36. genie_python/typings/CaChannel/ca.pyi +31 -0
  37. genie_python/utilities.py +406 -0
  38. genie_python/version.py +1 -0
  39. genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
  40. genie_python-15.1.0rc1.dist-info/METADATA +95 -0
  41. genie_python-15.1.0rc1.dist-info/RECORD +43 -0
  42. genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
  43. genie_python-15.1.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,366 @@
1
+ """
2
+ A matplotlib backend based on WebAgg, modified to:
3
+ - Be non-blocking
4
+ - Open plots in the IBEX client
5
+ """
6
+
7
+ import asyncio
8
+ import atexit
9
+ import json
10
+ import logging
11
+ import sys
12
+ import threading
13
+ from functools import wraps
14
+ from time import sleep
15
+
16
+ import tornado
17
+ from matplotlib._pylab_helpers import Gcf
18
+ from matplotlib.backend_bases import _Backend
19
+ from matplotlib.backends import backend_webagg
20
+ from matplotlib.backends import backend_webagg_core as core
21
+ from py4j.java_collections import ListConverter
22
+ from py4j.java_gateway import JavaGateway
23
+ from tornado.websocket import WebSocketClosedError
24
+
25
+ from genie_python.genie_logging import GenieLogger
26
+
27
+ logging.getLogger("asyncio").setLevel(logging.WARNING)
28
+
29
+ DEFAULT_HOST = "127.0.0.1"
30
+
31
+ PRIMARY_WEB_PORT = 8988
32
+ SECONDARY_WEB_PORT = 8989
33
+ max_number_of_figures = 3
34
+ figure_numbers = []
35
+
36
+ _web_backend_port = PRIMARY_WEB_PORT
37
+ _is_primary = True
38
+
39
+
40
+ def _ignore_if_websocket_closed(func):
41
+ """
42
+ Decorator which ignores exceptions that were caused by websockets being closed.
43
+ """
44
+
45
+ @wraps(func)
46
+ def wrapper(*a, **kw):
47
+ try:
48
+ return func(*a, **kw)
49
+ except WebSocketClosedError:
50
+ pass
51
+ except Exception as e:
52
+ # Plotting multiple graphs quickly can cause an error where pyplot tries to access a plot which
53
+ # has been removed. This error does not break anything, so log it and continue. It is better for the plot
54
+ # to fail to update than for the whole user script to crash.
55
+ try:
56
+ GenieLogger().log_info_msg(
57
+ f"Caught (non-fatal) exception while calling matplotlib function: "
58
+ f"{e.__class__.__name__}: {e}"
59
+ )
60
+ except Exception:
61
+ # Exception while logging, ignore...
62
+ pass
63
+
64
+ return wrapper
65
+
66
+
67
+ def _asyncio_send_exceptions_to_logfile_only(loop, context):
68
+ exception = context.get("exception")
69
+ try:
70
+ GenieLogger().log_info_msg(
71
+ f"Caught (non-fatal) asyncio exception: " f"{exception.__class__.__name__}: {exception}"
72
+ )
73
+ except Exception:
74
+ # Exception while logging, ignore...
75
+ pass
76
+
77
+
78
+ def set_up_plot_default(is_primary=True, should_open_ibex_window_on_show=True, max_figures=None):
79
+ """
80
+ Set the plot defaults for when show is called
81
+
82
+ Args:
83
+ is_primary: True display plot on primary web port; False display plot on secondary web port
84
+ should_open_ibex_window_on_show: Does nothing; provided for backwards-compatibility with older backend
85
+ max_figures: Maximum number of figures to plot simultaneously (int)
86
+ """
87
+ global _web_backend_port
88
+ if is_primary:
89
+ _web_backend_port = PRIMARY_WEB_PORT
90
+ else:
91
+ _web_backend_port = SECONDARY_WEB_PORT
92
+
93
+ global _is_primary
94
+ _is_primary = is_primary
95
+
96
+ global max_number_of_figures
97
+ if max_figures is not None:
98
+ max_number_of_figures = max_figures
99
+
100
+
101
+ class WebAggApplication(backend_webagg.WebAggApplication):
102
+ class WebSocket(tornado.websocket.WebSocketHandler):
103
+ supports_binary = True
104
+
105
+ def write_message(self, *args, **kwargs):
106
+ f = super().write_message(*args, **kwargs)
107
+
108
+ @_ignore_if_websocket_closed
109
+ def _cb(*args, **kwargs):
110
+ return f.result()
111
+
112
+ f.add_done_callback(_cb)
113
+
114
+ @_ignore_if_websocket_closed
115
+ def open(self, fignum):
116
+ self.fignum = int(fignum)
117
+ self.manager = Gcf.figs.get(self.fignum, None)
118
+ if self.manager is not None:
119
+ self.manager.add_web_socket(self)
120
+ if hasattr(self, "set_nodelay"):
121
+ self.set_nodelay(True)
122
+
123
+ @_ignore_if_websocket_closed
124
+ def on_close(self):
125
+ self.manager.remove_web_socket(self)
126
+
127
+ @_ignore_if_websocket_closed
128
+ def on_message(self, message):
129
+ message = json.loads(message)
130
+ # The 'supports_binary' message is on a client-by-client
131
+ # basis. The others affect the (shared) canvas as a
132
+ # whole.
133
+ if message["type"] == "supports_binary":
134
+ self.supports_binary = message["value"]
135
+ else:
136
+ manager = Gcf.figs.get(self.fignum, None)
137
+ # It is possible for a figure to be closed,
138
+ # but a stale figure UI is still sending messages
139
+ # from the browser.
140
+ if manager is not None:
141
+ manager.handle_json(message)
142
+
143
+ @_ignore_if_websocket_closed
144
+ def send_json(self, content):
145
+ self.write_message(json.dumps(content))
146
+
147
+ @_ignore_if_websocket_closed
148
+ def send_binary(self, blob):
149
+ if self.supports_binary:
150
+ self.write_message(blob, binary=True)
151
+ else:
152
+ blob_code = blob.encode("base64").replace("\n", "")
153
+ data_uri = f"data:image/png;base64,{blob_code}"
154
+ self.write_message(data_uri)
155
+
156
+ ioloop = None
157
+ asyncio_loop = None
158
+ started = False
159
+ app = None
160
+
161
+ @classmethod
162
+ def initialize(cls, url_prefix="", port=None, address=None):
163
+ """
164
+ Create the class instance
165
+
166
+ We use a constant, hard-coded port as we will only ever have one plot going at the same time.
167
+ """
168
+ cls.app = cls(url_prefix=url_prefix)
169
+ cls.url_prefix = url_prefix
170
+ cls.port = port
171
+ cls.address = address
172
+
173
+ @classmethod
174
+ def start(cls):
175
+ """
176
+ IOLoop.running() was removed as of Tornado 2.4; see for example
177
+ https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY
178
+ Thus there is no correct way to check if the loop has already been
179
+ launched. We may end up with two concurrently running loops in that
180
+ unlucky case with all the expected consequences.
181
+ """
182
+ try:
183
+ atexit.register(cls.stop)
184
+ loop = asyncio.SelectorEventLoop()
185
+ loop.set_exception_handler(_asyncio_send_exceptions_to_logfile_only)
186
+
187
+ # For running in asyncio debug mode, only log _very_ slow callbacks
188
+ # (we get quite a few that take just over 100ms which is the default)
189
+ loop.slow_callback_duration = 500
190
+
191
+ asyncio.set_event_loop(loop)
192
+ cls.asyncio_loop = loop
193
+ cls.ioloop = tornado.ioloop.IOLoop.current()
194
+ cls.app.listen(cls.port, cls.address)
195
+
196
+ # Set the flag to True *before* blocking on ioloop.start()
197
+ cls.started = True
198
+ cls.ioloop.start()
199
+ except Exception:
200
+ import traceback
201
+
202
+ traceback.print_exc()
203
+
204
+ @classmethod
205
+ def stop(cls):
206
+ try:
207
+
208
+ def _stop():
209
+ cls.ioloop.stop()
210
+ sys.stdout.flush()
211
+ cls.started = False
212
+
213
+ cls.ioloop.add_callback(_stop)
214
+ except Exception:
215
+ import traceback
216
+
217
+ traceback.print_exc()
218
+
219
+
220
+ def ibex_open_plot_window(figures, is_primary=True, host=None):
221
+ """
222
+ Open the plot window in ibex gui through py4j. With sensible defaults
223
+ Args:
224
+ is_primary: True for primary plot window; False for secondary
225
+ host: host that the plot is on; if None default to local host
226
+ """
227
+ port = PRIMARY_WEB_PORT if is_primary else SECONDARY_WEB_PORT
228
+ if host is None:
229
+ host = DEFAULT_HOST
230
+ url = f"{host}:{port}"
231
+ try:
232
+ gateway = JavaGateway()
233
+ figures = ListConverter().convert(figures, gateway._gateway_client)
234
+ gateway.entry_point.openMplRenderer(figures, url, is_primary)
235
+ except Exception as e:
236
+ # We need this try-except to be very broad as various exceptions can, in principle,
237
+ # be thrown while translating between python <-> java.
238
+ # If any exceptions occur, it is better to log and continue rather than crashing the entire script.
239
+ print(f"Failed to open plot in IBEX due to: {e}")
240
+
241
+
242
+ IBEX_BACKEND_LOCK = threading.RLock()
243
+
244
+ _IBEX_FIGURE_MANAGER_LOCK = threading.RLock()
245
+
246
+
247
+ class _FigureManager(core.FigureManagerWebAgg):
248
+ _toolbar2_class = core.NavigationToolbar2WebAgg
249
+
250
+ @_ignore_if_websocket_closed
251
+ def _send_event(self, *args, **kwargs):
252
+ with _IBEX_FIGURE_MANAGER_LOCK:
253
+ super()._send_event(*args, **kwargs)
254
+
255
+ def remove_web_socket(self, *args, **kwargs):
256
+ with _IBEX_FIGURE_MANAGER_LOCK:
257
+ super().remove_web_socket(*args, **kwargs)
258
+
259
+ def add_web_socket(self, *args, **kwargs):
260
+ with _IBEX_FIGURE_MANAGER_LOCK:
261
+ super().add_web_socket(*args, **kwargs)
262
+
263
+ @_ignore_if_websocket_closed
264
+ def refresh_all(self, *args, **kwargs):
265
+ with _IBEX_FIGURE_MANAGER_LOCK:
266
+ super().refresh_all(*args, **kwargs)
267
+
268
+ @classmethod
269
+ def pyplot_show(cls, *args, **kwargs):
270
+ """
271
+ Show a plot.
272
+
273
+ Args:
274
+ args and kwargs: ignored (needed for compatibility with genie_python)
275
+ """
276
+ if not WebAggApplication.started:
277
+ with IBEX_BACKEND_LOCK:
278
+ WebAggApplication.initialize(port=_web_backend_port)
279
+ worker_thread = threading.Thread(
280
+ target=WebAggApplication.start, daemon=True, name="ibex_websocket_backend"
281
+ )
282
+ worker_thread.start()
283
+
284
+ for _ in range(1000):
285
+ # Wait for it to start
286
+ if WebAggApplication.started:
287
+ break
288
+ sleep(0.01)
289
+ else:
290
+ # If for some reason thread failed to start, log an error then continue anyway
291
+ # (we do not want to hang the entire script)
292
+ print("Failed to start plotting thread - plots will not be available")
293
+
294
+ ibex_open_plot_window(list(Gcf.figs.keys()), is_primary=_is_primary)
295
+
296
+ with IBEX_BACKEND_LOCK:
297
+ try:
298
+ Gcf.draw_all()
299
+ except Exception:
300
+ # Very occasionally draw_all() can fail, if that's the case it's better to not draw
301
+ # (IBEX will force an update 2s later anyway) rather than crash.
302
+ pass
303
+
304
+
305
+ class _FigureCanvas(backend_webagg.FigureCanvasWebAgg):
306
+ manager_class = _FigureManager
307
+
308
+ def set_image_mode(self, mode):
309
+ """
310
+ Always send full images to ibex.
311
+ """
312
+ self._current_image_mode = "full"
313
+
314
+ def get_diff_image(self, *args, **kwargs):
315
+ """
316
+ Always send full images to ibex.
317
+ """
318
+ self._force_full = True
319
+ return super().get_diff_image(*args, **kwargs)
320
+
321
+ def draw_idle(self) -> None:
322
+ """
323
+ From
324
+ https://matplotlib.org/stable/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.draw_idle
325
+
326
+ 'Backends may choose to override the method and implement
327
+ their own strategy to prevent multiple renderings.'
328
+
329
+ The IBEX GUI has it's own (RCP) mechanism for preventing concurrent drawing,
330
+ therefore it is sufficient to define this as a no-op. The IBEX GUI also automatically
331
+ requests a plot redraw every 2 seconds.
332
+
333
+ Note: don't call superclass here. The superclass sends a "draw" websocket event,
334
+ which can lead to a queue of UI events building up and excessive memory use.
335
+ """
336
+
337
+
338
+ @_Backend.export
339
+ class _BackendIbexWebAgg(_Backend):
340
+ FigureCanvas = _FigureCanvas
341
+ FigureManager = _FigureManager
342
+
343
+ @classmethod
344
+ def trigger_manager_draw(cls, manager):
345
+ with IBEX_BACKEND_LOCK:
346
+ manager.canvas.draw_idle()
347
+
348
+ @classmethod
349
+ def draw_if_interactive(cls):
350
+ with IBEX_BACKEND_LOCK:
351
+ super(_BackendIbexWebAgg, cls).draw_if_interactive()
352
+
353
+ @classmethod
354
+ def new_figure_manager(cls, num, *args, **kwargs):
355
+ with IBEX_BACKEND_LOCK:
356
+ for x in list(figure_numbers):
357
+ if x not in Gcf.figs.keys():
358
+ figure_numbers.remove(x)
359
+ figure_numbers.append(num)
360
+ if len(figure_numbers) > max_number_of_figures:
361
+ Gcf.destroy(figure_numbers[0])
362
+ print(
363
+ f"There are too many figures so deleted the oldest figure, which was {figure_numbers[0]}."
364
+ )
365
+ figure_numbers.pop(0)
366
+ return super(_BackendIbexWebAgg, cls).new_figure_manager(num, *args, **kwargs)
@@ -0,0 +1,272 @@
1
+ """
2
+ Abstracting out the SQL connection.
3
+ """
4
+ # This file is part of the ISIS IBEX application.
5
+ # Copyright (C) 2012-2016 Science & Technology Facilities Council.
6
+ # All rights reserved.
7
+ #
8
+ # This program is distributed in the hope that it will be useful.
9
+ # This program and the accompanying materials are made available under the
10
+ # terms of the Eclipse Public License v1.0 which accompanies this distribution.
11
+ # EXCEPT AS EXPRESSLY SET FORTH IN THE ECLIPSE PUBLIC LICENSE V1.0, THE PROGRAM
12
+ # AND ACCOMPANYING MATERIALS ARE PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES
13
+ # OR CONDITIONS OF ANY KIND. See the Eclipse Public License v1.0 for more details.
14
+ #
15
+ # You should have received a copy of the Eclipse Public License v1.0
16
+ # along with this program; if not, you can obtain a copy from
17
+ # https://www.eclipse.org/org/documents/epl-v10.php or
18
+ # http://opensource.org/licenses/eclipse-1.0.php
19
+
20
+ from abc import abstractmethod
21
+ from collections.abc import Iterable
22
+ from typing import Generator, List, Optional, TypeAlias
23
+
24
+ import mysql.connector
25
+ from mysql.connector.abstracts import MySQLCursorAbstract
26
+ from mysql.connector.connection import MySQLConnection
27
+ from mysql.connector.pooling import PooledMySQLConnection
28
+ from mysql.connector.types import ParamsSequenceOrDictType, RowType
29
+
30
+ ParamsSequenceOrDictType = ParamsSequenceOrDictType
31
+ MySQLConnectionAbstract: TypeAlias = MySQLConnection | PooledMySQLConnection
32
+
33
+
34
+ class DatabaseError(IOError):
35
+ """
36
+ Exception that is thrown if there is a problem with the database
37
+ """
38
+
39
+ def __init__(self, message: str) -> None:
40
+ super(DatabaseError, self).__init__(message)
41
+
42
+
43
+ class AbstractSQLCommands(object):
44
+ """
45
+ Abstract base class for sql commands for testing
46
+ """
47
+
48
+ @staticmethod
49
+ def generate_in_binding(parameter_count: int) -> str:
50
+ """
51
+ Generate a list of python sql bindings for use in a sql in clause.
52
+ One binding for each parameter. i.e. %s, %s, %s for 3 parameters.
53
+
54
+ Args:
55
+ parameter_count: number of items in the in clause
56
+
57
+ Returns: in binding
58
+
59
+ """
60
+ return ", ".join(["%s"] * parameter_count)
61
+
62
+ def query_returning_cursor(
63
+ self, command: str, bound_variables: ParamsSequenceOrDictType
64
+ ) -> Generator[RowType, None, None]:
65
+ """
66
+ Generator which returns rows from query.
67
+ Args:
68
+ command: command to run
69
+ bound_variables: any bound variables
70
+
71
+ Yields: a row from the query
72
+
73
+ """
74
+ raise NotImplementedError()
75
+
76
+ @abstractmethod
77
+ def _execute_command(
78
+ self, command: str, is_query: bool, bound_variables: Optional[ParamsSequenceOrDictType]
79
+ ) -> Optional[List[RowType]]:
80
+ """Executes a command on the database, and returns all values
81
+
82
+ Args:
83
+ command: the SQL command to run
84
+ is_query: is this a query (i.e. do we expect return values)
85
+
86
+ Returns:
87
+ values: list of all rows returned. None if not is_query
88
+ """
89
+ raise NotImplementedError()
90
+
91
+ def query(
92
+ self, command: str, bound_variables: Optional[ParamsSequenceOrDictType] = None
93
+ ) -> Optional[List[RowType]]:
94
+ """Executes a query on the database, and returns all values
95
+
96
+ Args:
97
+ command: the SQL command to run
98
+ bound_variables: a tuple of parameters to bind into the query;
99
+ Default no parameters to bind
100
+
101
+ Returns:
102
+ values: list of all rows returned
103
+ """
104
+ return self._execute_command(command, True, bound_variables)
105
+
106
+ def update(
107
+ self, command: str, bound_variables: Optional[ParamsSequenceOrDictType] = None
108
+ ) -> None:
109
+ """Executes an update on the database, and returns all values
110
+
111
+ Args:
112
+ command: the SQL command to run
113
+ bound_variables: a tuple of parameters to bind into the query;
114
+ Default no parameters to bind
115
+ """
116
+ self._execute_command(command, False, bound_variables)
117
+
118
+
119
+ class SQLAbstraction(AbstractSQLCommands):
120
+ """
121
+ A wrapper to connect to MySQL databases.
122
+ """
123
+
124
+ # Number of available simultaneous connections to each connection pool
125
+ POOL_SIZE = 16
126
+
127
+ def __init__(self, dbid: str, user: str, password: str, host: str = "127.0.0.1") -> None:
128
+ """
129
+ Constructor.
130
+
131
+ Args:
132
+ dbid: The id of the database that holds the required information
133
+ user: The username to use to connect to the database
134
+ password: The password to use to connect to the database
135
+ host: The host address to use, defaults to local host
136
+ """
137
+ super(SQLAbstraction, self).__init__()
138
+ self._dbid = dbid
139
+ self._user = user
140
+ self._password = password
141
+ self._host = host
142
+ self._pool_name = self._generate_pool_name()
143
+ self._start_connection_pool()
144
+
145
+ @staticmethod
146
+ def generate_unique_pool_name() -> str:
147
+ """Generate a unique name for the connection pool so each object has its own pool"""
148
+ import uuid
149
+
150
+ return "DBSVR_CONNECTION_POOL_" + str(uuid.uuid4())
151
+
152
+ def _generate_pool_name(self) -> str:
153
+ """Generate a name for the connection pool based on host, user and database name
154
+ a connection in the pool is made with the frist set of credentials passed, so we
155
+ have to make sure a pool name is not used with different credentials
156
+ """
157
+ return "DBSVR_%s_%s_%s" % (self._host, self._dbid, self._user)
158
+
159
+ def _close_handles(self, curs: MySQLCursorAbstract, conn: MySQLConnectionAbstract) -> None:
160
+ """Several methods need to close the cursor and connection handles.
161
+ It's a bit complicated and it has been seperated out here.
162
+ """
163
+ curserr = None
164
+ if curs is not None:
165
+ try:
166
+ curs.close()
167
+ except mysql.connector.errors.InternalError as err:
168
+ curserr = err
169
+ if conn is not None:
170
+ conn.close()
171
+ if curserr is not None:
172
+ raise curserr
173
+
174
+ def _start_connection_pool(self) -> None:
175
+ """Initialises a connection pool"""
176
+ conn = mysql.connector.connect(
177
+ user=self._user,
178
+ password=self._password,
179
+ host=self._host,
180
+ database=self._dbid,
181
+ pool_name=self._pool_name,
182
+ pool_size=SQLAbstraction.POOL_SIZE,
183
+ )
184
+ assert isinstance(conn, MySQLConnectionAbstract)
185
+ curs = conn.cursor()
186
+ assert isinstance(curs, MySQLCursorAbstract)
187
+ # Check db exists
188
+ curs.execute("SHOW TABLES")
189
+ if len(curs.fetchall()) == 0:
190
+ # Database does not exist
191
+ raise Exception("Requested Database %s does not exist" % self._dbid)
192
+ self._close_handles(curs, conn)
193
+
194
+ def _get_connection(self) -> MySQLConnectionAbstract:
195
+ try:
196
+ conn = mysql.connector.connect(pool_name=self._pool_name)
197
+ assert isinstance(conn, MySQLConnectionAbstract)
198
+ return conn
199
+ except Exception as err:
200
+ raise Exception("Unable to get connection from pool: %s" % str(err))
201
+
202
+ def _execute_command(
203
+ self, command: str, is_query: bool, bound_variables: Optional[ParamsSequenceOrDictType]
204
+ ) -> Optional[List[RowType]]:
205
+ """Executes a command on the database, and returns all values
206
+
207
+ Args:
208
+ command: the SQL command to run
209
+ is_query: is this a query (i.e. do we expect return values)
210
+ bound_variables: parameters to be used for the given statement
211
+ Returns:
212
+ values (list): list of all rows returned. None if not is_query
213
+ """
214
+ conn = None
215
+ curs = None
216
+ values = None
217
+ try:
218
+ conn = self._get_connection()
219
+ curs = conn.cursor()
220
+ if bound_variables is not None:
221
+ curs.execute(command, bound_variables)
222
+ else:
223
+ curs.execute(command)
224
+ if is_query:
225
+ values = curs.fetchall()
226
+ if values is not None and len(values) > 0:
227
+ isinstance(values[0], List)
228
+
229
+ # Commit as part of the query or results won't be updated between subsequent
230
+ # transactions. Can lead to values not auto-updating in the GUI.
231
+ conn.commit()
232
+ except Exception as err:
233
+ raise DatabaseError(str(err))
234
+ finally:
235
+ assert isinstance(curs, MySQLCursorAbstract)
236
+ assert isinstance(conn, MySQLConnectionAbstract)
237
+ self._close_handles(curs, conn)
238
+ return values
239
+
240
+ def query_returning_cursor(
241
+ self, command: str, bound_variables: ParamsSequenceOrDictType
242
+ ) -> Generator[RowType, None, None]:
243
+ """
244
+ Generator which returns rows from query.
245
+
246
+ Args:
247
+ command: command to run
248
+ bound_variables: any bound variables
249
+ Yields:
250
+ a row from the query
251
+ """
252
+ conn = None
253
+ curs = None
254
+ try:
255
+ conn = self._get_connection()
256
+ curs = conn.cursor()
257
+ assert isinstance(curs, MySQLCursorAbstract)
258
+ assert isinstance(curs, Iterable)
259
+ curs.execute(command, bound_variables)
260
+
261
+ for row in curs:
262
+ yield row
263
+
264
+ # Commit as part of the query or results won't be updated between subsequent
265
+ # transactions. Can lead to values not auto-updating in the GUI.
266
+ conn.commit()
267
+ except Exception as err:
268
+ raise DatabaseError(str(err))
269
+ finally:
270
+ assert isinstance(conn, MySQLConnectionAbstract), "Wrong type"
271
+ assert isinstance(curs, MySQLCursorAbstract)
272
+ self._close_handles(curs, conn)
@@ -0,0 +1,56 @@
1
+ # This file is part of the ISIS IBEX application.
2
+ # Copyright (C) 2012-2016 Science & Technology Facilities Council.
3
+ # All rights reserved.
4
+ #
5
+ # This program is distributed in the hope that it will be useful.
6
+ # This program and the accompanying materials are made available under the
7
+ # terms of the Eclipse Public License v1.0 which accompanies this distribution.
8
+ # EXCEPT AS EXPRESSLY SET FORTH IN THE ECLIPSE PUBLIC LICENSE V1.0, THE PROGRAM
9
+ # AND ACCOMPANYING MATERIALS ARE PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES
10
+ # OR CONDITIONS OF ANY KIND. See the Eclipse Public License v1.0 for more details.
11
+ #
12
+ # You should have received a copy of the Eclipse Public License v1.0
13
+ # along with this program; if not, you can obtain a copy from
14
+ # https://www.eclipse.org/org/documents/epl-v10.php or
15
+ # http://opensource.org/licenses/eclipse-1.0.php
16
+
17
+ # Add root path for access to server_commons
18
+ from __future__ import absolute_import, print_function
19
+
20
+ import argparse
21
+ import os
22
+ import sys
23
+
24
+ # Standard imports
25
+ import unittest
26
+
27
+ import xmlrunner
28
+
29
+ DEFAULT_DIRECTORY = os.path.join(".", "test-reports")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ # get output directory from command line arguments
34
+ parser = argparse.ArgumentParser()
35
+ parser.add_argument(
36
+ "-o",
37
+ "--output_dir",
38
+ nargs=1,
39
+ type=str,
40
+ default=[DEFAULT_DIRECTORY],
41
+ help="The directory to save the test reports",
42
+ )
43
+ args = parser.parse_args()
44
+ xml_dir = args.output_dir[0]
45
+
46
+ # Load tests from test suites
47
+ test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_modules"))
48
+ pattern = "*test_*.py"
49
+ test_suite = unittest.TestLoader().discover(test_dir, pattern=pattern)
50
+
51
+ print("\n\n------ BEGINNING GENIE_PYTHON UNIT TESTS ------")
52
+ ret_vals = xmlrunner.XMLTestRunner(output=xml_dir).run(test_suite)
53
+ print("------ GENIE_PYTHON UNIT TESTS COMPLETE ------\n\n")
54
+
55
+ # Return failure exit code if a test failed
56
+ sys.exit(bool(ret_vals.errors or ret_vals.failures))