pytrms 0.9.9__py3-none-any.whl → 0.9.10__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.9'
1
+ _version = '0.9.10'
2
2
 
3
3
  import logging
4
4
  from functools import wraps
@@ -33,6 +33,30 @@ def _on_publish(client, self, mid):
33
33
  log.debug(f"[{self}] published {mid = }")
34
34
 
35
35
 
36
+ def _exception_safe(callback_fun):
37
+ # rest assured, that we never throw inside callbacks!
38
+
39
+ cb_name = callback_fun.__code__.co_name
40
+ cb_argc = callback_fun.__code__.co_argcount
41
+ short_payload = lambda s: s[:50] + ('...' if len(s) > 50 else '')
42
+
43
+ assert cb_argc == 3, "subscriber callback must have arguments (client, obj, msg)"
44
+
45
+ def exception_safe_callback_wrapper(client, data, msg):
46
+ try:
47
+ callback_fun(client, data, msg)
48
+ except Exception as exc:
49
+ log.warning(f"unhandled {exc.__class__.__name__}: {exc} "
50
+ + f"in callback {cb_name}({msg.topic}, <obj>, {short_payload(msg.payload)})")
51
+ pass
52
+ except:
53
+ log.warning(f"exception unhandled "
54
+ + f"in callback {cb_name}({msg.topic}, <obj>, {short_payload(msg.payload)})")
55
+ pass
56
+
57
+ return exception_safe_callback_wrapper
58
+
59
+
36
60
  class MqttClientBase:
37
61
  """Mix-in class that supplies basic MQTT-callback functions.
38
62
 
@@ -88,7 +112,7 @@ class MqttClientBase:
88
112
  self._subscriber_functions = list(subscriber_functions)
89
113
  for subscriber in self._subscriber_functions:
90
114
  for topic in getattr(subscriber, "topics", []):
91
- self.client.message_callback_add(topic, subscriber)
115
+ self.client.message_callback_add(topic, _exception_safe(subscriber))
92
116
  # ...pass this instance to each callback...
93
117
  self.client.user_data_set(self)
94
118
  # ...and connect to the server:
pytrms/clients/db_api.py CHANGED
@@ -67,22 +67,17 @@ class IoniConnect(_IoniClientBase):
67
67
  self.session = requests.sessions.Session()
68
68
  self.session.mount('http://', self._http_adapter)
69
69
  self.session.mount('https://', self._http_adapter)
70
- started_at = time.monotonic()
71
- while timeout_s is None or time.monotonic() < started_at + timeout_s:
72
- try:
73
- self.current_meas_loc = self.get_location("/api/measurements/current")
74
- break
75
- except requests.exceptions.HTTPError:
70
+ try:
71
+ self.current_meas_loc = self.get_location("/api/measurements/current")
72
+ except requests.exceptions.HTTPError as e:
73
+ if e.response.status_code == 410: # Gone
76
74
  # OK, no measurement running..
77
75
  self.current_meas_loc = ''
78
- break
79
- except Exception:
80
- pass
81
-
82
- time.sleep(10e-1)
83
- else:
76
+ return
77
+ except requests.exceptions.ConnectionError as e:
84
78
  self.session = self.current_meas_loc = None
85
- raise TimeoutError(f"no connection to '{self.url}'");
79
+ log.error(type(e).__name__, str(e))
80
+ raise
86
81
 
87
82
  def disconnect(self):
88
83
  if self.session is not None:
@@ -124,7 +119,7 @@ class IoniConnect(_IoniClientBase):
124
119
  self.current_meas_loc = None
125
120
  try:
126
121
  self.connect(timeout_s=3.3)
127
- except TimeoutError:
122
+ except requests.exceptions.ConnectionError:
128
123
  log.warning("no connection! make sure the DB-API is running and try again")
129
124
 
130
125
  def get(self, endpoint, **kwargs):
@@ -402,6 +397,8 @@ class IoniConnect(_IoniClientBase):
402
397
  stream-implementation), unless the server sends a keep-alive at regular
403
398
  intervals (as every well-behaved server should be doing)!
404
399
  """
