pytrms 0.9.2__py3-none-any.whl → 0.9.5__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.
pytrms/__init__.py CHANGED
@@ -1,38 +1,42 @@
1
- _version = '0.9.2'
2
-
3
- __all__ = ['load', 'connect']
4
-
5
-
6
- def load(path):
7
- '''Open a datafile for post-analysis or batch processing.
8
-
9
- `path` may be a glob-expression to collect a whole batch.
10
-
11
- returns a `Measurement` instance.
12
- '''
13
- import glob
14
- from .measurement import FinishedMeasurement
15
-
16
- files = glob.glob(path)
17
-
18
- return FinishedMeasurement(*files)
19
-
20
- def connect(host=None, method='webapi'):
21
- '''Connect a client to a running measurement server.
22
-
23
- 'method' is the preferred connection, either 'webapi' (default) or 'modbus'.
24
-
25
- returns an `Instrument` if connected successfully.
26
- '''
27
- from .instrument import Instrument
28
-
29
- if method.lower() == 'webapi':
30
- from .clients.ioniclient import IoniClient
31
- return IoniClient(host)
32
-
33
- if method.lower() == 'modbus':
34
- from .modbus import IoniconModbus
35
- return IoniconModbus(host)
36
-
37
- raise NotImplementedError(str(method))
38
-
1
+ _version = '0.9.5'
2
+
3
+ __all__ = ['load', 'connect']
4
+
5
+
6
+ def load(path):
7
+ '''Open a datafile for post-analysis or batch processing.
8
+
9
+ `path` may be a glob-expression to collect a whole batch.
10
+
11
+ returns a `Measurement` instance.
12
+ '''
13
+ import glob
14
+ from .measurement import FinishedMeasurement
15
+
16
+ files = glob.glob(path)
17
+
18
+ return FinishedMeasurement(*files)
19
+
20
+
21
+ def connect(host='localhost', port=None, method='mqtt'):
22
+ '''Connect a client to a running measurement server.
23
+
24
+ 'method' is the preferred connection, either 'mqtt' (default), 'webapi' or 'modbus'.
25
+
26
+ returns an `Instrument` if connected successfully.
27
+ '''
28
+ from .instrument import Instrument
29
+
30
+ if method.lower() == 'mqtt':
31
+ from .clients.mqtt import MqttClient as _client
32
+ elif method.lower() == 'webapi':
33
+ from .clients.ioniclient import IoniClient as _client
34
+ elif method.lower() == 'modbus':
35
+ from .modbus import IoniconModbus as _client
36
+ else:
37
+ raise NotImplementedError(str(method))
38
+
39
+ backend = _client(host, port) if port is not None else _client(host)
40
+
41
+ return Instrument(backend)
42
+
pytrms/_base/__init__.py CHANGED
@@ -1,24 +1,24 @@
1
- from collections import namedtuple
2
-
3
- from .mqttclient import MqttClientBase
4
- from .ioniclient import IoniClientBase
5
-
6
- class itype:
7
-
8
- table_setting_t = namedtuple('mass_mapping', ['name', 'mass2value'])
9
- timecycle_t = namedtuple('timecycle', ['rel_cycle','abs_cycle','abs_time','rel_time'])
10
- masscal_t = namedtuple('masscal', ['mode', 'masses', 'timebins', 'cal_pars', 'cal_segs'])
11
- add_data_item_t = namedtuple('add_data', ['value', 'name', 'unit', 'view'])
12
- fullcycle_t = namedtuple('fullcycle', ['timecycle', 'intensity', 'mass_cal', 'add_data'])
13
-
14
- AME_RUN = 8
15
- AME_STEP = 7
16
- AME_ACTION = 5
17
- USE_MEAN = 2 # (only in AUTO_UseMean)
18
-
19
- REACT_Udrift = 0
20
- REACT_pDrift = 1
21
- REACT_Tdrift = 2
22
- REACT_PI_Idx = 4 # skipping E/N = 3
23
- REACT_TM_Idx = 5
24
-
1
+ from collections import namedtuple
2
+
3
+ from .mqttclient import MqttClientBase
4
+ from .ioniclient import IoniClientBase
5
+
6
+ class itype:
7
+
8
+ table_setting_t = namedtuple('mass_mapping', ['name', 'mass2value'])
9
+ timecycle_t = namedtuple('timecycle', ['rel_cycle','abs_cycle','abs_time','rel_time'])
10
+ masscal_t = namedtuple('masscal', ['mode', 'masses', 'timebins', 'cal_pars', 'cal_segs'])
11
+ add_data_item_t = namedtuple('add_data', ['value', 'name', 'unit', 'view'])
12
+ fullcycle_t = namedtuple('fullcycle', ['timecycle', 'intensity', 'mass_cal', 'add_data'])
13
+
14
+ AME_RUN = 8
15
+ AME_STEP = 7
16
+ AME_ACTION = 5
17
+ USE_MEAN = 2 # (only in AUTO_UseMean)
18
+
19
+ REACT_Udrift = 0
20
+ REACT_pDrift = 1
21
+ REACT_Tdrift = 2
22
+ REACT_PI_Idx = 4 # skipping E/N = 3
23
+ REACT_TM_Idx = 5
24
+
@@ -1,32 +1,32 @@
1
- from abc import ABC, abstractmethod
2
-
3
- class IoniClientBase(ABC):
4
-
5
- @property
6
- @abstractmethod
7
- def is_connected(self):
8
- '''Returns `True` if connection to IoniTOF could be established.'''
9
- pass
10
-
11
- @property
12
- @abstractmethod
13
- def is_running(self):
14
- '''Returns `True` if IoniTOF is currently acquiring data.'''
15
- pass
16
-
17
- @abstractmethod
18
- def connect(self, timeout_s):
19
- pass
20
-
21
- @abstractmethod
22
- def disconnect(self):
23
- pass
24
-
25
- def __init__(self, host, port):
26
- # Note: circumvent (potentially sluggish) Windows DNS lookup:
27
- self.host = '127.0.0.1' if host == 'localhost' else str(host)
28
- self.port = int(port)
29
-
30
- def __repr__(self):
31
- return f"<{self.__class__.__name__} @ {self.host}[:{self.port}]>"
32
-
1
+ from abc import ABC, abstractmethod
2
+
3
+ class IoniClientBase(ABC):
4
+
5
+ @property
6
+ @abstractmethod
7
+ def is_connected(self):
8
+ '''Returns `True` if connection to IoniTOF could be established.'''
9
+ pass
10
+
11
+ @property
12
+ @abstractmethod
13
+ def is_running(self):
14
+ '''Returns `True` if IoniTOF is currently acquiring data.'''
15
+ pass
16
+
17
+ @abstractmethod
18
+ def connect(self, timeout_s):
19
+ pass
20
+
21
+ @abstractmethod
22
+ def disconnect(self):
23
+ pass
24
+
25
+ def __init__(self, host, port):
26
+ # Note: circumvent (potentially sluggish) Windows DNS lookup:
27
+ self.host = '127.0.0.1' if host == 'localhost' else str(host)
28
+ self.port = int(port)
29
+
30
+ def __repr__(self):
31
+ return f"<{self.__class__.__name__} @ {self.host}[:{self.port}]>"
32
+
@@ -1,119 +1,119 @@
1
- import os
2
- import time
3
- import logging
4
- import json
5
- from collections import deque
6
- from itertools import cycle
7
- from threading import Condition, RLock
8
- from datetime import datetime as dt
9
-
10
- import paho.mqtt.client
11
-
12
- from .ioniclient import IoniClientBase
13
-
14
- log = logging.getLogger(__name__)
15
-
16
- __all__ = ['MqttClientBase']
17
-
18
-
19
- def _on_connect(client, self, flags, rc):
20
- # Note: ensure subscription after re-connecting,
21
- # wildcards are '+' (one level), '#' (all levels):
22
- default_QoS = 2
23
- topics = set()
24
- for subscriber in self._subscriber_functions:
25
- topics.update(set(getattr(subscriber, "topics", [])))
26
- subs = sorted(zip(topics, cycle([default_QoS])))
27
- log.debug(f"[{self}] " + "\n --> ".join(["subscribing to"] + list(map(str, subs))))
28
- rv = client.subscribe(subs)
29
- log.info(f"[{self}] successfully connected with {rv = }")
30
-
31
- def _on_subscribe(client, self, mid, granted_qos):
32
- log.info(f"[{self}] successfully subscribed with {mid = } | {granted_qos = }")
33
-
34
- def _on_publish(client, self, mid):
35
- log.debug(f"[{self}] published {mid = }")
36
-
37
-
38
- class MqttClientBase(IoniClientBase):
39
-
40
- @property
41
- def is_connected(self):
42
- '''Returns `True` if connected to the server.
43
-
44
- Note: this property will be polled on initialization and should
45
- return `True` if a connection could be established!
46
- '''
47
- return (True
48
- and self.client.is_connected())
49
-
50
- def __init__(self, host, port, subscriber_functions,
51
- on_connect, on_subscribe, on_publish,
52
- connect_timeout_s=10):
53
- assert len(subscriber_functions) > 0, "no subscribers: for some unknown reason this causes disconnects"
54
- super().__init__(host, port)
55
-
56
- # Note: Version 2.0 of paho-mqtt introduced versioning of the user-callback to fix
57
- # some inconsistency in callback arguments and to provide better support for MQTTv5.
58
- # VERSION1 of the callback is deprecated, but is still supported in version 2.x.
59
- # If you want to upgrade to the newer version of the API callback, you will need
60
- # to update your callbacks:
61
- paho_version = int(paho.mqtt.__version__.split('.')[0])
62
- if paho_version == 1:
63
- self.client = paho.mqtt.client.Client(clean_session=True)
64
- elif paho_version == 2:
65
- self.client = paho.mqtt.client.Client(paho.mqtt.client.CallbackAPIVersion.VERSION1,
66
- clean_session=True)
67
- else:
68
- # see https://eclipse.dev/paho/files/paho.mqtt.python/html/migrations.html
69
- raise NotImplementedError("API VERSION2 for MQTTv5 (use paho-mqtt 2.x or implement user callbacks)")
70
-
71
- # clean_session is a boolean that determines the client type. If True,
72
- # the broker will remove all information about this client when it
73
- # disconnects. If False, the client is a persistent client and
74
- # subscription information and queued messages will be retained when the
75
- # client disconnects.
76
- # The clean_session argument only applies to MQTT versions v3.1.1 and v3.1.
77
- # It is not accepted if the MQTT version is v5.0 - use the clean_start
78
- # argument on connect() instead.
79
- self.client.on_connect = on_connect if on_connect is not None else _on_connect
80
- self.client.on_subscribe = on_subscribe if on_subscribe is not None else _on_subscribe
81
- self.client.on_publish = on_publish if on_publish is not None else _on_publish
82
- # ...subscribe to topics...
83
- self._subscriber_functions = list(subscriber_functions)
84
- for subscriber in self._subscriber_functions:
85
- for topic in getattr(subscriber, "topics", []):
86
- self.client.message_callback_add(topic, subscriber)
87
- # ...pass this instance to each callback...
88
- self.client.user_data_set(self)
89
- # ...and connect to the server:
90
- try:
91
- self.connect(connect_timeout_s)
92
- except TimeoutError as exc:
93
- log.warn(f"{exc} (retry connecting when the Instrument is set up)")
94
-
95
- def connect(self, timeout_s=10):
96
- log.info(f"[{self}] connecting to MQTT broker...")
97
- self.client.connect(self.host, self.port, timeout_s)
98
- self.client.loop_start() # runs in a background thread
99
- started_at = time.monotonic()
100
- while time.monotonic() < started_at + timeout_s:
101
- if self.is_connected:
102
- break
103
-
104
- time.sleep(10e-3)
105
- else:
106
- self.disconnect()
107
- raise TimeoutError(f"[{self}] no connection to IoniTOF")
108
-
109
- def publish_with_ack(self, *args, timeout_s=10, **kwargs):
110
- # Note: this is important when publishing just before exiting the application
111
- # to ensure that all messages get through (timeout_s is set on `.__init__()`)
112
- msg = self.client.publish(*args, **kwargs)
113
- msg.wait_for_publish(timeout=timeout_s)
114
- return msg
115
-
116
- def disconnect(self):
117
- self.client.loop_stop()
118
- self.client.disconnect()
119
-
1
+ import os
2
+ import time
3
+ import logging
4
+ import json
5
+ from collections import deque
6
+ from itertools import cycle
7
+ from threading import Condition, RLock
8
+ from datetime import datetime as dt
9
+
10
+ import paho.mqtt.client
11
+
12
+ from .ioniclient import IoniClientBase
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+ __all__ = ['MqttClientBase']
17
+
18
+
19
+ def _on_connect(client, self, flags, rc):
20
+ # Note: ensure subscription after re-connecting,
21
+ # wildcards are '+' (one level), '#' (all levels):
22
+ default_QoS = 2
23
+ topics = set()
24
+ for subscriber in self._subscriber_functions:
25
+ topics.update(set(getattr(subscriber, "topics", [])))
26
+ subs = sorted(zip(topics, cycle([default_QoS])))
27
+ log.debug(f"[{self}] " + "\n --> ".join(["subscribing to"] + list(map(str, subs))))
28
+ rv = client.subscribe(subs)
29
+ log.info(f"[{self}] successfully connected with {rv = }")
30
+
31
+ def _on_subscribe(client, self, mid, granted_qos):
32
+ log.info(f"[{self}] successfully subscribed with {mid = } | {granted_qos = }")
33
+
34
+ def _on_publish(client, self, mid):
35
+ log.debug(f"[{self}] published {mid = }")
36
+
37
+
38
+ class MqttClientBase(IoniClientBase):
39
+
40
+ @property
41
+ def is_connected(self):
42
+ '''Returns `True` if connected to the server.
43
+
44
+ Note: this property will be polled on initialization and should
45
+ return `True` if a connection could be established!
46
+ '''
47
+ return (True
48
+ and self.client.is_connected())
49
+
50
+ def __init__(self, host, port, subscriber_functions,
51
+ on_connect, on_subscribe, on_publish,
52
+ connect_timeout_s=10):
53
+ assert len(subscriber_functions) > 0, "no subscribers: for some unknown reason this causes disconnects"
54
+ super().__init__(host, port)
55
+
56
+ # Note: Version 2.0 of paho-mqtt introduced versioning of the user-callback to fix
57
+ # some inconsistency in callback arguments and to provide better support for MQTTv5.
58
+ # VERSION1 of the callback is deprecated, but is still supported in version 2.x.
59
+ # If you want to upgrade to the newer version of the API callback, you will need
60
+ # to update your callbacks:
61
+ paho_version = int(paho.mqtt.__version__.split('.')[0])
62
+ if paho_version == 1:
63
+ self.client = paho.mqtt.client.Client(clean_session=True)
64
+ elif paho_version == 2:
65
+ self.client = paho.mqtt.client.Client(paho.mqtt.client.CallbackAPIVersion.VERSION1,
66
+ clean_session=True)
67
+ else:
68
+ # see https://eclipse.dev/paho/files/paho.mqtt.python/html/migrations.html
69
+ raise NotImplementedError("API VERSION2 for MQTTv5 (use paho-mqtt 2.x or implement user callbacks)")
70
+
71
+ # clean_session is a boolean that determines the client type. If True,
72
+ # the broker will remove all information about this client when it
73
+ # disconnects. If False, the client is a persistent client and
74
+ # subscription information and queued messages will be retained when the
75
+ # client disconnects.
76
+ # The clean_session argument only applies to MQTT versions v3.1.1 and v3.1.
77
+ # It is not accepted if the MQTT version is v5.0 - use the clean_start
78
+ # argument on connect() instead.
79
+ self.client.on_connect = on_connect if on_connect is not None else _on_connect
80
+ self.client.on_subscribe = on_subscribe if on_subscribe is not None else _on_subscribe
81
+ self.client.on_publish = on_publish if on_publish is not None else _on_publish
82
+ # ...subscribe to topics...
83
+ self._subscriber_functions = list(subscriber_functions)
84
+ for subscriber in self._subscriber_functions:
85
+ for topic in getattr(subscriber, "topics", []):
86
+ self.client.message_callback_add(topic, subscriber)
87
+ # ...pass this instance to each callback...
88
+ self.client.user_data_set(self)
89
+ # ...and connect to the server:
90
+ try:
91
+ self.connect(connect_timeout_s)
92
+ except TimeoutError as exc:
93
+ log.warn(f"{exc} (retry connecting when the Instrument is set up)")
94
+
95
+ def connect(self, timeout_s=10):
96
+ log.info(f"[{self}] connecting to MQTT broker...")
97
+ self.client.connect(self.host, self.port, timeout_s)
98
+ self.client.loop_start() # runs in a background thread
99
+ started_at = time.monotonic()
100
+ while time.monotonic() < started_at + timeout_s:
101
+ if self.is_connected:
102
+ break
103
+
104
+ time.sleep(10e-3)
105
+ else:
106
+ self.disconnect()
107
+ raise TimeoutError(f"[{self}] no connection to IoniTOF")
108
+
109
+ def publish_with_ack(self, *args, timeout_s=10, **kwargs):
110
+ # Note: this is important when publishing just before exiting the application
111
+ # to ensure that all messages get through (timeout_s is set on `.__init__()`)
112
+ msg = self.client.publish(*args, **kwargs)
113
+ msg.wait_for_publish(timeout=timeout_s)
114
+ return msg
115
+
116
+ def disconnect(self):
117
+ self.client.loop_stop()
118
+ self.client.disconnect()
119
+
pytrms/_version.py CHANGED
@@ -1,26 +1,26 @@
1
- __version__ = '0.2.2' # must be on the first line!
2
-
3
- __all__ = ['branch', 'digest', '__version__']
4
-
5
-
6
- from os.path import abspath, dirname, join, isdir
7
-
8
- # 'gitdir' might be defined by the setup.py script..
9
- if not 'gitdir' in vars():
10
- gitdir = abspath(join(dirname(__file__), '..', '.git'))
11
-
12
- # in development mode: get git branch and digest..
13
- if isdir(gitdir):
14
- with open(join(gitdir, 'HEAD')) as f:
15
- refinfo = f.readline().strip()
16
- detached = not refinfo.startswith('ref:')
17
- if detached:
18
- branch = 'detached-HEAD'
19
- digest = refinfo[:7]
20
- else:
21
- refpath = refinfo.split()[-1]
22
- branch = refinfo.split('/')[-1]
23
- digest = open(join(gitdir, refpath)).read(7)
24
-
25
- if not branch == 'master':
26
- __version__ += '.dev' + str(eval('0x'+digest))
1
+ __version__ = '0.2.2' # must be on the first line!
2
+
3
+ __all__ = ['branch', 'digest', '__version__']
4
+
5
+
6
+ from os.path import abspath, dirname, join, isdir
7
+
8
+ # 'gitdir' might be defined by the setup.py script..
9
+ if not 'gitdir' in vars():
10
+ gitdir = abspath(join(dirname(__file__), '..', '.git'))
11
+
12
+ # in development mode: get git branch and digest..
13
+ if isdir(gitdir):
14
+ with open(join(gitdir, 'HEAD')) as f:
15
+ refinfo = f.readline().strip()
16
+ detached = not refinfo.startswith('ref:')
17
+ if detached:
18
+ branch = 'detached-HEAD'
19
+ digest = refinfo[:7]
20
+ else:
21
+ refpath = refinfo.split()[-1]
22
+ branch = refinfo.split('/')[-1]
23
+ digest = open(join(gitdir, refpath)).read(7)
24
+
25
+ if not branch == 'master':
26
+ __version__ += '.dev' + str(eval('0x'+digest))
@@ -1,33 +1,33 @@
1
- import os
2
-
3
- _root = os.path.dirname(__file__)
4
- _par_id_file = os.path.abspath(os.path.join(_root, '..', 'data', 'ParaIDs.csv'))
5
- assert os.path.exists(_par_id_file), "par-id file not found: please re-install PyTRMS package"
6
-
7
-
8
- import logging as _logging
9
-
10
- _logging.TRACE = 5 # even more verbose than logging.DEBUG
11
-
12
- def enable_extended_logging(log_level=_logging.DEBUG):
13
- '''make output of http-requests more talkative.
14
-
15
- set 'log_level=logging.TRACE' (defined as 0 in pytrms.__init__) for highest verbosity!
16
- '''
17
- if log_level <= _logging.DEBUG:
18
- # enable logging of http request urls on the library, that is
19
- # underlying the 'requests'-package:
20
- _logging.warn(f"enabling logging-output on 'urllib3' ({log_level = })")
21
- requests_log = _logging.getLogger("urllib3")
22
- requests_log.setLevel(log_level)
23
- requests_log.propagate = True
24
-
25
- if log_level <= _logging.TRACE:
26
- # Enabling debugging at http.client level (requests->urllib3->http.client)
27
- # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with
28
- # HEADERS but without DATA. the only thing missing will be the response.body,
29
- # which is not logged.
30
- _logging.warn(f"enabling logging-output on 'HTTPConnection' ({log_level = })")
31
- from http.client import HTTPConnection
32
- HTTPConnection.debuglevel = 1
33
-
1
+ import os
2
+
3
+ _root = os.path.dirname(__file__)
4
+ _par_id_file = os.path.abspath(os.path.join(_root, '..', 'data', 'ParaIDs.csv'))
5
+ assert os.path.exists(_par_id_file), "par-id file not found: please re-install PyTRMS package"
6
+
7
+
8
+ import logging as _logging
9
+
10
+ _logging.TRACE = 5 # even more verbose than logging.DEBUG
11
+
12
+ def enable_extended_logging(log_level=_logging.DEBUG):
13
+ '''make output of http-requests more talkative.
14
+
15
+ set 'log_level=logging.TRACE' (defined as 0 in pytrms.__init__) for highest verbosity!
16
+ '''
17
+ if log_level <= _logging.DEBUG:
18
+ # enable logging of http request urls on the library, that is
19
+ # underlying the 'requests'-package:
20
+ _logging.warn(f"enabling logging-output on 'urllib3' ({log_level = })")
21
+ requests_log = _logging.getLogger("urllib3")
22
+ requests_log.setLevel(log_level)
23
+ requests_log.propagate = True
24
+
25
+ if log_level <= _logging.TRACE:
26
+ # Enabling debugging at http.client level (requests->urllib3->http.client)
27
+ # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with
28
+ # HEADERS but without DATA. the only thing missing will be the response.body,
29
+ # which is not logged.
30
+ _logging.warn(f"enabling logging-output on 'HTTPConnection' ({log_level = })")
31
+ from http.client import HTTPConnection
32
+ HTTPConnection.debuglevel = 1
33
+