pytrms 0.2.2__tar.gz → 0.9.1__tar.gz
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-0.9.1/PKG-INFO +19 -0
- {pytrms-0.2.2 → pytrms-0.9.1}/pyproject.toml +15 -10
- pytrms-0.9.1/pytrms/__init__.py +36 -0
- pytrms-0.9.1/pytrms/_base/__init__.py +24 -0
- pytrms-0.9.1/pytrms/_base/ioniclient.py +32 -0
- pytrms-0.9.1/pytrms/_base/mqttclient.py +106 -0
- pytrms-0.9.1/pytrms/clients/__init__.py +33 -0
- pytrms-0.9.1/pytrms/clients/db_api.py +178 -0
- {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/clients/ioniclient.py +5 -21
- pytrms-0.9.1/pytrms/clients/modbus.py +528 -0
- pytrms-0.9.1/pytrms/clients/mqtt.py +793 -0
- pytrms-0.9.1/pytrms/clients/ssevent.py +77 -0
- pytrms-0.9.1/pytrms/compose/__init__.py +2 -0
- pytrms-0.9.1/pytrms/compose/composition.py +302 -0
- pytrms-0.9.1/pytrms/data/ParaIDs.csv +731 -0
- pytrms-0.9.1/pytrms/helpers.py +120 -0
- pytrms-0.9.1/pytrms/instrument.py +114 -0
- pytrms-0.9.1/pytrms/measurement.py +171 -0
- pytrms-0.9.1/pytrms/peaktable.py +501 -0
- pytrms-0.9.1/pytrms/plotting/__init__.py +4 -0
- pytrms-0.9.1/pytrms/readers/__init__.py +4 -0
- pytrms-0.9.1/pytrms/readers/ionitof_reader.py +472 -0
- pytrms-0.2.2/PKG-INFO +0 -17
- pytrms-0.2.2/par_ID_list.txt +0 -653
- pytrms-0.2.2/pytrms/__init__.py +0 -42
- pytrms-0.2.2/pytrms/clients/__init__.py +0 -7
- pytrms-0.2.2/pytrms/clients/mockclient.py +0 -612
- pytrms-0.2.2/pytrms/factory.py +0 -35
- pytrms-0.2.2/pytrms/helpers.py +0 -17
- pytrms-0.2.2/pytrms/instrument.py +0 -178
- pytrms-0.2.2/pytrms/measurement.py +0 -90
- pytrms-0.2.2/pytrms/modbus.py +0 -126
- pytrms-0.2.2/pytrms/reader.py +0 -177
- pytrms-0.2.2/pytrms/testing/__init__.py +0 -14
- pytrms-0.2.2/pytrms/testing/response_TRACES_webAPI.txt +0 -556
- pytrms-0.2.2/pytrms/tracebuffer.py +0 -108
- pytrms-0.2.2/setup.py +0 -33
- {pytrms-0.2.2 → pytrms-0.9.1}/LICENSE +0 -0
- {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/_version.py +0 -0
- {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/data/IoniTofPrefs.ini +0 -0
- {pytrms-0.2.2/pytrms → pytrms-0.9.1/pytrms/plotting}/plotting.py +0 -0
pytrms-0.9.1/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytrms
|
|
3
|
+
Version: 0.9.1
|
|
4
|
+
Summary: Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS).
|
|
5
|
+
License: GPL-2.0
|
|
6
|
+
Author: Moritz Koenemann
|
|
7
|
+
Author-email: moritz.koenemann@ionicon.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: h5py (>=3.12.1,<4.0.0)
|
|
15
|
+
Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
|
|
16
|
+
Requires-Dist: paho-mqtt (>=1.6.1,<1.7.0)
|
|
17
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
18
|
+
Requires-Dist: pyModbusTCP (>=0.1.9)
|
|
19
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pytrms"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS)."
|
|
5
5
|
authors = ["Moritz Koenemann <moritz.koenemann@ionicon.com>"]
|
|
6
6
|
license = "GPL-2.0"
|
|
7
7
|
include = [
|
|
8
|
-
"
|
|
8
|
+
"pytrms/data/ParaIDs.csv",
|
|
9
9
|
"pytrms/data/IoniTofPrefs.ini",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
|
-
python = "^3.
|
|
14
|
-
h5py = "^3.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
python = "^3.10"
|
|
14
|
+
h5py = "^3.12.1"
|
|
15
|
+
matplotlib = "^3.9.2"
|
|
16
|
+
requests = "^2.32.3"
|
|
17
|
+
pandas = "^2.2.3"
|
|
18
|
+
pyModbusTCP = ">=0.1.9"
|
|
19
|
+
paho-mqtt = "~1.6.1"
|
|
20
|
+
|
|
21
|
+
[tool.poetry.group.test.dependencies]
|
|
22
|
+
pytest = "^8.3.0"
|
|
18
23
|
|
|
19
24
|
[tool.poetry.group.dev.dependencies]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Sphinx = "^5.3.0"
|
|
25
|
+
Sphinx = "^8.0.0"
|
|
26
|
+
ipykernel = "^6.29.0"
|
|
23
27
|
|
|
24
28
|
[build-system]
|
|
25
29
|
requires = ["poetry-core>=1.0.0"]
|
|
26
30
|
build-backend = "poetry.core.masonry.api"
|
|
31
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
_version = '0.9.1'
|
|
2
|
+
|
|
3
|
+
__all__ = ['load', 'connect']
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load(path):
|
|
7
|
+
'''Open a datafile for post-analysis or batch processing.
|
|
8
|
+
|
|
9
|
+
returns a `Measurement`.
|
|
10
|
+
'''
|
|
11
|
+
from .measurement import OfflineMeasurement
|
|
12
|
+
from .readers import IoniTOFReader
|
|
13
|
+
|
|
14
|
+
reader = IoniTOFReader(path)
|
|
15
|
+
|
|
16
|
+
return OfflineMeasurement(reader)
|
|
17
|
+
|
|
18
|
+
def connect(host=None, method='webapi'):
|
|
19
|
+
'''Connect a client to a running measurement server.
|
|
20
|
+
|
|
21
|
+
'method' is the preferred connection, either 'webapi' (default) or 'modbus'.
|
|
22
|
+
|
|
23
|
+
returns an `Instrument` if connected successfully.
|
|
24
|
+
'''
|
|
25
|
+
from .instrument import Instrument
|
|
26
|
+
|
|
27
|
+
if method.lower() == 'webapi':
|
|
28
|
+
from .clients.ioniclient import IoniClient
|
|
29
|
+
return IoniClient(host)
|
|
30
|
+
|
|
31
|
+
if method.lower() == 'modbus':
|
|
32
|
+
from .modbus import IoniconModbus
|
|
33
|
+
return IoniconModbus(host)
|
|
34
|
+
|
|
35
|
+
raise NotImplementedError(str(method))
|
|
36
|
+
|
|
@@ -0,0 +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
|
+
|
|
@@ -0,0 +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
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
import paho.mqtt.client as mqtt
|
|
12
|
+
|
|
13
|
+
from .ioniclient import IoniClientBase
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
__all__ = ['MqttClientBase']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _on_connect(client, self, flags, rc):
|
|
21
|
+
# Note: ensure subscription after re-connecting,
|
|
22
|
+
# wildcards are '+' (one level), '#' (all levels):
|
|
23
|
+
default_QoS = 2
|
|
24
|
+
topics = set()
|
|
25
|
+
for subscriber in self._subscriber_functions:
|
|
26
|
+
topics.update(set(getattr(subscriber, "topics", [])))
|
|
27
|
+
subs = sorted(zip(topics, cycle([default_QoS])))
|
|
28
|
+
log.debug(f"[{self}] " + "\n --> ".join(["subscribing to"] + list(map(str, subs))))
|
|
29
|
+
rv = client.subscribe(subs)
|
|
30
|
+
log.info(f"[{self}] successfully connected with {rv = }")
|
|
31
|
+
|
|
32
|
+
def _on_subscribe(client, self, mid, granted_qos):
|
|
33
|
+
log.info(f"[{self}] successfully subscribed with {mid = } | {granted_qos = }")
|
|
34
|
+
|
|
35
|
+
def _on_publish(client, self, mid):
|
|
36
|
+
log.debug(f"[{self}] published {mid = }")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MqttClientBase(IoniClientBase):
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def is_connected(self):
|
|
44
|
+
'''Returns `True` if connected to the server.
|
|
45
|
+
|
|
46
|
+
Note: this property will be polled on initialization and should
|
|
47
|
+
return `True` if a connection could be established!
|
|
48
|
+
'''
|
|
49
|
+
return (True
|
|
50
|
+
and self.client.is_connected())
|
|
51
|
+
|
|
52
|
+
def __init__(self, host, subscriber_functions,
|
|
53
|
+
on_connect, on_subscribe, on_publish,
|
|
54
|
+
connect_timeout_s=10):
|
|
55
|
+
super().__init__(host, port=1883)
|
|
56
|
+
# configure connection...
|
|
57
|
+
self.client = mqtt.Client(clean_session=True)
|
|
58
|
+
# clean_session is a boolean that determines the client type. If True,
|
|
59
|
+
# the broker will remove all information about this client when it
|
|
60
|
+
# disconnects. If False, the client is a persistent client and
|
|
61
|
+
# subscription information and queued messages will be retained when the
|
|
62
|
+
# client disconnects.
|
|
63
|
+
# The clean_session argument only applies to MQTT versions v3.1.1 and v3.1.
|
|
64
|
+
# It is not accepted if the MQTT version is v5.0 - use the clean_start
|
|
65
|
+
# argument on connect() instead.
|
|
66
|
+
self.client.on_connect = on_connect if on_connect is not None else _on_connect
|
|
67
|
+
self.client.on_subscribe = on_subscribe if on_subscribe is not None else _on_subscribe
|
|
68
|
+
self.client.on_publish = on_publish if on_publish is not None else _on_publish
|
|
69
|
+
# ...subscribe to topics...
|
|
70
|
+
self._subscriber_functions = list(subscriber_functions)
|
|
71
|
+
for subscriber in self._subscriber_functions:
|
|
72
|
+
for topic in getattr(subscriber, "topics", []):
|
|
73
|
+
self.client.message_callback_add(topic, subscriber)
|
|
74
|
+
# ...pass this instance to each callback...
|
|
75
|
+
self.client.user_data_set(self)
|
|
76
|
+
# ...and connect to the server:
|
|
77
|
+
try:
|
|
78
|
+
self.connect(connect_timeout_s)
|
|
79
|
+
except TimeoutError as exc:
|
|
80
|
+
log.warn(f"{exc} (retry connecting when the Instrument is set up)")
|
|
81
|
+
|
|
82
|
+
def connect(self, timeout_s=10):
|
|
83
|
+
log.info(f"[{self}] connecting to MQTT broker...")
|
|
84
|
+
self.client.connect(self.host, self.port, timeout_s)
|
|
85
|
+
self.client.loop_start() # runs in a background thread
|
|
86
|
+
started_at = time.monotonic()
|
|
87
|
+
while time.monotonic() < started_at + timeout_s:
|
|
88
|
+
if self.is_connected:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
time.sleep(10e-3)
|
|
92
|
+
else:
|
|
93
|
+
self.disconnect()
|
|
94
|
+
raise TimeoutError(f"[{self}] no connection to IoniTOF")
|
|
95
|
+
|
|
96
|
+
def publish_with_ack(self, *args, timeout_s=10, **kwargs):
|
|
97
|
+
# Note: this is important when publishing just before exiting the application
|
|
98
|
+
# to ensure that all messages get through (timeout_s is set on `.__init__()`)
|
|
99
|
+
msg = self.client.publish(*args, **kwargs)
|
|
100
|
+
msg.wait_for_publish(timeout=timeout_s)
|
|
101
|
+
return msg
|
|
102
|
+
|
|
103
|
+
def disconnect(self):
|
|
104
|
+
self.client.loop_stop()
|
|
105
|
+
self.client.disconnect()
|
|
106
|
+
|
|
@@ -0,0 +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 = 0 # overwrites logging.NOTSET
|
|
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
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from . import _logging
|
|
7
|
+
from .ssevent import SSEventListener
|
|
8
|
+
from .._base import IoniClientBase
|
|
9
|
+
|
|
10
|
+
log = _logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# TODO :: sowas waer auch ganz cool: die DBAPI bietes sich geradezu an,
|
|
13
|
+
# da mehr object-oriented zu arbeiten:
|
|
14
|
+
# currentVariable = get_component(currentComponentNameAction, ds)
|
|
15
|
+
# currentVariable.save_value({'value': currentValue})
|
|
16
|
+
|
|
17
|
+
class IoniConnect(IoniClientBase):
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def is_connected(self):
|
|
21
|
+
'''Returns `True` if connection to IoniTOF could be established.'''
|
|
22
|
+
try:
|
|
23
|
+
self.get("/api/status")
|
|
24
|
+
return True
|
|
25
|
+
except:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_running(self):
|
|
30
|
+
'''Returns `True` if IoniTOF is currently acquiring data.'''
|
|
31
|
+
raise NotImplementedError("is_running")
|
|
32
|
+
|
|
33
|
+
def connect(self, timeout_s):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def disconnect(self):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def __init__(self, host='127.0.0.1', port=5066, session=None):
|
|
40
|
+
super().__init__(host, port)
|
|
41
|
+
self.url = f"http://{self.host}:{self.port}"
|
|
42
|
+
if session is None:
|
|
43
|
+
session = requests.sessions.Session()
|
|
44
|
+
self.session = session
|
|
45
|
+
# ??
|
|
46
|
+
self.current_avg_endpoint = None
|
|
47
|
+
self.comp_dict = dict()
|
|
48
|
+
|
|
49
|
+
def get(self, endpoint, **kwargs):
|
|
50
|
+
return self._get_object(endpoint, **kwargs).json()
|
|
51
|
+
|
|
52
|
+
def post(self, endpoint, data, **kwargs):
|
|
53
|
+
return self._create_object(endpoint, data, 'post', **kwargs).headers.get('Location')
|
|
54
|
+
|
|
55
|
+
def put(self, endpoint, data, **kwargs):
|
|
56
|
+
return self._create_object(endpoint, data, 'put', **kwargs).headers.get('Location')
|
|
57
|
+
|
|
58
|
+
def upload(self, endpoint, filename):
|
|
59
|
+
if not endpoint.startswith('/'):
|
|
60
|
+
endpoint = '/' + endpoint
|
|
61
|
+
with open(filename) as f:
|
|
62
|
+
# Note (important!): this is a "form-data" entry, where the server
|
|
63
|
+
# expects the "name" to be 'file' and rejects it otherwise:
|
|
64
|
+
name = 'file'
|
|
65
|
+
r = self.session.post(self.url + endpoint, files=[(name, (filename, f, ''))])
|
|
66
|
+
r.raise_for_status()
|
|
67
|
+
|
|
68
|
+
return r
|
|
69
|
+
|
|
70
|
+
def _get_object(self, endpoint, **kwargs):
|
|
71
|
+
if not endpoint.startswith('/'):
|
|
72
|
+
endpoint = '/' + endpoint
|
|
73
|
+
if 'headers' not in kwargs:
|
|
74
|
+
kwargs['headers'] = {'content-type': 'application/hal+json'}
|
|
75
|
+
elif 'content-type' not in (k.lower() for k in kwargs['headers']):
|
|
76
|
+
kwargs['headers'].update({'content-type': 'application/hal+json'})
|
|
77
|
+
r = self.session.request('get', self.url + endpoint, **kwargs)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
|
|
80
|
+
return r
|
|
81
|
+
|
|
82
|
+
def _create_object(self, endpoint, data, method='post', **kwargs):
|
|
83
|
+
if not endpoint.startswith('/'):
|
|
84
|
+
endpoint = '/' + endpoint
|
|
85
|
+
if not isinstance(data, str):
|
|
86
|
+
data = json.dumps(data, ensure_ascii=False) # default is `True`, escapes Umlaute!
|
|
87
|
+
if 'headers' not in kwargs:
|
|
88
|
+
kwargs['headers'] = {'content-type': 'application/hal+json'}
|
|
89
|
+
elif 'content-type' not in (k.lower() for k in kwargs['headers']):
|
|
90
|
+
kwargs['headers'].update({'content-type': 'application/hal+json'})
|
|
91
|
+
r = self.session.request(method, self.url + endpoint, data=data, **kwargs)
|
|
92
|
+
if not r.ok:
|
|
93
|
+
log.error(f"POST {endpoint}\n{data}\n\nreturned [{r.status_code}]: {r.content}")
|
|
94
|
+
r.raise_for_status()
|
|
95
|
+
|
|
96
|
+
return r
|
|
97
|
+
|
|
98
|
+
def sync(self, peaktable):
|
|
99
|
+
"""Compare and upload any differences in `peaktable` to the database."""
|
|
100
|
+
from pytrms.peaktable import Peak, PeakTable
|
|
101
|
+
from operator import attrgetter
|
|
102
|
+
|
|
103
|
+
# Note: a `Peak` is a hashable object that serves as a key that
|
|
104
|
+
# distinguishes between peaks as defined by PyTRMS:
|
|
105
|
+
make_key = lambda peak: Peak(center=peak['center'], label=peak['name'], shift=peak['shift'])
|
|
106
|
+
|
|
107
|
+
if isinstance(peaktable, str):
|
|
108
|
+
log.info(f"loading peaktable '{peaktable}'...")
|
|
109
|
+
peaktable = PeakTable.from_file(peaktable)
|
|
110
|
+
|
|
111
|
+
# get the PyTRMS- and IoniConnect-peaks on the same page:
|
|
112
|
+
conv = {
|
|
113
|
+
'name': attrgetter('label'),
|
|
114
|
+
'center': attrgetter('center'),
|
|
115
|
+
'kRate': attrgetter('k_rate'),
|
|
116
|
+
'low': lambda p: p.borders[0],
|
|
117
|
+
'high': lambda p: p.borders[1],
|
|
118
|
+
'shift': attrgetter('shift'),
|
|
119
|
+
'multiplier': attrgetter('multiplier'),
|
|
120
|
+
}
|
|
121
|
+
# normalize the input argument and create a hashable set:
|
|
122
|
+
updates = dict()
|
|
123
|
+
for peak in peaktable:
|
|
124
|
+
payload = {k: conv[k](peak) for k in conv}
|
|
125
|
+
updates[make_key(payload)] = {'payload': payload}
|
|
126
|
+
|
|
127
|
+
log.info(f"fetching current peaktable from the server...")
|
|
128
|
+
# create a comparable collection of peaks already on the database by
|
|
129
|
+
# reducing the keys in the response to what we actually want to update:
|
|
130
|
+
db_peaks = {make_key(p): {
|
|
131
|
+
'payload': {k: p[k] for k in conv.keys()},
|
|
132
|
+
'self': p['_links']['self'],
|
|
133
|
+
'parent': p['_links'].get('parent'),
|
|
134
|
+
} for p in self.get('/api/peaks')['_embedded']['peaks']}
|
|
135
|
+
|
|
136
|
+
to_update = updates.keys() & db_peaks.keys()
|
|
137
|
+
to_upload = updates.keys() - db_peaks.keys()
|
|
138
|
+
updated = 0
|
|
139
|
+
for key in sorted(to_update):
|
|
140
|
+
# check if an existing peak needs an update
|
|
141
|
+
if db_peaks[key]['payload'] == updates[key]['payload']:
|
|
142
|
+
# nothing to do..
|
|
143
|
+
log.debug(f"up-to-date: {key}")
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
self.put(db_peaks[key]['self']['href'], updates[key]['payload'])
|
|
147
|
+
log.info(f"updated: {key}")
|
|
148
|
+
updated += 1
|
|
149
|
+
|
|
150
|
+
if len(to_upload):
|
|
151
|
+
# Note: POSTing the embedded-collection is *miles faster*
|
|
152
|
+
# than doing separate requests for each peak!
|
|
153
|
+
payload = {'_embedded': {'peaks': [updates[key]['payload'] for key in sorted(to_upload)]}}
|
|
154
|
+
self.post('/api/peaks', payload)
|
|
155
|
+
for key in sorted(to_upload): log.info(f"added new: {key}")
|
|
156
|
+
|
|
157
|
+
# Note: this disregards the peak-parent-relationship, but in
|
|
158
|
+
# order to implement this correctly, one would need to check
|
|
159
|
+
# if the parent-peak with a specific 'parentID' is already
|
|
160
|
+
# uploaded and search it.. there's an endpoint
|
|
161
|
+
# 'LINK /api/peaks/{parentID} Location: /api/peaks/{childID}'
|
|
162
|
+
# to link a child to its parent, but it remains complicated.
|
|
163
|
+
# TODO :: maybe later implement parent-peaks!?
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
'added': len(to_upload),
|
|
167
|
+
'updated': updated,
|
|
168
|
+
'up-to-date': len(to_update) - updated,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def iter_events(self, event_re=r".*"):
|
|
172
|
+
"""Follow the server-sent-events (SSE) on the DB-API.
|
|
173
|
+
|
|
174
|
+
`event_re` a regular expression to filter events (default: matches everything)
|
|
175
|
+
"""
|
|
176
|
+
yield from SSEventListener(event_re, host_url=self.url, endpoint="/api/events",
|
|
177
|
+
session=self.session)
|
|
178
|
+
|
|
@@ -18,17 +18,17 @@ class IoniClient:
|
|
|
18
18
|
Access the Ionicon WebAPI.
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
> client = IoniClient()
|
|
22
|
+
> client.get('TPS_Pull_H')
|
|
23
23
|
{'TPS_Pull_H': 123.45, ... }
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
> client.set('TPS_Pull_H', 42)
|
|
26
26
|
{'TPS_Pull_H': 42.0, ... }
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
> client.start_measurement()
|
|
29
29
|
ACK
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
> client.host, client.port
|
|
32
32
|
('localhost', 8002)
|
|
33
33
|
|
|
34
34
|
'''
|
|
@@ -85,19 +85,3 @@ class IoniClient:
|
|
|
85
85
|
|
|
86
86
|
def stop_measurement(self):
|
|
87
87
|
return self.set('ACQ_SRV_Stop_Meas', 1)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if __name__ == '__main__':
|
|
91
|
-
import sys
|
|
92
|
-
client = IoniClient()
|
|
93
|
-
|
|
94
|
-
if len(sys.argv) == 2:
|
|
95
|
-
print(client.get(sys.argv[1]))
|
|
96
|
-
elif len(sys.argv) == 3:
|
|
97
|
-
print(client.set(sys.argv[1], sys.argv[2]))
|
|
98
|
-
else:
|
|
99
|
-
print(f"""\
|
|
100
|
-
usage:
|
|
101
|
-
python {sys.argv[0]} <varname> [<value>]
|
|
102
|
-
""")
|
|
103
|
-
|