405
- yield from SSEventListener(event_re, host_url=self.url, endpoint="/api/events",
406
- session=self.session)
400
+ # Note: DO NOT inject our `requests.session` with the 'session' kw-arg!!
401
+ # For some unknown reason this didn't work. Maybe in combination with
402
+ # the new _http_adapter? Who knows.. let the listener use its own session:
403
+ yield from SSEventListener(event_re, host_url=self.url, endpoint="/api/events")
407
404
 
pytrms/clients/mqtt.py CHANGED
@@ -376,12 +376,6 @@ def follow_act_set_values(client, self, msg):
376
376
  self.act_values[parID] = _parse_data_element(payload["DataElement"])
377
377
  if kind == "Set":
378
378
  self.set_values[parID] = _parse_data_element(payload["DataElement"])
379
- except json.decoder.JSONDecodeError as exc:
380
- log.error(f"{exc.__class__.__name__}: {exc} :: while processing [{msg.topic}] ({msg.payload})")
381
- raise
382
- except KeyError as exc:
383
- log.error(f"{exc.__class__.__name__}: {exc} :: while processing [{msg.topic}] ({msg.payload})")
384
- pass
385
379
  except ParsingError as exc:
386
380
  log.error(f"while parsing [{parID}] :: {str(exc)}")
387
381
  pass
pytrms/clients/ssevent.py CHANGED
@@ -106,6 +106,6 @@ class SSEventListener(Iterable):
106
106
 
107
107
  def __iter__(self):
108
108
  g = self.follow_events(timeout_s=None, prime=True)
109
- assert next(g).event == 'new connection', "invalid program: pseude-event expected"
109
+ assert next(g).event == 'new connection', "invalid program: pseudo-event expected"
110
110
  yield from g
111
111
 
