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.
- genie_python/.pylintrc +539 -0
- genie_python/__init__.py +1 -0
- genie_python/block_names.py +123 -0
- genie_python/channel_access_exceptions.py +45 -0
- genie_python/genie.py +2462 -0
- genie_python/genie_advanced.py +418 -0
- genie_python/genie_alerts.py +195 -0
- genie_python/genie_api_setup.py +451 -0
- genie_python/genie_blockserver.py +64 -0
- genie_python/genie_cachannel_wrapper.py +545 -0
- genie_python/genie_change_cache.py +151 -0
- genie_python/genie_dae.py +2218 -0
- genie_python/genie_epics_api.py +906 -0
- genie_python/genie_experimental_data.py +186 -0
- genie_python/genie_logging.py +200 -0
- genie_python/genie_p4p_wrapper.py +203 -0
- genie_python/genie_plot.py +77 -0
- genie_python/genie_pre_post_cmd_manager.py +21 -0
- genie_python/genie_pv_connection_protocol.py +36 -0
- genie_python/genie_script_checker.py +507 -0
- genie_python/genie_script_generator.py +212 -0
- genie_python/genie_simulate.py +69 -0
- genie_python/genie_simulate_impl.py +1265 -0
- genie_python/genie_startup.py +29 -0
- genie_python/genie_toggle_settings.py +58 -0
- genie_python/genie_wait_for_move.py +154 -0
- genie_python/genie_waitfor.py +576 -0
- genie_python/matplotlib_backend/__init__.py +0 -0
- genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
- genie_python/mysql_abstraction_layer.py +272 -0
- genie_python/run_tests.py +56 -0
- genie_python/scanning_instrument_pylint_plugin.py +31 -0
- genie_python/typings/CaChannel/CaChannel.pyi +893 -0
- genie_python/typings/CaChannel/__init__.pyi +9 -0
- genie_python/typings/CaChannel/_version.pyi +6 -0
- genie_python/typings/CaChannel/ca.pyi +31 -0
- genie_python/utilities.py +406 -0
- genie_python/version.py +1 -0
- genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
- genie_python-15.1.0rc1.dist-info/METADATA +95 -0
- genie_python-15.1.0rc1.dist-info/RECORD +43 -0
- genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
- 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))
|