genie-python 15.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. genie_python/.pylintrc +539 -0
  2. genie_python/__init__.py +1 -0
  3. genie_python/_version.py +16 -0
  4. genie_python/block_names.py +123 -0
  5. genie_python/channel_access_exceptions.py +45 -0
  6. genie_python/genie.py +2462 -0
  7. genie_python/genie_advanced.py +418 -0
  8. genie_python/genie_alerts.py +195 -0
  9. genie_python/genie_api_setup.py +451 -0
  10. genie_python/genie_blockserver.py +64 -0
  11. genie_python/genie_cachannel_wrapper.py +551 -0
  12. genie_python/genie_change_cache.py +151 -0
  13. genie_python/genie_dae.py +2219 -0
  14. genie_python/genie_epics_api.py +906 -0
  15. genie_python/genie_experimental_data.py +186 -0
  16. genie_python/genie_logging.py +200 -0
  17. genie_python/genie_p4p_wrapper.py +203 -0
  18. genie_python/genie_plot.py +77 -0
  19. genie_python/genie_pre_post_cmd_manager.py +21 -0
  20. genie_python/genie_pv_connection_protocol.py +36 -0
  21. genie_python/genie_script_checker.py +507 -0
  22. genie_python/genie_script_generator.py +212 -0
  23. genie_python/genie_simulate.py +69 -0
  24. genie_python/genie_simulate_impl.py +1265 -0
  25. genie_python/genie_startup.py +29 -0
  26. genie_python/genie_toggle_settings.py +58 -0
  27. genie_python/genie_wait_for_move.py +154 -0
  28. genie_python/genie_waitfor.py +576 -0
  29. genie_python/matplotlib_backend/__init__.py +0 -0
  30. genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
  31. genie_python/mysql_abstraction_layer.py +272 -0
  32. genie_python/scanning_instrument_pylint_plugin.py +31 -0
  33. genie_python/testing_utils/__init__.py +4 -0
  34. genie_python/testing_utils/script_checker.py +63 -0
  35. genie_python/typings/CaChannel/CaChannel.pyi +893 -0
  36. genie_python/typings/CaChannel/__init__.pyi +9 -0
  37. genie_python/typings/CaChannel/_version.pyi +6 -0
  38. genie_python/typings/CaChannel/ca.pyi +31 -0
  39. genie_python/utilities.py +406 -0
  40. genie_python/version.py +6 -0
  41. genie_python-15.1.0.dist-info/LICENSE +28 -0
  42. genie_python-15.1.0.dist-info/METADATA +84 -0
  43. genie_python-15.1.0.dist-info/RECORD +45 -0
  44. genie_python-15.1.0.dist-info/WHEEL +5 -0
  45. genie_python-15.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,451 @@
