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.
Files changed (41) hide show
  1. pytrms-0.9.1/PKG-INFO +19 -0
  2. {pytrms-0.2.2 → pytrms-0.9.1}/pyproject.toml +15 -10
  3. pytrms-0.9.1/pytrms/__init__.py +36 -0
  4. pytrms-0.9.1/pytrms/_base/__init__.py +24 -0
  5. pytrms-0.9.1/pytrms/_base/ioniclient.py +32 -0
  6. pytrms-0.9.1/pytrms/_base/mqttclient.py +106 -0
  7. pytrms-0.9.1/pytrms/clients/__init__.py +33 -0
  8. pytrms-0.9.1/pytrms/clients/db_api.py +178 -0
  9. {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/clients/ioniclient.py +5 -21
  10. pytrms-0.9.1/pytrms/clients/modbus.py +528 -0
  11. pytrms-0.9.1/pytrms/clients/mqtt.py +793 -0
  12. pytrms-0.9.1/pytrms/clients/ssevent.py +77 -0
  13. pytrms-0.9.1/pytrms/compose/__init__.py +2 -0
  14. pytrms-0.9.1/pytrms/compose/composition.py +302 -0
  15. pytrms-0.9.1/pytrms/data/ParaIDs.csv +731 -0
  16. pytrms-0.9.1/pytrms/helpers.py +120 -0
  17. pytrms-0.9.1/pytrms/instrument.py +114 -0
  18. pytrms-0.9.1/pytrms/measurement.py +171 -0
  19. pytrms-0.9.1/pytrms/peaktable.py +501 -0
  20. pytrms-0.9.1/pytrms/plotting/__init__.py +4 -0
  21. pytrms-0.9.1/pytrms/readers/__init__.py +4 -0
  22. pytrms-0.9.1/pytrms/readers/ionitof_reader.py +472 -0
  23. pytrms-0.2.2/PKG-INFO +0 -17
  24. pytrms-0.2.2/par_ID_list.txt +0 -653
  25. pytrms-0.2.2/pytrms/__init__.py +0 -42
  26. pytrms-0.2.2/pytrms/clients/__init__.py +0 -7
  27. pytrms-0.2.2/pytrms/clients/mockclient.py +0 -612
  28. pytrms-0.2.2/pytrms/factory.py +0 -35
  29. pytrms-0.2.2/pytrms/helpers.py +0 -17
  30. pytrms-0.2.2/pytrms/instrument.py +0 -178
  31. pytrms-0.2.2/pytrms/measurement.py +0 -90
  32. pytrms-0.2.2/pytrms/modbus.py +0 -126
  33. pytrms-0.2.2/pytrms/reader.py +0 -177
  34. pytrms-0.2.2/pytrms/testing/__init__.py +0 -14
  35. pytrms-0.2.2/pytrms/testing/response_TRACES_webAPI.txt +0 -556
  36. pytrms-0.2.2/pytrms/tracebuffer.py +0 -108
  37. pytrms-0.2.2/setup.py +0 -33
  38. {pytrms-0.2.2 → pytrms-0.9.1}/LICENSE +0 -0
  39. {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/_version.py +0 -0
  40. {pytrms-0.2.2 → pytrms-0.9.1}/pytrms/data/IoniTofPrefs.ini +0 -0
  41. {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.2.2"
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
- "par_ID_list.txt",
8
+ "pytrms/data/ParaIDs.csv",
9
9
  "pytrms/data/IoniTofPrefs.ini",
10
10
  ]
11
11
 
12
12
  [tool.poetry.dependencies]
13
- python = "^3.8"
14
- h5py = "^3.6.0"
15
- requests = "^2.27.1"
16
- pandas = "^1.4.0"
17
- pyModbusTCP = "^0.2.0"
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
- ipykernel = "^6.13.1"
21
- matplotlib = "^3.5.2"
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
- >>> client = IoniClient()
22
- >>> client.get('TPS_Pull_H')
21
+ > client = IoniClient()
22
+ > client.get('TPS_Pull_H')
23
23
  {'TPS_Pull_H': 123.45, ... }
24
24
 
25
- >>> client.set('TPS_Pull_H', 42)
25
+ > client.set('TPS_Pull_H', 42)
26
26
  {'TPS_Pull_H': 42.0, ... }
27
27
 
28
- >>> client.start_measurement()
28
+ > client.start_measurement()
29
29
  ACK
30
30
 
31
- >>> client.host, client.port
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
-