pytrms/measurement.py CHANGED
@@ -218,6 +218,9 @@ class FinishedMeasurement(Measurement):
218
218
  """
219
219
  return pd.concat(sf.read_all(kind, index, force_original) for sf in self._readers)
220
220
 
221
+ get_traces = read_traces
222
+ get_traces.__doc__ = read_traces.__doc__ + "\nAlias: 'get_traces'."
223
+
221
224
  def __iter__(self):
222
225
  for reader in self._readers:
223
226
  for specdata in reader.iter_specdata():
pytrms/peaktable.py CHANGED
@@ -114,10 +114,16 @@ class Peak:
114
114
  label = 'm{:.4f}'.format(self.center)
115
115
  self.label = str(label)
116
116
  self.formula = formula
117
- if parent is not None and not isinstance(parent, Peak):
118
- raise ValueError(parent)
117
+ # check parent and use its borders:
118
+ if parent is not None:
119
+ if not isinstance(parent, Peak):
120
+ raise ValueError(parent)
121
+
122
+ borders = parent.borders
123
+ self.parent = parent
124
+ else:
125
+ self.parent = None
119
126
 
120
- self.parent = parent
121
127
  self._borders = tuple(map(lambda x: round(float(x), ndigits=4), borders))
122
128
  self.isotopic_abundance = float(isotopic_abundance)
123
129
  self.k_rate = float(k_rate)
@@ -194,7 +200,7 @@ class PeakTable:
194
200
  peaks = []
195
201
  for row in table.itertuples(index=False):
196
202
  borders = row.BorderLow, row.BorderHigh
197
- peaks.append(Peak(center=row.MassCenters, label=row.Descriptions,
203
+ peaks.append(Peak(center=row.MassCenters, label=row.Descriptions.strip(),
198
204
  borders=borders, k_rate=row.kRates,
199
205
  multiplier=row.Multipliers))
200
206
 
@@ -327,6 +333,46 @@ class PeakTable:
327
333
  indent=2)
328
334
  fp.write(s)
329
335
 
336
+ def _write_ionipt(self, fp, fileversion='1.0'):
337
+ # modes:
338
+ IGNORE = 0b00
339
+ INTEGRATE = 0b01
340
+ FIT_PEAKS = 0b10
341
+ BOTH = 0b11
342
+
343
+ # Note (Warning): We can only deduce BOTH and INTEGRATE, because all parents
344
+ # with children are always fitted in our context. The IGNORE and FIT_PEAKS
345
+ # modes will be lost when saving in .ionipt format!
346
+
347
+ render_borderpeak = lambda bp: {
348
+ "name": bp.label,
349
+ "center": bp.center,
350
+ "ion": "",
351
+ "ionic_isotope": bp.formula,
352
+ "parent": "", # not used!
353
+ "isotopic_abundance": bp.isotopic_abundance,
354
+ "k_rate": bp.k_rate,
355
+ "multiplier": bp.multiplier,
356
+ "resolution": bp.resolution
357
+ }
358
+ parent2children = self.group()
359
+ s = json.dumps([
360
+ {
361
+ "border_peak": render_borderpeak(parent),
362
+ "low": parent.borders[0],
363
+ "high": parent.borders[1],
364
+ "peak": [
365
+ render_borderpeak(child)
366
+ for child in parent2children[parent]
367
+ ],
368
+ "mode": int(BOTH if len(parent2children[parent]) else INTEGRATE),
369
+ "shift": parent.shift,
370
+ }
371
+ for parent in self.nominal
372
+ ],
373
+ indent=4)
374
+ fp.write(s)
375
+
330
376
  def _write_ipt(self, fp, fileversion='1.0'):
331
377
  if fileversion not in ['1.0', '1.1']:
332
378
  raise NotImplementedError("Can't write .ipt version %s!" % fileversion)
@@ -426,6 +472,16 @@ class PeakTable:
426
472
 
427
473
  raise KeyError("No such peak at %s!" % str(exact_mass))
428
474
 
475
+ def find_by_label(self, label):
476
+ """Return the peak with the given `label`.
477
+
478
+ Raises KeyError if not found.
479
+ """
480
+ try:
481
+ return next((peak for peak in self.peaks if peak.label == label))
482
+ except StopIteration:
483
+ raise KeyError("No such peak at %s!" % str(label))
484
+
429
485
  def group(self):
430
486
  groups = defaultdict(list)
431
487
  for peak in self:
@@ -443,6 +499,8 @@ class PeakTable:
443
499
  writer = partial(self._write_ipt, fileversion='1.0')
444
500
  elif ext == '.ipta':
445
501
  writer = partial(self._write_ipta, fileversion='1.0')
502
+ elif ext == '.ionipt':
503
+ writer = partial(self._write_ionipt, fileversion='1.0')
446
504
  else:
447
505
  raise NotImplementedError("can't export with file extension <%s>!" % ext)
448
506
 
@@ -206,13 +206,13 @@ class IoniTOFReader:
206
206
  except KeyError as exc:
207
207
  msg = "Unknown index-type! `kind` must be one of {0}.".format(', '.join(lut.keys()))
208
208
  raise KeyError(msg) from exc
209
-
209
+
210
210
  return convert2iterator(self.hf['SPECdata/Times'][:, _N])
211
211
 
212
212
  @lru_cache
213
213
  def make_index(self, kind='abs_cycle'):
214
214
  return pd.Index(self.iter_index(kind))
215
-
215
+
216
216
  def __len__(self):
217
217
  return self.hf['SPECdata/Intensities'].shape[0]
218
218
 
@@ -249,11 +249,11 @@ class IoniTOFReader:
249
249
  self.hf.visit(lambda obj_name: obj_names.add(obj_name))
250
250
 
251
251
  return sorted(obj_names)
252
-
252
+
253
253
  def list_addtrace_groups(self):
254
254
  """Lists the recorded additional trace-groups."""
255
255
  return sorted(self._locate_datainfo())
256
-
256
+
257
257
  def __repr__(self):
258
258
  return "<%s (%s) [no. %s] %s>" % (self.__class__.__name__,
259
259
  self.inst_type, self.serial_nr, self.hf.filename)
@@ -263,7 +263,7 @@ class IoniTOFReader:
263
263
  """Lookup groups with data-info traces."""
264
264
  dataloc = set()
265
265
  infoloc = set()
266
-
266
+
267
267
  def func(object_name):
268
268
  nonlocal dataloc
269
269
  nonlocal infoloc
@@ -272,13 +272,13 @@ class IoniTOFReader:
272
272
  if object_name.endswith('/Info'):
273
273
  infoloc |= {object_name[:-5], }
274
274
  return None
275
-
275
+
276
276
  # use the above 'visit'-function that appends matched sections...
277
277
  self.hf.visit(func)
278
-
278
+
279
279
  # ...and return only groups with both /Data and /Info datasets:
280
280
  return dataloc.intersection(infoloc)
281
-
281
+
282
282
  def traces(self):
283
283
  """Returns a 'pandas.DataFrame' with all traces concatenated."""
284
284
  return self.read_all(kind='conc', index='abs_cycle', force_original=False)
@@ -292,7 +292,7 @@ class IoniTOFReader:
292
292
  # |__ wird dann bei bedarf ge-populated
293
293
  #
294
294
  # das sourcefile / measurement soll sich wie ein pd.DataFrame "anfuehlen":
295
-
295
+
296
296
  # das loest das Problem, aus einer "Matrix2 gezielt eine Zeile oder eine "Spalte"
297
297
  # oder alles (d.h. iterieren ueber Zeilen) zu selektieren und zwar intuitiv!!
298
298
 
@@ -304,7 +304,7 @@ class IoniTOFReader:
304
304
  # 3. _column_getter
305
305
  # 4. _row_getter
306
306
  # 5. parID_resolver ~> keys() aus ParID.txt zu addtrace-group + column!
307
-
307
+
308
308
  ###################################################################################
309
309
  # #
310
310
  # ENDZIEL: times u. Automation fuer die "letzte" Zeile an die Datenbank schicken! #
@@ -341,7 +341,7 @@ class IoniTOFReader:
341
341
  dset_name, column = lut[key] # may raise KeyError
342
342
 
343
343
  return self.hf[dset_name][:,column]
344
-
344
+
345
345
  def loc(self, label):
346
346
  if isinstance(label, int):
347
347
  return self.iloc[self.make_index('abs_cycle')[label]]
@@ -374,7 +374,7 @@ class IoniTOFReader:
374
374
 
375
375
  if hasattr(labels[0], 'decode'):
376
376
  labels = [b.decode('latin1') for b in labels]
377
-
377
+
378
378
  # TODO :: wir haben hier diese doesigen Set/Act werte drin, was wollen wir??
379
379
  # if keys[0].endswith('[Set]'):
380
380
  # rv = {key[:-5]: (value, unit)
@@ -414,10 +414,10 @@ class IoniTOFReader:
414
414
  # act_values.columns = [col.replace('_Act', '') for col in act_values.columns]
415
415
  #
416
416
  # return _trace(set_values, act_values)
417
-
418
-
417
+
418
+
419
419
  return pd.DataFrame(data, columns=labels)
420
-
420
+
421
421
  def _read_processed_traces(self, kind, index):
422
422
  # error conditions:
423
423
  # 1) 'kind' is not recognized -> ValueError
@@ -444,11 +444,12 @@ class IoniTOFReader:
444
444
  except KeyError as exc:
445
445
  raise KeyError(f'unknown group {exc}. filetype is not supported yet.') from exc
446
446
 
447
- labels = [b.decode('latin1') for b in pt['label']]
448
- mapper = dict(zip(data.columns, labels))
449
- data.rename(columns=mapper, inplace=True)
447
+ # Note [#3433]: the .h5 'ConcentrationsInfo' is what *should* label the
448
+ # columns, but it is ambiguous, because it rounds to 3 decimal places
449
+ # (for whatever reason...)! Just overwrite it with the 'PeakTableInfo':
450
+ data.columns = [b.decode('latin1') for b in pt['label']]
450
451
  data.index = list(self.iter_index(index))
451
-
452
+
452
453
  return data
453
454
 
454
455
  def _read_original_traces(self, kind, index):
@@ -464,9 +465,9 @@ class IoniTOFReader:
464
465
  except KeyError as exc:
465
466
  msg = ("Unknown trace-type! `kind` must be one of 'raw', 'corrected' or 'concentration'.")
466
467
  raise ValueError(msg) from exc
467
-
468
+
468
469
  info = self.hf['TRACEdata/TraceInfo']
469
470
  labels = [b.decode('latin1') for b in info[1,:]]
470
-
471
+
471
472
  return pd.DataFrame(data, columns=labels, index=list(self.iter_index(index)))
472
473
 
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pytrms
3
- Version: 0.9.9
3
+ Version: 0.9.10
4
4
  Summary: Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS).
5
5
  License: GPL-2.0
6
+ License-File: LICENSE
6
7
  Author: Moritz Koenemann
7
8
  Author-email: moritz.koenemann@ionicon.com
8
9
  Requires-Python: >=3.10,<4.0
@@ -12,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
15
17
  Requires-Dist: h5py (>=3.12.1,<4.0.0)
16
18
  Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
17
19
  Requires-Dist: paho-mqtt (>=1.6.1,<3.0)
@@ -1,28 +1,28 @@
1
- pytrms/__init__.py,sha256=ESUOj8liG6hH2yqeGKgeqjSy2AmNEVMM5qqaucushsM,2821
1
+ pytrms/__init__.py,sha256=FbnSXVO5QUj3oEJiB_d7pwviEGkWpT99zc-bt_ujCiM,2822
2
2
  pytrms/_base/__init__.py,sha256=2KUO7R_SPZxsV0Xe_Hy4e2HnQI5zDzOYJc4H7G3ZQpE,850
3
3
  pytrms/_base/ioniclient.py,sha256=eTkksDwB8u9i-rnwCGH7o8rXcNbez-QakXLqQFJJCw0,1368
4
- pytrms/_base/mqttclient.py,sha256=DxETIjK_qIvxCGiHLzHXdvyGcCgiwYfggWlXrxzwpqY,5347
4
+ pytrms/_base/mqttclient.py,sha256=VlibD2jN3AfRR_z0OIxalCSo9xNZbWtR94B2czuFSzk,6272
5
5
  pytrms/_version.py,sha256=yRCN1kvPaX0FHycK0NBHTluhkf5euj8U7WNTKdVQafg,828
6
6
  pytrms/clients/__init__.py,sha256=_IHR-GMeAemtrkeQR5I4lPvO9wVR0pEWuswgdi4Rw0I,263
7
- pytrms/clients/db_api.py,sha256=xmqqMWm8p3Xr7ZcS-Lt-jrUqSEq94_Dmb4wnDxjGaDU,17771
7
+ pytrms/clients/db_api.py,sha256=I-mOtBK4PZ4xY0UoKPwWWlMiJUnMG6nvFj40HjAvNW0,17871
8
8
  pytrms/clients/dummy.py,sha256=BaCAhR30eE-ULu8U0bE-7L_cTdAeWsW0Dn1C_cwiCjw,1147
9
9
  pytrms/clients/ioniclient.py,sha256=cp37XSoCzLamjDzcUCrKbp530YMstQ0Eoj81r5tClL8,2390
10
10
  pytrms/clients/modbus.py,sha256=Mgxps17amE033P6cCJ5Bv4r8vdWZC2SiyxqfQ7KfEhA,26219
11
- pytrms/clients/mqtt.py,sha256=ybtRbKeRfxz2bPOgOtlJDAkCP79fWEu47z7pliZHAKQ,31809
12
- pytrms/clients/ssevent.py,sha256=zj5LPkIbGm3APOJuol9GPc_A_zsas6gjY-w9sM-LQwo,4354
11
+ pytrms/clients/mqtt.py,sha256=K5HOcmjXCUuv4zS_j6UIMRHSkWuj8BMo9HmPyPQe8nI,31498
12
+ pytrms/clients/ssevent.py,sha256=WqRtugrHlCj9K6_HnWnxmvumJzNDHVG3IpK-06zJhHY,4354
13
13
  pytrms/compose/__init__.py,sha256=hbjX-rzFlBnZpp7OP8edyZAw0r2-im1MtWxQNU8tG_A,28
14
14
  pytrms/compose/composition.py,sha256=VU0E50zIdqS4cIzTdMMDxWWCUd5jkLbuPcNAET93oqo,11779
15
15
  pytrms/data/IoniTofPrefs.ini,sha256=BGyTijnmtdGqa-kGbAuB1RysG4MPK9nlF9TR069tKwE,1887
16
16
  pytrms/data/ParaIDs.csv,sha256=CWOXx83OTmHRgsl39Si35DwND94UnvhdIrkekrnbpNg,27401
17
17
  pytrms/helpers.py,sha256=0GhSlX4zwMIQqNx2-G7WrLecoYIG7MVFZb5doODaenA,4985
18
18
  pytrms/instrument.py,sha256=x0lBK5O9BBLNdRAZMnc4DGsQSCQe0Lu-WH5pgNGkoDI,5824
19
- pytrms/measurement.py,sha256=iHsEWmJhCSFxMON_N-5mtCmFqigBqGQM4AImqE7pS44,7629
20
- pytrms/peaktable.py,sha256=w76wPgVARQH8bOppDejpvVHjSDY88ZZYkGBMmgRFmTM,17195
19
+ pytrms/measurement.py,sha256=I_IdhG9LKlHehp4l2pYOv57MNL1KPQMVqFNYV7icf3A,7731
20
+ pytrms/peaktable.py,sha256=AebDqAO1RQRFJPp3dNSbzLxazNk0_tBADUPu6gIcY5c,19249
21
21
  pytrms/plotting/__init__.py,sha256=sfL4k2PeBmzIf-5Z_2rnkeM8As3psbPxaVvmpi1T48Q,62
22
22
  pytrms/plotting/plotting.py,sha256=WzP4Is2PSu_NdhY73HsCU8iEHmLgnIgNY-opsxwIjH8,675
23
23
  pytrms/readers/__init__.py,sha256=2r9dXwPRqYkIGpM0EjY-m_Ti3qtxE0jeC9FnudDDoXc,72
24
- pytrms/readers/ionitof_reader.py,sha256=c8W5aglrY1bmuljnaAqx6TGWAPGXXMkuHjJ1aRw3Wrc,17624
25
- pytrms-0.9.9.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
26
- pytrms-0.9.9.dist-info/METADATA,sha256=mCiLqsfAAJCVcXo4oBsLtMY3_arH_nouleCUkEgFGh8,812
27
- pytrms-0.9.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
- pytrms-0.9.9.dist-info/RECORD,,
24
+ pytrms/readers/ionitof_reader.py,sha256=ShpMk90gwX-1fOvC12iJ8ZQaLWJNJvGBvgDIwyy4ZTk,17691
25
+ pytrms-0.9.10.dist-info/METADATA,sha256=aAmjGaXSAUgXBuWJ0mlYvM-QDZmmncfVPb6kRb5QDa0,886
26
+ pytrms-0.9.10.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
27
+ pytrms-0.9.10.dist-info/licenses/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
28
+ pytrms-0.9.10.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any