1
+ from __future__ import print_function
2
+
3
+ import ctypes
4
+ import functools
5
+ import glob
6
+ import inspect
7
+ import os
8
+ import re
9
+ import sys
10
+ import time
11
+ import traceback
12
+ from typing import Any, Callable, ParamSpec, TypeVar
13
+
14
+ import IPython
15
+ from decorator import getfullargspec
16
+ from IPython.core.completer import IPCompleter
17
+
18
+ from genie_python.utilities import get_correct_filepath_existing
19
+
20
+ P = ParamSpec("P")
21
+ T = TypeVar("T")
22
+
23
+ # Determine whether to start in simulation mode
24
+
25
+ if "GENIE_SIMULATE" not in os.environ or os.environ["GENIE_SIMULATE"] != "1":
26
+ from genie_python.genie_epics_api import API
27
+ else:
28
+ print("\n=========== RUNNING IN SIMULATION MODE ===========\n")
29
+ from genie_python.genie_simulate_impl import API
30
+
31
+
32
+ # Windows specific stuff
33
+ if os.name == "nt":
34
+ # Needed for correcting file paths
35
+ pass
36
+
37
+ # INITIALISATION CODE - DO NOT DELETE
38
+ try:
39
+ # If __api does not exist or is None then we need to create it.
40
+ if __api is None: # noqa: F821 __api currently gets added to globals which is bad.
41
+ raise Exception("API does not exist")
42
+ except Exception:
43
+ # This should only get called the first time genie is imported
44
+ my_pv_prefix = None
45
+ if "MYPVPREFIX" in os.environ:
46
+ my_pv_prefix = os.environ["MYPVPREFIX"]
47
+ __api = API(my_pv_prefix, globals())
48
+ else:
49
+ print("No instrument specified - loading local instrument")
50
+ __api = API(None, globals())
51
+
52
+
53
+ # END INITIALISATION CODE
54
+
55
+
56
+ def set_user_script_dir(*directory: str | list[str]) -> None:
57
+ """
58
+ Set the user script directory, ensuring it ends in a slash
59
+ Args:
60
+ directory: directory to set it to, or list of directories
61
+
62
+ """
63
+ global USER_SCRIPT_DIR
64
+
65
+ requested_dir = os.path.join(*directory)
66
+ directory_name = requested_dir
67
+ dirs_to_create = []
68
+
69
+ base_name = None
70
+ while base_name != "":
71
+ try:
72
+ directory_name = get_correct_filepath_existing(directory_name)
73
+ break
74
+ except OSError:
75
+ directory_name, base_name = os.path.split(directory_name.strip(r"\\/"))
76
+ dirs_to_create.append(base_name)
77
+
78
+ # Got to a single directory which does not exist
79
+ if base_name == "":
80
+ raise OSError("Script dir does not exist and can not be created: {}".format(requested_dir))
81
+
82
+ for dir_to_create in reversed(dirs_to_create):
83
+ directory_name = os.path.join(directory_name, dir_to_create)
84
+ os.mkdir(directory_name)
85
+ directory_name = get_correct_filepath_existing(directory_name)
86
+
87
+ if len(directory_name) > 1 and directory_name[-1] != "/":
88
+ directory_name += "/"
89
+ USER_SCRIPT_DIR = directory_name
90
+
91
+
92
+ def get_user_script_dir() -> str:
93
+ """
94
+ Returns: the user script directory
95
+ """
96
+ global USER_SCRIPT_DIR
97
+ return USER_SCRIPT_DIR
98
+
99
+
100
+ try:
101
+ set_user_script_dir("C:/scripts/")
102
+ except Exception:
103
+ USER_SCRIPT_DIR = ""
104
+
105
+
106
+ # TAB COMPLETE CODE
107
+
108
+ LOAD_SCRIPT_COMMAND = "load_script("
109
+
110
+
111
+ class LoadScriptCompleter:
112
+ """
113
+ A class holding a custom complete function which replaces the normal completion
114
+ function of the ipython completer. We are replacing it and not just adding it
115
+ as a custom completer because it allows us to return just paths after
116
+ load_script and no other completions from the IPython backend. If this
117
+ does not have load script in it will return what the original one did.
118
+ """
119
+
120
+ def __init__(self, original_complete_fn: Callable) -> None:
121
+ """
122
+ Initialise.
123
+ Args:
124
+ original_complete_fn: function which was originally used to complete
125
+ """
126
+ self._original_complete = original_complete_fn
127
+ self.is_pydev = False
128
+
129
+ def _get_completion_paths(self, line_buffer: str) -> tuple[list[str], str, bool, bool]:
130
+ """
131
+ Given a line buffer, get the relevant filename completions for a g.load_script command
132
+
133
+ Args:
134
+ line_buffer: the line to complete
135
+ Returns:
136
+ tuple (list[str], str, bool, bool) - a list of the completions,
137
+ the search path which was used, whether the path was already quoted,
138
+ whether the path was absolute (e.g. c:/scripts/myscript.py)
139
+ or relative (e.g. myscript.py)
140
+ """
141
+ # cope with None as search path
142
+ if line_buffer is None:
143
+ line_buffer = ""
144
+
145
+ # get file path
146
+ search_path = line_buffer.split(LOAD_SCRIPT_COMMAND)[-1]
147
+
148
+ had_quotes = search_path.startswith("'") or search_path.startswith('"')
149
+ if had_quotes:
150
+ search_path = search_path[1:]
151
+
152
+ is_absolute = os.path.isabs(search_path)
153
+ if not is_absolute:
154
+ search_path = "{}{}".format(get_user_script_dir(), search_path)
155
+
156
+ # search and return results
157
+ paths = glob.glob("{}*".format(search_path))
158
+
159
+ return paths, search_path, had_quotes, is_absolute
160
+
161
+ def standalone_complete(self, completer: IPCompleter, text: str | None = None) -> list[str]:
162
+ """
163
+ Find completions for the given text and line context.
164
+
165
+ Parameters
166
+ ----------
167
+ completer : IPyCompleter
168
+ The IPython completer instance being used to perform this completion.
169
+
170
+ text : string, optional
171
+ Text to perform the completion on. Line buffer
172
+ is always used except when using the default completer.
173
+
174
+ Returns
175
+ -------
176
+ completion : list
177
+ A list of completion matches.
178
+ """
179
+ line_buffer = completer.line_buffer
180
+ if LOAD_SCRIPT_COMMAND not in line_buffer:
181
+ return []
182
+ else:
183
+ paths, search_path, had_quotes, was_absolute = self._get_completion_paths(line_buffer)
184
+
185
+ # add quote if it is missing
186
+ if had_quotes:
187
+ quote = ""
188
+ else:
189
+ quote = '"'
190
+
191
+ # if the path isn't absolute it should refer to script dir
192
+ if was_absolute:
193
+ len_added_user_script = 0
194
+ else:
195
+ len_added_user_script = len(get_user_script_dir())
196
+
197
+ # Console expects the whole path
198
+ # return / to avoid a quoting issue with \ in paths and do not include the script dir
199
+ completion = [
200
+ "{}{}".format(quote, path.replace("\\", "/")[len_added_user_script:])
201
+ for path in paths
202
+ ]
203
+
204
+ return completion
205
+
206
+ def pydev_complete(
207
+ self, text: str | None = None, line_buffer: str | None = None, cursor_pos: int | None = None
208
+ ) -> tuple[str, list[str]]:
209
+ """
210
+ Find completions for the given text and line context.
211
+ Note that both the text and the line_buffer are optional, but at least
212
+ one of them must be given.
213
+ Parameters
214
+ ----------
215
+ text : string, optional
216
+ Text to perform the completion on. Line buffer
217
+ is always used except when using the default completer.
218
+ line_buffer : string, optional
219
+ line to match
220
+ cursor_pos : int, optional
221
+ Index of the cursor in the full line buffer. Should be provided by
222
+ remote frontends where kernel has no access to frontend state.
223
+ Returns
224
+ -------
225
+ text : str
226
+ Text that was actually used in the completion.
227
+ matches : list
228
+ A list of completion matches.
229
+ """
230
+
231
+ if LOAD_SCRIPT_COMMAND not in line_buffer:
232
+ match, completion = self._original_complete(text, line_buffer, cursor_pos)
233
+ else:
234
+ paths, search_path, _, _ = self._get_completion_paths(line_buffer)
235
+ match = search_path
236
+
237
+ # PyDev expects just the end part of the expression back broken at the last punctuation
238
+ completion = []
239
+ for path in paths:
240
+ # py dev replaces back to the last splitting character.
241
+ # Find that in the line buffer. E.g.
242
+ # I have a script called play.py
243
+ # I type C:/script/pl
244
+ # This function will add only play.py to the list
245
+ # (back to the last splitting character, i.e. /)
246
+ # There is a special case when it is just load_script(
247
+ if line_buffer.endswith("load_script("):
248
+ line_buffer_back_to_last_splitting_character = '"'
249
+ else:
250
+ line_buffer_back_to_last_splitting_character = re.split(
251
+ r"[/:\\ .-]", line_buffer
252
+ )[-1]
253
+
254
+ # find extra path to add
255
+ len_of_overlap = len(search_path)
256
+ completion_path = path[len_of_overlap:].replace("\\", "/")
257
+
258
+ # assemble the auto replace
259
+ completion.append(
260
+ "{}{}".format(line_buffer_back_to_last_splitting_character, completion_path)
261
+ )
262
+
263
+ return match, completion
264
+
265
+
266
+ class PyDevComplete:
267
+ """
268
+ In PyDev the completer is at a higher level, it uses the ipython
269
+ completer above and adds extra completion
270
+ we need to override this to return just the paths and
271
+ not the other possible completions.
272
+ This is not nice code because I am manipulating
273
+ private methods but there is no other way to do this.
274
+ """
275
+
276
+ def __init__(self, original_function: Callable) -> None:
277
+ """
278
+ Initialise
279
+ Args:
280
+ original_function: the original completion function that we will call
281
+ """
282
+ self.original_function = original_function
283
+
284
+ def just_path_on_load(self, text: str, act_tok: Any) -> list[str]:
285
+ """
286
+ Returns completions for load on a path if load_script in path otherwise returns as before.
287
+ Will replace .metadata\.plugins\org.eclipse.pde.core\.bundle_pool\plugins\
288
+ org.python.pydev_5.9.2.201708151115\pysrc\_pydev_bundle\pydev_ipython_console_011.py
289
+
290
+ This functions will filter out the pydev completions if the line contains load_script.
291
+ It know which are the pydev ones because they are marked with the type '11'.
292
+
293
+ Args:
294
+ text: text to complete
295
+ act_tok: token, used only in original completion
296
+
297
+ Returns:
298
+ pydev completions
299
+
300
+ """
301
+ # This is the completion type assigned to all ipython completions in pydev
302
+ ipython_completer_completion_type = "11"
303
+ ans = self.original_function(text, act_tok)
304
+ # returns list of tuples (completion, py doc, ?, type)
305
+ if LOAD_SCRIPT_COMMAND in text:
306
+ ans = [an for an in ans if an[3] == ipython_completer_completion_type]
307
+ return ans
308
+
309
+
310
+ try:
311
+ # replace the original completer in ipython with the one above
312
+ ipy_completer = IPython.get_ipython().Completer.complete
313
+ _load_script_completer = LoadScriptCompleter(ipy_completer)
314
+ IPython.get_ipython().set_custom_completer(_load_script_completer.standalone_complete, 0)
315
+
316
+ try:
317
+ # Replace the old completer in pydev with the new completer above and turns on pydev
318
+ from _pydev_bundle.pydev_ipython_console_011 import _PyDevFrontEndContainer
319
+
320
+ _PyDevFrontEndContainer._instance.getCompletions = PyDevComplete(
321
+ _PyDevFrontEndContainer._instance.getCompletions
322
+ ).just_path_on_load
323
+ _load_script_completer.is_pydev = True
324
+ # Pydev does not honor IPython custom_completer, so use an old-style function replace
325
+ # (this no longer works in standalone python window so cannot use same style for both cases)
326
+ IPython.get_ipython().Completer.complete = _load_script_completer.pydev_complete
327
+ except ImportError:
328
+ pass
329
+ # this means we are not in pydev
330
+ except AttributeError:
331
+ print("ERROR: IPython does not exist, auto complete not installed")
332
+
333
+ # END TAB COMPLETE
334
+
335
+
336
+ def usercommand(func: Callable[P, T]) -> Callable[P, T]:
337
+ """
338
+ Decorator that marks a function as a user command (e.g. for NICOS).
339
+ """
340
+ func.is_usercommand = True
341
+ func.is_hidden = False
342
+ return func
343
+
344
+
345
+ def helparglist(args: Any) -> Callable[[Callable[P, T]], Callable[P, T]]:
346
+ """
347
+ Decorator that supplies a custom argument list to be displayed by
348
+ a help (e.g. for NICOS).
349
+ """
350
+
351
+ def deco(func: Callable[P, T]) -> Callable[P, T]:
352
+ func.help_arglist = args # type: ignore
353
+ return func
354
+
355
+ return deco
356
+
357
+
358
+ def log_command_and_handle_exception(f: Callable[P, T]) -> Callable[P, T]:
359
+ """
360
+ Decorator that will log the command when run and will
361
+ catch all exceptions to be handled by genie.
362
+
363
+ Note: _func_name_unlikely_to_be_reused must be called something that a user will not
364
+ accidentally put into a genie function
365
+ """
366
+
367
+ @functools.wraps(f)
368
+ def decorator(*args: P.args, **kwargs: P.kwargs) -> T:
369
+ log_args = {"kwargs": kwargs}
370
+ arg_names = getfullargspec(f).args
371
+ # If we can get the argument names the include them in the log
372
+ if len(arg_names) >= len(args):
373
+ log_args["args"] = dict(zip(arg_names, args))
374
+ else:
375
+ log_args["args"] = args
376
+
377
+ command_exception = None
378
+ start = time.time()
379
+ try:
380
+ return_value = f(*args, **kwargs)
381
+ return return_value
382
+ except Exception as e:
383
+ command_exception = traceback.format_exc()
384
+ _handle_exception(e)
385
+ return None # type: ignore
386
+ finally:
387
+ end = time.time()
388
+ time_taken = end - start
389
+ # hack to allow tests to pass. linux has microsecond resolution,
390
+ # Windows only as millisecond but can be >1ms occasionaly and
391
+ # mock is expecting 0.0
392
+ if time_taken < 0.002:
393
+ time_taken = 0.0
394
+ __api.logger.log_command(f.__name__, log_args, command_exception, time_taken=time_taken)
395
+ if command_exception is not None:
396
+ __api.logger.log_command_error_msg(f.__name__, command_exception)
397
+
398
+ decorator.__signature__ = inspect.signature(f) # type: ignore
399
+ return decorator
400
+
401
+
402
+ def _print_error_message(message: str) -> None:
403
+ """
404
+ Print the error message to screen.
405
+ """
406
+ if os.name == "nt":
407
+ # Is windows
408
+ class ConsoleScreenBufferInfo(ctypes.Structure):
409
+ _fields_ = [
410
+ ("dwSize", ctypes.wintypes._COORD), # type: ignore
411
+ ("dwCursorPosition", ctypes.wintypes._COORD), # type: ignore
412
+ ("wAttributes", ctypes.c_ushort), # type: ignore
413
+ ("srWindow", ctypes.wintypes._SMALL_RECT), # type: ignore
414
+ ("dwMaximumWindowSize", ctypes.wintypes._COORD), # type: ignore
415
+ ]
416
+
417
+ std_output_handle = -11
418
+ stdout_handle = ctypes.windll.kernel32.GetStdHandle(std_output_handle)
419
+ csbi = ConsoleScreenBufferInfo()
420
+ ctypes.windll.kernel32.GetConsoleScreenBufferInfo(stdout_handle, ctypes.byref(csbi))
421
+ old_attrs = csbi.wAttributes
422
+ ctypes.windll.kernel32.SetConsoleTextAttribute(stdout_handle, 12)
423
+ print("ERROR: " + message)
424
+ ctypes.windll.kernel32.SetConsoleTextAttribute(stdout_handle, old_attrs)
425
+ else:
426
+ # Non-windows
427
+ print("\033[91m" + "ERROR: " + message + "\033[0m")
428
+ # Log it
429
+ __api.logger.log_error_msg(message)
430
+
431
+
432
+ _exceptions_raised = False
433
+
434
+
435
+ def _handle_exception(exception: Exception | None = None, message: str | None = None) -> None:
436
+ """
437
+ Handles any exception in the way we want.
438
+ """
439
+ if exception is not None:
440
+ if _exceptions_raised:
441
+ raise exception
442
+ if message is not None:
443
+ _print_error_message(message)
444
+ else:
445
+ traceback.print_exc(file=sys.stderr)
446
+ elif message is not None:
447
+ _print_error_message(message)
448
+ if _exceptions_raised:
449
+ raise Exception(message)
450
+ else:
451
+ _print_error_message("UNSPECIFIED")
@@ -0,0 +1,64 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import time
4
+ from builtins import object
5
+
6
+ from genie_python.utilities import compress_and_hex, dehex_decompress_and_dejson
7
+
8
+ # Prefix for block server pvs
9
+ PV_BLOCK_NAMES = "BLOCKNAMES"
10
+ BLOCK_SERVER_PREFIX = "CS:BLOCKSERVER:"
11
+
12
+
13
+ def _blockserver_retry(func):
14
+ def wrapper(*args, **kwargs):
15
+ while True:
16
+ try:
17
+ return func(*args, **kwargs)
18
+ except Exception as e:
19
+ print(
20
+ "Exception thrown from {}: {}, will retry in 15 seconds".format(
21
+ func.__name__, e.__class__.__name__
22
+ )
23
+ )
24
+ time.sleep(15)
25
+
26
+ return wrapper
27
+
28
+
29
+ class BlockServer(object):
30
+ def __init__(self, api):
31
+ self.api = api
32
+
33
+ def _get_pv_value(self, pv, as_string=False):
34
+ """Just a convenient wrapper for calling the api's get_pv_value method"""
35
+ return self.api.get_pv_value(self.api.prefix_pv_name(pv), as_string)
36
+
37
+ def _set_pv_value(self, pv, value, wait=False):
38
+ """Just a convenient wrapper for calling the api's set_pv_value method"""
39
+ return self.api.set_pv_value(self.api.prefix_pv_name(pv), value, wait)
40
+
41
+ @_blockserver_retry
42
+ def get_sample_par_names(self):
43
+ """Get the current sample parameter names as a list."""
44
+ # Get the names from the blockserver
45
+ raw = self._get_pv_value(BLOCK_SERVER_PREFIX + "SAMPLE_PARS", True)
46
+ return dehex_decompress_and_dejson(raw)
47
+
48
+ @_blockserver_retry
49
+ def get_beamline_par_names(self):
50
+ """Get the current beamline parameter names as a list."""
51
+ # Get the names from the blockserver
52
+ raw = self._get_pv_value(BLOCK_SERVER_PREFIX + "BEAMLINE_PARS", True)
53
+ return dehex_decompress_and_dejson(raw)
54
+
55
+ @_blockserver_retry
56
+ def get_runcontrol_settings(self):
57
+ """Get the current run-control settings."""
58
+ raw = self._get_pv_value(BLOCK_SERVER_PREFIX + "GET_RC_PARS", True)
59
+ return dehex_decompress_and_dejson(raw)
60
+
61
+ def reload_current_config(self):
62
+ """Reload the current configuration."""
63
+ raw = compress_and_hex("1")
64
+ self._set_pv_value(BLOCK_SERVER_PREFIX + "RELOAD_CURRENT_CONFIG", raw, True)