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,186 @@
1
+ """
2
+ Genie Database Access module.
3
+ """
4
+
5
+ from typing import TypedDict
6
+
7
+ from genie_python.mysql_abstraction_layer import SQLAbstraction
8
+
9
+ SELECT_FOR_EXP_DETAILS = """
10
+ SELECT e.experimentID, u.name as userName, r.name as roleName,"""
11
+ """ t.startDate, e.duration FROM `experimentteams` t
12
+ JOIN experiment e ON e.experimentID = t.experimentID
13
+ JOIN user u ON u.userID = t.userID
14
+ JOIN role r ON r.roleID = t.roleID
15
+ """
16
+
17
+
18
+ class GetExperimentData:
19
+ """
20
+ Class for storing the get_exp_data RB lookup command and related utility methods.
21
+ """
22
+
23
+ def __init__(self, instrument: str) -> None:
24
+ self.instrument = instrument
25
+ self._sql = SQLAbstraction(
26
+ dbid="exp_data", user="report", password="$report", host=self.instrument
27
+ )
28
+
29
+ def get_sql(self) -> SQLAbstraction:
30
+ """
31
+ Get the one and only SQL abstraction layer.
32
+
33
+ Returns:
34
+ The SQL abstraction layer
35
+ """
36
+ return self._sql
37
+
38
+ def _parameter_is_valid(self, value: bool | int | float | str, column: str, table: str) -> bool:
39
+ """
40
+ Checks whether the given value exists in the table.
41
+
42
+ Args:
43
+ value: the value to search for
44
+ column: the column to search in
45
+ table: the table to search in
46
+
47
+ Returns:
48
+ True if value was found, False otherwise
49
+
50
+ """
51
+ result = self.get_sql().query(
52
+ command=f"SELECT * FROM `{table}` WHERE {column} = %s", bound_variables=(value,)
53
+ )
54
+
55
+ return True if result else False
56
+
57
+ class _GetExpDataReturn(TypedDict):
58
+ rb_number: int | str
59
+ user: str
60
+ role: str
61
+ start_date: str
62
+ duration: float
63
+
64
+ def get_exp_data(
65
+ self, rb: int | str = "%", user: str = "%", role: str = "%", verbose: bool = False
66
+ ) -> list[_GetExpDataReturn]:
67
+ """
68
+ Returns the data of experiments that match the given criteria,
69
+ or all if none is given, from the exp_data
70
+ database. If verbose is enabled, only pretty-print the data.
71
+
72
+ Args:
73
+ rb (int, optional): The RB number of the experiment to look for, Defaults to Any.
74
+ user (str, optional): The name of the user who is running/has
75
+ run the experiment, Defaults to Any.
76
+ role (str, optional): The user role, Defaults to Any.
77
+ verbose (bool, optional): Pretty-print the data, Defaults to False.
78
+
79
+ Returns:
80
+ exp_data (list): The experiment(s) data as a list of dicts.
81
+
82
+ Raises:
83
+ NotFoundError: Thrown if a parameter's value was not found in the database.
84
+
85
+ """
86
+ self._validate_parameters(rb, user, role)
87
+
88
+ args, sql = self._create_sql_statement_and_args(rb, role, user)
89
+
90
+ exp_data = self._query_database_for_data(args, sql)
91
+
92
+ if verbose:
93
+ if exp_data:
94
+ self._pretty_print(exp_data)
95
+ else:
96
+ raise NotFoundError(
97
+ f'Found no experiments that match the given criteria '
98
+ f'(RB: {rb if rb != "%" else "Any"}, '
99
+ f'User: {user if user != "%" else "Any"}, '
100
+ f'Role: {role if role != "%" else "Any"}).\n'
101
+ )
102
+
103
+ return exp_data
104
+
105
+ def _validate_parameters(self, rb: int | str, user: str, role: str) -> None:
106
+ if rb != "%" and not self._parameter_is_valid(rb, "experimentID", "experimentteams"):
107
+ raise NotFoundError(f"Found no experiments with RB number {rb}.")
108
+ if user != "%" and not self._parameter_is_valid(user, "name", "user"):
109
+ raise NotFoundError(
110
+ f'User with name "{user}" was not found. Please make sure the title and name are '
111
+ f'correct (e.g. "Dr John Smith").'
112
+ )
113
+ if role != "%" and not self._parameter_is_valid(role, "name", "role"):
114
+ roles = ", ".join(
115
+ [x[0] for x in self.get_sql().query_returning_cursor("SELECT name FROM `role`", ())]
116
+ )
117
+ raise NotFoundError(f'Role "{role}" was not found. Existing roles: {roles}')
118
+
119
+ def _query_database_for_data(
120
+ self, args: bool | int | float | str, sql: str
121
+ ) -> list[_GetExpDataReturn]:
122
+ exp_data = []
123
+ for exp_id, user, role, start_date, duration in self.get_sql().query_returning_cursor(
124
+ sql, args
125
+ ):
126
+ start_date = start_date.strftime("%Y-%m-%d %H:%M:%S")
127
+ experiment_details = {
128
+ "rb_number": exp_id,
129
+ "user": user,
130
+ "role": role,
131
+ "start_date": start_date,
132
+ "duration": duration,
133
+ }
134
+ exp_data.append(experiment_details)
135
+ return exp_data
136
+
137
+ @staticmethod
138
+ def _create_sql_statement_and_args(
139
+ rb: int | str, user: str, role: str
140
+ ) -> tuple[bool | int | float | str, str]:
141
+ """
142
+ Prepare the statement and bound variables
143
+
144
+ Args:
145
+ rb: rb number of the experiment
146
+ role: role being searched for
147
+ user: user being searched for
148
+
149
+ Returns:
150
+ args: the bound variables
151
+ sql: the SQL command
152
+ """
153
+ sql = SELECT_FOR_EXP_DETAILS + " WHERE "
154
+ where_clauses = [
155
+ "e.experimentID = %s" if rb != "%" else "e.experimentID LIKE '%'",
156
+ "upper(u.name) = upper(%s)" if user != "%" else "u.name LIKE '%'",
157
+ "upper(r.name) = upper(%s)" if role != "%" else "r.name LIKE '%'",
158
+ ]
159
+ args = [x for x in [rb, user, role] if x != "%"]
160
+ sql += " AND ".join(where_clauses) + " ORDER BY experimentID DESC"
161
+ return args, sql
162
+
163
+ @staticmethod
164
+ def _pretty_print(exp_data: list[_GetExpDataReturn]) -> None:
165
+ # For pretty printing
166
+ rb_padding = max(len(x) for x in [y["rb_number"] for y in exp_data])
167
+ user_padding = max(len(x) for x in [y["user"] for y in exp_data])
168
+ role_padding = max(len(x) for x in [y["role"] for y in exp_data])
169
+
170
+ for exp in exp_data:
171
+ print(
172
+ f'Experiment RB number: {exp["rb_number"]:{rb_padding}} | '
173
+ f'User: {exp["user"]:{user_padding}} | '
174
+ f'Role: {exp["role"]:{role_padding}} | '
175
+ f'Start date: {exp["start_date"]} | '
176
+ f'Duration: {exp["duration"]}'
177
+ )
178
+
179
+
180
+ class NotFoundError(IOError):
181
+ """
182
+ Exception that is thrown if a value was not found in the database.
183
+ """
184
+
185
+ def __init__(self, message: str) -> None:
186
+ super(NotFoundError, self).__init__(message)
@@ -0,0 +1,200 @@
1
+ """Logging module for genie"""
2
+
3
+ import getpass
4
+ import logging
5
+ import logging.config
6
+
7
+ import graypy
8
+
9
+
10
+ def send(self, s):
11
+ """
12
+ Needed otherwise graypy spits out logs if the DNS alias cannot be resolved.
13
+ See https://github.com/ISISComputingGroup/IBEX/issues/7059
14
+ """
15
+ try:
16
+ if len(s) < self.gelf_chunker.chunk_size:
17
+ super(graypy.GELFUDPHandler, self).send(s)
18
+ else:
19
+ for chunk in self.gelf_chunker.chunk_message(s):
20
+ super(graypy.GELFUDPHandler, self).send(chunk)
21
+ except Exception:
22
+ # logging to graypy failed, silently fail rather than throwing
23
+ pass
24
+
25
+
26
+ graypy.GELFUDPHandler.send = send
27
+
28
+ import os
29
+ import socket
30
+ from contextlib import contextmanager
31
+ from time import localtime, strftime
32
+
33
+ from genie_python.version import VERSION
34
+
35
+
36
+ class InstrumentFilter(logging.Filter):
37
+ """For Graylog fields (https://graypy.readthedocs.io/en/latest/readme.html#adding-custom-logging-fields)"""
38
+
39
+ def __init__(self):
40
+ self.instrument = ""
41
+ self.sim_mode = False
42
+ self.genie_version = VERSION
43
+ self.reset()
44
+
45
+ def set_inst_name(self, inst_name):
46
+ self.instrument = inst_name
47
+
48
+ def set_sim_mode(self, sim_mode):
49
+ self.sim_mode = sim_mode
50
+
51
+ def filter(self, record):
52
+ record.instrument = self.instrument
53
+ record.command_called = self.command_called
54
+ record.function_args = self.function_args
55
+ record.function_kwargs = self.function_kwargs
56
+ record.time_taken = self.time_taken
57
+ record.exception_text = self.exception_text
58
+ record.from_channel_access = str(self.from_channel_access) # need to cast bool to str
59
+ record.from_ibex = str(os.getenv("FROM_IBEX"))
60
+ record.genie_version = self.genie_version
61
+ record.is_sim_mode = str(self.sim_mode)
62
+ self.reset()
63
+ return True
64
+
65
+ def reset(self):
66
+ """
67
+ Reset filter fields back to blank values.
68
+ """
69
+ self.command_called = ""
70
+ self.function_args = ""
71
+ self.function_kwargs = ""
72
+ self.time_taken = 0
73
+ self.exception_text = ""
74
+ self.from_channel_access = False
75
+
76
+
77
+ filter = InstrumentFilter()
78
+
79
+ vhd_build_machines = ["NDHSPARE11"]
80
+
81
+
82
+ class LoggingConfigurer:
83
+ @staticmethod
84
+ def get_file_name():
85
+ curr_time = localtime()
86
+ current_date_time = strftime("%Y-%m-%d-%a", curr_time)
87
+ return f"genie-{current_date_time}.log"
88
+
89
+ @staticmethod
90
+ def get_log_file_dir():
91
+ if socket.gethostname() in vhd_build_machines:
92
+ return os.path.join("C:", os.sep, "genie_python_logs")
93
+ else:
94
+ if os.name == "nt":
95
+ return os.path.join("C:", os.sep, "Instrument", "Var", "logs", "genie_python")
96
+ else:
97
+ return os.path.join("/tmp/{}/genie_python".format(getpass.getuser()))
98
+
99
+ @staticmethod
100
+ def get_log_file_path():
101
+ logs_dir = LoggingConfigurer.get_log_file_dir()
102
+ filename = LoggingConfigurer.get_file_name()
103
+ return os.path.join(logs_dir, filename)
104
+
105
+ @staticmethod
106
+ def get_logging_config():
107
+ return {
108
+ "version": 1,
109
+ "disable_existing_loggers": False,
110
+ "formatters": {
111
+ "verbose": {
112
+ "format": "[{asctime}] [{process:d}:{thread:d}] [{levelname}]\t{message}",
113
+ "style": "{",
114
+ "datefmt": "%Y-%m-%dT%H:%M:%S",
115
+ }
116
+ },
117
+ "handlers": {
118
+ "graypy": {
119
+ "level": "DEBUG",
120
+ "class": "graypy.GELFUDPHandler",
121
+ "host": "ino.isis.cclrc.ac.uk",
122
+ "port": 12201,
123
+ "formatter": "verbose",
124
+ },
125
+ "file": {
126
+ "level": "DEBUG",
127
+ "formatter": "verbose",
128
+ "class": "logging.FileHandler",
129
+ "filename": LoggingConfigurer.get_log_file_path(),
130
+ "mode": "a",
131
+ },
132
+ },
133
+ "loggers": {
134
+ "genie_python_graylogger": {
135
+ "handlers": ["graypy", "file"],
136
+ "level": "DEBUG",
137
+ "propagate": False,
138
+ "namer": LoggingConfigurer.get_file_name(),
139
+ },
140
+ },
141
+ }
142
+
143
+
144
+ @contextmanager
145
+ def genie_logger():
146
+ try:
147
+ logging.config.dictConfig(LoggingConfigurer.get_logging_config())
148
+ yield logging.getLogger("genie_python_graylogger")
149
+ finally:
150
+ logging.shutdown()
151
+
152
+
153
+ class GenieLogger:
154
+ def __init__(self, sim_mode=False):
155
+ self.sim_mode = sim_mode
156
+ self.logs_dir = LoggingConfigurer.get_log_file_dir()
157
+ if not os.path.exists(self.logs_dir):
158
+ os.makedirs(self.logs_dir)
159
+
160
+ filter.set_sim_mode(self.sim_mode)
161
+ with genie_logger() as logger:
162
+ logger.addFilter(filter)
163
+
164
+ def log_info_msg(self, message):
165
+ with genie_logger() as logger:
166
+ logger.info(self._get_message_with_mode(message))
167
+
168
+ def log_command(self, function_name, arguments, command_exception, time_taken=None):
169
+ filter.command_called = function_name
170
+ filter.function_args = arguments
171
+ filter.exception_text = command_exception
172
+ if time_taken is not None:
173
+ filter.time_taken = time_taken
174
+ with genie_logger() as logger:
175
+ logger.debug(self._get_message_with_mode(f"{function_name} {arguments}"))
176
+
177
+ def log_command_error_msg(self, function_name, error_msg):
178
+ error_msg = f"An exception has occurred as a result of the command:\n{error_msg}"
179
+ filter.command_called = function_name
180
+ filter.exception_text = error_msg
181
+ with genie_logger() as logger:
182
+ logger.error(self._get_message_with_mode(error_msg))
183
+
184
+ def log_error_msg(self, error_msg):
185
+ filter.exception_text = error_msg
186
+ with genie_logger() as logger:
187
+ logger.error(self._get_message_with_mode(error_msg))
188
+
189
+ def log_ca_msg(self, error_msg):
190
+ """Log the CA error (from CaChannel)"""
191
+ filter.exception_text = error_msg
192
+ with genie_logger() as logger:
193
+ logger.error(self._get_message_with_mode(error_msg))
194
+
195
+ def set_sim_mode(self, sim_mode):
196
+ self.sim_mode = sim_mode
197
+ filter.set_sim_mode(sim_mode)
198
+
199
+ def _get_message_with_mode(self, msg):
200
+ return f"(SIMULATION) {msg}" if self.sim_mode is True else msg
@@ -0,0 +1,203 @@
1
+ """
2
+ Wrapping of p4p in genie_python
3
+ """
4
+
5
+ from __future__ import absolute_import, print_function
6
+
7
+ import threading
8
+ from builtins import object
9
+ from collections.abc import Callable
10
+ from typing import TYPE_CHECKING, Optional, Tuple
11
+
12
+ from p4p import Value
13
+ from p4p.client.thread import Context, Subscription
14
+
15
+ from .channel_access_exceptions import WriteAccessException
16
+ from .utilities import waveform_to_string
17
+
18
+ if TYPE_CHECKING:
19
+ from genie_python.genie import PVValue
20
+
21
+ TIMEOUT = 15 # Default timeout for PV set/get
22
+ EXIST_TIMEOUT = 3 # Separate smaller timeout for pv_exists() and searchw() operations
23
+ CACHE = threading.local()
24
+ CACHE_LOCK = threading.local()
25
+
26
+
27
+ class P4PWrapper(object):
28
+ context: Optional[Context] = None
29
+ error_log_function: Optional[Callable[[str], None]] = None
30
+
31
+ # noinspection PyPep8Naming
32
+ @staticmethod
33
+ def _log_error(message: str) -> None:
34
+ """
35
+ Log an error
36
+ Args:
37
+ message: message to log
38
+ """
39
+ if P4PWrapper.error_log_function is not None:
40
+ try:
41
+ P4PWrapper.error_log_function(message)
42
+ except Exception:
43
+ pass
44
+ else:
45
+ print("PVERROR: {}".format(message))
46
+
47
+ @staticmethod
48
+ def set_pv_value(
49
+ name: str,
50
+ value: "PVValue",
51
+ wait: bool = False,
52
+ timeout: float = TIMEOUT,
53
+ safe_not_quick: bool = True,
54
+ ) -> None:
55
+ if safe_not_quick:
56
+ P4PWrapper._check_for_disp(name)
57
+ context = P4PWrapper.get_context()
58
+ context.put(name, value, timeout=timeout, wait=wait)
59
+
60
+ @staticmethod
61
+ def clear_monitor(name: str, timeout: float) -> None:
62
+ try:
63
+ lock = CACHE_LOCK.lock
64
+ except AttributeError:
65
+ lock = CACHE_LOCK.lock = threading.RLock()
66
+ with lock:
67
+ try:
68
+ subscriptions = CACHE.subscriptions
69
+ if name in subscriptions:
70
+ subscriptions.pop(name).close()
71
+ except AttributeError:
72
+ return
73
+
74
+ @staticmethod
75
+ def get_pv_value(
76
+ name: str,
77
+ to_string: bool = False,
78
+ timeout: float = TIMEOUT,
79
+ use_numpy: Optional[bool] = None,
80
+ ) -> "PVValue":
81
+ context = P4PWrapper.get_context()
82
+ output = context.get(name, timeout=timeout)
83
+ if isinstance(output, Exception):
84
+ raise output
85
+
86
+ # Required to convince pyright the type won't be [Exception] which is only a valid response
87
+ # to a list of names. This should be replaced by proper handling for [Value] and [Exception]
88
+ # In a non-minimal/equivalent to CaChannel implementation.
89
+ assert isinstance(output, Value)
90
+
91
+ val = output.value
92
+
93
+ # If it's still a Value type then it's an Enum, so get the index or choice.
94
+ if isinstance(val, Value):
95
+ return val.choices[val.index]
96
+
97
+ if to_string:
98
+ val = str(val)
99
+ return val
100
+
101
+ @staticmethod
102
+ def get_pv_timestamp(name: str, timeout: float = TIMEOUT) -> Tuple[int, int]:
103
+ context = P4PWrapper.get_context()
104
+ output = context.get(name, timeout=timeout)
105
+ if isinstance(output, Exception):
106
+ raise output
107
+
108
+ # Required to convince pyright the type won't be [Exception] which is only a valid response
109
+ # to a list of names. This should be replaced by proper handling for [Value] and [Exception]
110
+ # In a non-minimal/equivalent to CaChannel implementation.
111
+ assert isinstance(output, Value)
112
+
113
+ time = output.timeStamp
114
+ return time.get("secondsPastEpoch"), time.get("nanoseconds")
115
+
116
+ @staticmethod
117
+ def pv_exists(name: str, timeout: float) -> bool:
118
+ try:
119
+ P4PWrapper.get_pv_value(name, timeout=timeout)
120
+ return True
121
+ except TimeoutError:
122
+ return False
123
+
124
+ @staticmethod
125
+ def add_monitor(
126
+ name: str,
127
+ call_back_function: "Callable[[PVValue, str, str], None]",
128
+ link_alarm_on_disconnect: bool = True,
129
+ to_string: bool = False,
130
+ use_numpy: Optional[bool] = None,
131
+ ) -> Subscription:
132
+ def _process_call_back(response: Value | Exception) -> None:
133
+ if isinstance(response, Exception):
134
+ P4PWrapper._log_error(str(response))
135
+ return
136
+ value = response.get("value")
137
+ if isinstance(value, Value):
138
+ value = value.choices[value.index]
139
+ if to_string:
140
+ # Could see if the element count is > 1 instead
141
+ if isinstance(value, list):
142
+ value = waveform_to_string(value)
143
+ else:
144
+ value = str(value)
145
+ elif isinstance(value, Value):
146
+ value = value.index
147
+
148
+ call_back_function(
149
+ value,
150
+ response.get("alarm").get("severity"),
151
+ response.get("alarm").get("status"),
152
+ )
153
+
154
+ context = P4PWrapper.get_context()
155
+ subscription = context.monitor(name, _process_call_back, notify_disconnect=True)
156
+
157
+ # Add to a dict of subscriptions to reproduce ability to close a monitor by its name.
158
+ try:
159
+ lock = CACHE_LOCK.lock
160
+ except AttributeError:
161
+ lock = CACHE_LOCK.lock = threading.RLock()
162
+ with lock:
163
+ try:
164
+ subscriptions = CACHE.subscriptions
165
+ subscriptions.update({name: subscription})
166
+ except AttributeError:
167
+ CACHE.subscriptions = {name: Subscription}
168
+ return subscription
169
+
170
+ @staticmethod
171
+ def get_context(autowrap: bool = False) -> Context:
172
+ try:
173
+ lock = CACHE_LOCK.lock
174
+ except AttributeError:
175
+ lock = CACHE_LOCK.lock = threading.RLock()
176
+
177
+ with lock:
178
+ try:
179
+ thread_context = CACHE.context
180
+ except AttributeError:
181
+ thread_context = CACHE.context = Context("pva", nt=autowrap)
182
+ if thread_context is None:
183
+ thread_context = Context("pva", nt=autowrap)
184
+ return thread_context
185
+
186
+ @staticmethod
187
+ def _check_for_disp(name: str) -> None:
188
+ """
189
+ Check if DISP is set on a PV. If passed a field instead of a PV, do nothing.
190
+ Only check DISP if it exists.
191
+ """
192
+ if (
193
+ ".DISP" not in name
194
+ ): # Do not check for DISP if it's already in the name of the PV to check
195
+ if "." in name: # If given a field on a PV, check the PV itself if DISP is set
196
+ name = name.split(".")[0]
197
+ _disp_name = "{}.DISP".format(name)
198
+ if P4PWrapper.pv_exists(_disp_name, 0) and P4PWrapper.get_pv_value(_disp_name) != "0":
199
+ raise WriteAccessException("{} (DISP is set)".format(name))
200
+
201
+ @staticmethod
202
+ def close_context() -> None:
203
+ P4PWrapper.get_context().close()
@@ -0,0 +1,77 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ from builtins import object, str
4
+ from functools import wraps
5
+
6
+
7
+ def _plotting_func(func):
8
+ """
9
+ Decorator for functions that interact with MPL.
10
+
11
+ Specifically, turns off interactive mode while the graphs are being manipulated and turns
12
+ it back on (and shows the graph) when finished.
13
+ """
14
+
15
+ @wraps(func)
16
+ def wrapper(*args, **kwargs):
17
+ import matplotlib.pyplot as pyplt
18
+
19
+ was_interactive = pyplt.isinteractive()
20
+ pyplt.ioff()
21
+ result = func(*args, **kwargs)
22
+ pyplt.interactive(was_interactive)
23
+ pyplt.show(block=False)
24
+ return result
25
+
26
+ return wrapper
27
+
28
+
29
+ class SpectraPlot(object):
30
+ @_plotting_func
31
+ def __init__(self, api, spectrum, period, dist):
32
+ import matplotlib.pyplot as pyplt
33
+
34
+ self.api = api
35
+ self.spectra = []
36
+ self.fig = pyplt.figure()
37
+ self.ax = pyplt.subplot(111)
38
+ self.ax.autoscale_view(True, True, True)
39
+ self.ax.set_xlabel("Time")
40
+ self.ax.set_ylabel("Counts")
41
+ self.ax.set_title("Spectrum {}".format(spectrum))
42
+ self.add_spectrum(spectrum, period, dist)
43
+
44
+ @_plotting_func
45
+ def add_spectrum(self, spectrum, period=1, dist=True):
46
+ self.spectra.append((spectrum, period, dist))
47
+ data = self.api.dae.get_spectrum(spectrum, period, dist)
48
+ name = "Spect {}".format(spectrum)
49
+ self.ax.plot(data["time"], data["signal"], label=name)
50
+ self.__update_legend()
51
+ return self
52
+
53
+ @_plotting_func
54
+ def refresh(self):
55
+ for i in range(len(self.ax.lines)):
56
+ data = self.api.dae.get_spectrum(
57
+ self.spectra[0][0], self.spectra[0][1], self.spectra[0][2]
58
+ )
59
+ line = self.ax.lines[i]
60
+ line.set_data(data["time"], data["signal"])
61
+ self.ax.autoscale_view(True, True, True)
62
+
63
+ @_plotting_func
64
+ def delete_plot(self, plotnum):
65
+ del self.ax.lines[plotnum]
66
+ del self.spectra[plotnum]
67
+ self.__update_legend()
68
+
69
+ def __update_legend(self):
70
+ handles, labels = self.ax.get_legend_handles_labels()
71
+ self.ax.legend(handles, labels)
72
+
73
+ def __repr__(self):
74
+ if len(self.spectra) > 0:
75
+ return "Spectra plot ({})".format(", ".join([str(x[0]) for x in self.spectra]))
76
+ else:
77
+ return "Spectra plot (empty)"
@@ -0,0 +1,21 @@
1
+ from builtins import object
2
+
3
+
4
+ class PrePostCmdManager(object):
5
+ """
6
+ A class to manager the precmd and postcmd commands such as used in begin, end, abort, resume, pause.
7
+ """
8
+
9
+ def __init__(self):
10
+ self.begin_precmd = lambda **pars: None
11
+ self.begin_postcmd = lambda **pars: None
12
+ self.abort_precmd = lambda **pars: None
13
+ self.abort_postcmd = lambda **pars: None
14
+ self.end_precmd = lambda **pars: None
15
+ self.end_postcmd = lambda **pars: None
16
+ self.pause_precmd = lambda **pars: None
17
+ self.pause_postcmd = lambda **pars: None
18
+ self.resume_precmd = lambda **pars: None
19
+ self.resume_postcmd = lambda **pars: None
20
+ self.cset_precmd = lambda **pars: True
21
+ self.cset_postcmd = lambda **pars: None