pytrms 0.9.1__py3-none-any.whl → 0.9.2__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,4 +1,4 @@
1
- _version = '0.9.1'
1
+ _version = '0.9.2'
2
2
 
3
3
  __all__ = ['load', 'connect']
4
4
 
@@ -6,14 +6,16 @@ __all__ = ['load', 'connect']
6
6
  def load(path):
7
7
  '''Open a datafile for post-analysis or batch processing.
8
8
 
9
- returns a `Measurement`.
9
+ `path` may be a glob-expression to collect a whole batch.
10
+
11
+ returns a `Measurement` instance.
10
12
  '''
11
- from .measurement import OfflineMeasurement
12
- from .readers import IoniTOFReader
13
+ import glob
14
+ from .measurement import FinishedMeasurement
13
15
 
14
- reader = IoniTOFReader(path)
16
+ files = glob.glob(path)
15
17
 
16
- return OfflineMeasurement(reader)
18
+ return FinishedMeasurement(*files)
17
19
 
18
20
  def connect(host=None, method='webapi'):
19
21
  '''Connect a client to a running measurement server.
@@ -6,9 +6,8 @@ from collections import deque
6
6
  from itertools import cycle
7
7
  from threading import Condition, RLock
8
8
  from datetime import datetime as dt
9
- from abc import ABC, abstractmethod
10
9
 
11
- import paho.mqtt.client as mqtt
10
+ import paho.mqtt.client
12
11
 
13
12
  from .ioniclient import IoniClientBase
14
13
 
@@ -39,7 +38,6 @@ def _on_publish(client, self, mid):
39
38
  class MqttClientBase(IoniClientBase):
40
39
 
41
40
  @property
42
- @abstractmethod
43
41
  def is_connected(self):
44
42
  '''Returns `True` if connected to the server.
45
43
 
@@ -49,12 +47,27 @@ class MqttClientBase(IoniClientBase):
49
47
  return (True
50
48
  and self.client.is_connected())
51
49
 
52
- def __init__(self, host, subscriber_functions,
50
+ def __init__(self, host, port, subscriber_functions,
53
51
  on_connect, on_subscribe, on_publish,
54
52
  connect_timeout_s=10):
55
- super().__init__(host, port=1883)
56
- # configure connection...
57
- self.client = mqtt.Client(clean_session=True)
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
+
58
71
  # clean_session is a boolean that determines the client type. If True,
59
72
  # the broker will remove all information about this client when it
60
73
  # disconnects. If False, the client is a persistent client and
@@ -7,7 +7,7 @@ assert os.path.exists(_par_id_file), "par-id file not found: please re-install P
7
7
 
8
8
  import logging as _logging
9
9
 
10
- _logging.TRACE = 0 # overwrites logging.NOTSET
10
+ _logging.TRACE = 5 # even more verbose than logging.DEBUG
11
11
 
12
12
  def enable_extended_logging(log_level=_logging.DEBUG):
13
13
  '''make output of http-requests more talkative.
@@ -22,7 +22,7 @@ def enable_extended_logging(log_level=_logging.DEBUG):
22
22
  requests_log.setLevel(log_level)
23
23
  requests_log.propagate = True
24
24
 
25
- if log_level == _logging.TRACE:
25
+ if log_level <= _logging.TRACE:
26
26
  # Enabling debugging at http.client level (requests->urllib3->http.client)
27
27
  # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with
28
28
  # HEADERS but without DATA. the only thing missing will be the response.body,
pytrms/clients/db_api.py CHANGED
@@ -172,6 +172,11 @@ class IoniConnect(IoniClientBase):
172
172
  """Follow the server-sent-events (SSE) on the DB-API.
173
173
 
174
174
  `event_re` a regular expression to filter events (default: matches everything)
175
+
176
+ Note: This will block until a matching event is received.
177
+ Especially, it cannot be cancelled by KeyboardInterrupt (due to the `requests`
178
+ stream-implementation), unless the server sends a keep-alive at regular
179
+ intervals (as every well-behaved server should be doing)!
175
180
  """
176
181
  yield from SSEventListener(event_re, host_url=self.url, endpoint="/api/events",
177
182
  session=self.session)
pytrms/clients/mqtt.py CHANGED
@@ -361,6 +361,10 @@ def follow_act_set_values(client, self, msg):
361
361
  # Note: this topic doesn't strictly follow the convention and is handled separately
362
362
  return
363
363
 
364
+ if server == "Sequencer":
365
+ # Note: this is a separate program and will be ignored (has its own AUTO_-numbers et.c.)
366
+ return
367
+
364
368
  if parID == "PTR_CalcConzInfo":
365
369
  # another "special" topic handled in 'follow_calc_conz_info' ...
366
370
  return
@@ -497,9 +501,9 @@ class MqttClient(MqttClientBase):
497
501
  return self._overallcycle[0]
498
502
  return 0
499
503
 
500
- def __init__(self, host='127.0.0.1'):
504
+ def __init__(self, host='127.0.0.1', port=1883):
501
505
  # this sets up the mqtt connection with default callbacks:
502
- super().__init__(host, _subscriber_functions, None, None, None)
506
+ super().__init__(host, port, _subscriber_functions, None, None, None)
503
507
  log.debug(f"connection check ({self.is_connected}) :: {self._server_state = } / {self._sched_cmds = }");
504
508
 
505
509
  def disconnect(self):
pytrms/clients/ssevent.py CHANGED
@@ -6,7 +6,7 @@ import requests
6
6
 
7
7
  from . import _logging
8
8
 
9
- log = _logging.getLogger()
9
+ log = _logging.getLogger(__name__)
10
10
 
11
11
  _event_rv = namedtuple('ssevent', ['event', 'data'])
12
12
 
@@ -19,7 +19,7 @@ class SSEventListener(Iterable):
19
19
  for bite in response.iter_content(chunk_size=1, decode_unicode=True):
20
20
  line += bite
21
21
  if bite == '\n':
22
- yield line
22
+ yield line.strip()
23
23
  line = ''
24
24
 
25
25
  def __init__(self, event_re=None, host_url='http://127.0.0.1:5066',
@@ -59,19 +59,24 @@ class SSEventListener(Iterable):
59
59
 
60
60
  event = msg = ''
61
61
  for line in self._line_stream(self._connect_response): # blocks...
62
- if not line.strip():
62
+ if not line:
63
+ # an empty line concludes an event
63
64
  if event and any(re.match(sub, event) for sub in self.subscriptions):
64
65
  yield _event_rv(event, msg)
65
- event = msg = ''
66
- else:
67
- log.log(_logging.TRACE, "sse: still alive...")
66
+
67
+ # Note: any further empty lines are ignored (may be used as keep-alive),
68
+ # but in either case clear event and msg to rearm for the next event:
69
+ event = msg = ''
68
70
  continue
69
71
 
70
72
  key, val = line.split(':', maxsplit=1)
71
- if key == 'event':
72
- event = val.strip()
73
+ if not key:
74
+ # this is a comment, starting with a colon ':' ...
75
+ log.log(_logging.TRACE, "sse:" + val)
76
+ elif key == 'event':
77
+ event = val.lstrip()
73
78
  elif key == 'data':
74
- msg += val.strip()
79
+ msg += val.lstrip()
75
80
  else:
76
81
  log.warning(f"unknown SSE-key <{key}> in stream")
77
82
 
pytrms/instrument.py CHANGED
@@ -17,7 +17,6 @@ class Instrument(ABC):
17
17
  Note, that for every client PTR instrument there is only one instance of this class.
18
18
  This is to prevent different instances to be in other states than the instrument.
19
19
  '''
20
-
21
20
  __instance = None
22
21
 
23
22
  def _new_state(self, newstate):
@@ -69,38 +68,41 @@ class Instrument(ABC):
69
68
  """Set a variable to a new value."""
70
69
  return self.backend.set(varname, value, unit='-')
71
70
 
71
+ _current_sourcefile = ''
72
+
72
73
  def start_measurement(self, filename=''):
73
74
  # this method must be implemented by each state
74
75
  raise RuntimeError("can't start %s" % self.__class__)
75
76
 
76
- start_measurement.__doc__ = Measurement.start.__doc__
77
-
78
77
  def stop_measurement(self):
79
78
  # this method must be implemented by each state
80
79
  raise RuntimeError("can't stop %s" % self.__class__)
81
80
 
82
- stop_measurement.__doc__ = Measurement.stop.__doc__
83
-
84
81
 
85
82
  class IdleInstrument(Instrument):
86
83
 
87
84
  def start_measurement(self, filename=''):
88
- # if we send a filepath to the server that does not exist there, the server will
89
- # open a dialog and "hang" (which I'd very much like to avoid).
90
- # the safest way is to not send a path at all and start a 'Quick' measurement.
91
- # but if the server is the local machine, we do our best to verify the path:
92
- if filename and self.is_local:
93
- home = os.path.dirname(filename)
94
- os.makedirs(home, exist_ok=True)
95
- base = os.path.basename(filename)
96
- if not base:
97
- base = '%Y-%m-%d_%H-%M-%S.h5'
98
- base = time.strftime(base)
99
- filename = os.path.join(home, base)
85
+ dirname = os.path.dirname(filename)
86
+ if dirname and self.is_local:
87
+ # Note: if we send a filepath to the server that does not exist there, the
88
+ # server will open a dialog and "hang" (which I'd very much like to avoid).
89
+ # the safest way is to not send a path at all and start a 'Quick' measurement.
90
+ # but if the server is the local machine, we do our best to verify the path:
91
+ os.makedirs(dirname, exist_ok=True)
92
+
93
+ if filename:
94
+ basename = os.path.basename(filename)
95
+ # this may very well be a directory to record a filename into:
96
+ if not basename:
97
+ basename = '%Y-%m-%d_%H-%M-%S.h5'
98
+ filename = os.path.join(dirname, basename)
99
+ # finally, pass everything through strftime...
100
+ filename = time.strftime(filename)
100
101
  if os.path.exists(filename):
101
102
  raise RuntimeError(f'filename exists and cannot be overwritten')
102
103
 
103
104
  self.backend.start_measurement(filename)
105
+ self._current_sourcefile = filename
104
106
  self._new_state(RunningInstrument)
105
107
 
106
108
  return RunningMeasurement(self)
@@ -112,3 +114,6 @@ class RunningInstrument(Instrument):
112
114
  self.backend.stop_measurement()
113
115
  self._new_state(IdleInstrument)
114
116
 
117
+ # TODO :: this catches only one sourcefile.. it'll do for simple cases:
118
+ return FinishedMeasurement(_current_sourcefile)
119
+
pytrms/measurement.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import time
2
- from glob import glob
3
2
  from operator import attrgetter
4
3
  from itertools import chain
5
4
  from abc import abstractmethod, ABC
6
5
 
6
+ import pandas as pd
7
+
7
8
  from .readers import IoniTOFReader
8
9
 
9
10
  __all__ = ['Measurement', 'PreparingMeasurement', 'RunningMeasurement', 'FinishedMeasurement']
@@ -145,16 +146,14 @@ class FinishedMeasurement(Measurement):
145
146
  def timebin_width_ps(self):
146
147
  return next(iter(self.sourcefiles)).timebin_width_ps
147
148
 
148
- def __init__(self, filenames, _reader=IoniTOFReader):
149
- if isinstance(filenames, str):
150
- filenames = glob(filenames)
149
+ def __init__(self, *filenames, _reader=IoniTOFReader):
151
150
  if not len(filenames):
152
- raise ValueError("file not found or empty glob expression")
151
+ raise ValueError("no filename given")
153
152
 
154
153
  self.sourcefiles = sorted((_reader(f) for f in filenames), key=attrgetter('time_of_file'))
155
154
  self._check(self.sourcefiles)
156
155
 
157
- def iter_traces(self, kind='raw', index='abs_cycle', force_original=False):
156
+ def read_traces(self, kind='conc', index='abs_cycle', force_original=False):
158
157
  """Return the timeseries ("traces") of all masses, compounds and settings.
159
158
 
160
159
  'kind' is the type of traces and must be one of 'raw', 'concentration' or
@@ -164,8 +163,11 @@ class FinishedMeasurement(Measurement):
164
163
  'abs_time' or 'rel_time'.
165
164
 
166
165
  """
167
- return chain.from_iterable(sf.get_all(kind, index, force_original) for sf in self.sourcefiles)
166
+ return pd.concat(sf.read_all(kind, index, force_original) for sf in self.sourcefiles)
167
+
168
+ def __iter__(self):
169
+ return iter(self.sourcefiles)
168
170
 
169
171
  def __len__(self):
170
- return sum(len(sf) for sf in self.sourcefiles)
172
+ return len(self.sourcefiles)
171
173
 
@@ -70,7 +70,7 @@ class IoniTOFReader:
70
70
  # well it didn't work..
71
71
  pass
72
72
  finally:
73
- self.hf = h5py.File(path, 'r', swmr=True)
73
+ self.hf = h5py.File(path, 'r', swmr=False)
74
74
 
75
75
  @property
76
76
  def number_of_timebins(self):
@@ -97,7 +97,7 @@ class IoniTOFReader:
97
97
  return float(self.hf.attrs.get('Single Spec Duration (ms)'))
98
98
 
99
99
  def __init__(self, path):
100
- self.hf = h5py.File(path, 'r', swmr=True)
100
+ self.hf = h5py.File(path, 'r', swmr=False)
101
101
  self.filename = os.path.abspath(self.hf.filename)
102
102
 
103
103
  table_locs = {
@@ -439,14 +439,14 @@ class IoniTOFReader:
439
439
  raise ValueError(msg) from exc
440
440
 
441
441
  try:
442
- data = self._read_datainfo(tracedata, prefix=prefix) # may raise KeyError
443
- pt = self._read_datainfo(tracedata, prefix='PeakTable') # may raise KeyError
444
- labels = [b.decode('latin1') for b in pt['label']]
442
+ data = self._read_datainfo(tracedata, prefix=prefix)
443
+ pt = self._read_datainfo(tracedata, prefix='PeakTable')
445
444
  except KeyError as exc:
446
445
  raise KeyError(f'unknown group {exc}. filetype is not supported yet.') from exc
447
446
 
447
+ labels = [b.decode('latin1') for b in pt['label']]
448
448
  mapper = dict(zip(data.columns, labels))
449
- data.rename(columns=mapper)
449
+ data.rename(columns=mapper, inplace=True)
450
450
  data.index = list(self.iter_index(index))
451
451
 
452
452
  return data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrms
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS).
5
5
  License: GPL-2.0
6
6
  Author: Moritz Koenemann
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: h5py (>=3.12.1,<4.0.0)
15
15
  Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
16
- Requires-Dist: paho-mqtt (>=1.6.1,<1.7.0)
16
+ Requires-Dist: paho-mqtt (>=1.6.1,<3.0)
17
17
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
18
18
  Requires-Dist: pyModbusTCP (>=0.1.9)
19
19
  Requires-Dist: requests (>=2.32.3,<3.0.0)
@@ -1,27 +1,27 @@
1
- pytrms/__init__.py,sha256=bEgQmBOD5ftYjdwWCsZALJzhodK8CsOxI24V8Dm9Vcg,925
1
+ pytrms/__init__.py,sha256=e7CfZbs2LclwfPeL74K17jqM4xgnc6q4xLsJ9meXlgw,973
2
2
  pytrms/_base/__init__.py,sha256=GBALqAy1kUPMc0CWnWRmn_Cg_HGKGCeE-V2rdZEmN8A,836
3
3
  pytrms/_base/ioniclient.py,sha256=f4xWW3PL0p1gP7pczBaDEva3WUUb_J73n7yTIY5pW4s,826
4
- pytrms/_base/mqttclient.py,sha256=iua-AK7S_3rH1hsuLepqIjf7ivF5Ihb6T9OJNafJXXE,4233
4
+ pytrms/_base/mqttclient.py,sha256=taO-pHfsqTDTiTV306ehY6G5fpq86RA5YaJZrRrHbsc,5157
5
5
  pytrms/_version.py,sha256=fXD69jGmDocNcc-uipbNIhjWxC7EuyhPUQ6BrBvK0Wg,854
6
- pytrms/clients/__init__.py,sha256=RejfXqq7zSKLMZbUHdxtBvLiyeshLX-JQzgthmpYeMw,1404
7
- pytrms/clients/db_api.py,sha256=wEEhPUsdbqSH8TsYRICeYxAfBzdyNekM-rYe4xlCXkM,7248
6
+ pytrms/clients/__init__.py,sha256=7q-mRjyf0xH01ohU1pPVvMgm2Gj4Q0eWf3BT50CewhM,1415
7
+ pytrms/clients/db_api.py,sha256=pfMmfOlDCRmPCpsawwf-Rh4rix2sp3gHTF-afhXXMAQ,7556
8
8
  pytrms/clients/ioniclient.py,sha256=pHGzaNtKuVrjZ2O2nTmZYJObNMDkxNwLcNerlFit1vA,2477
9
9
  pytrms/clients/modbus.py,sha256=BbluHm8iYQY4V2Wbxd4eSiWJtIORyxRWRjhJo1_1JzQ,20585
10
- pytrms/clients/mqtt.py,sha256=H8uRBGcB4j4MaIlN7Hk_DcRnINJ7hxSxSA6sw4YJKAM,31041
11
- pytrms/clients/ssevent.py,sha256=umNeWMEYS65qLjmD9zawHTqzatnMDphFDALaxOgCKao,2731
10
+ pytrms/clients/mqtt.py,sha256=BJ4CESjR4DTrggL3gWCr54c9Xm1_SSZ_FCKWduPPgEk,31217
11
+ pytrms/clients/ssevent.py,sha256=zYS7MM4pkplC8wAj9ZY2Cb3fG3e-HtkyLt1qOosx-bk,3028
12
12
  pytrms/compose/__init__.py,sha256=gRkwGezTsuiMLA4jr5hhQsY7XX_FonBeWcvDfDuMFnY,30
13
13
  pytrms/compose/composition.py,sha256=hlV8g6n6HaLLLKileSh7sk8EtwPvaIQjOFXCEKLDKJ0,12161
14
14
  pytrms/data/IoniTofPrefs.ini,sha256=e1nU7Hyuh0efpgfN-G5-ERAFC6ZeehUiotsM4VtZIT0,1999
15
15
  pytrms/data/ParaIDs.csv,sha256=eWQxmHFfeTSxFfMcFpqloiZWDyK4VLZaV9zCnzLHNYs,27996
16
16
  pytrms/helpers.py,sha256=QfJgdQ6b0RcyiAuNHbDGGfuRfeZIs20dPF5flZVR12o,4979
17
- pytrms/instrument.py,sha256=OIFTbS6fuhos6oYMsrA_qdgvs_7T-H-sOSl1a0qpxZ8,3977
18
- pytrms/measurement.py,sha256=RqACqedT0JQDkv5cmKxcXVSgl1n0Fbp1mQj81aOsZkg,5507
17
+ pytrms/instrument.py,sha256=BVQLFac5466PgppipmaBDInakau6lxztijQgQueDz1U,4289
18
+ pytrms/measurement.py,sha256=yjTAskKKwXPzkW3I-Y4GawJxmJpLNxA7GQB8j1RDb8g,5445
19
19
  pytrms/peaktable.py,sha256=Ms0_dyHg8e6Oc8mwSTk8rD70EPjosX92pDXstf4f6Tk,17572
20
20
  pytrms/plotting/__init__.py,sha256=vp5mAa3npo4kP5wvXXNDxKnryFK693P4PSwC-BxEUH8,66
21
21
  pytrms/plotting/plotting.py,sha256=t7RXUhjOwXchtYXVgb0rJPD_9aVMDCT6DCAGu8TZQfE,702
22
22
  pytrms/readers/__init__.py,sha256=F1ZX4Jv7NcY8nuSWnIbwjYnNFC2JsTnEp0BnfBHUMSM,76
23
- pytrms/readers/ionitof_reader.py,sha256=zWKIdkeueRMDMxknUGOu68kUrPJz0rjVu4D_aoZUQQ4,18128
24
- pytrms-0.9.1.dist-info/LICENSE,sha256=GJsa-V1mEVHgVM6hDJGz11Tk3k0_7PsHTB-ylHb3Fns,18431
25
- pytrms-0.9.1.dist-info/METADATA,sha256=hmNvBCwFj49gPdStWhw5DrWneupksW5DQuPsX64on7Q,763
26
- pytrms-0.9.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
27
- pytrms-0.9.1.dist-info/RECORD,,
23
+ pytrms/readers/ionitof_reader.py,sha256=9O_enLxLYWgwRlpjZdK8wcX5rMCzt2B6PkgHJM9bPzw,18096
24
+ pytrms-0.9.2.dist-info/LICENSE,sha256=GJsa-V1mEVHgVM6hDJGz11Tk3k0_7PsHTB-ylHb3Fns,18431
25
+ pytrms-0.9.2.dist-info/METADATA,sha256=tPXvyFroNSiZAKAFwA4Krsd_I2o7BGvA1pH7iouqszM,761
26
+ pytrms-0.9.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
27
+ pytrms-0.9.2.dist-info/RECORD,,
File without changes