slowpy 0.4.0__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.
Files changed (65) hide show
  1. slowpy/__init__.py +11 -0
  2. slowpy/basetypes.py +118 -0
  3. slowpy/control/__init__.py +13 -0
  4. slowpy/control/control_AsyncDripline.py +492 -0
  5. slowpy/control/control_AsyncHTTP.py +153 -0
  6. slowpy/control/control_AsyncLocalPubsub.py +213 -0
  7. slowpy/control/control_AsyncMQTT.py +370 -0
  8. slowpy/control/control_AsyncModbus.py +208 -0
  9. slowpy/control/control_AsyncNATS.py +275 -0
  10. slowpy/control/control_AsyncRabbitMQ.py +594 -0
  11. slowpy/control/control_AsyncRedis.py +500 -0
  12. slowpy/control/control_AsyncSlowMQ.py +321 -0
  13. slowpy/control/control_AsyncSlowdash.py +43 -0
  14. slowpy/control/control_CAMAC.py +497 -0
  15. slowpy/control/control_DataStore.py +53 -0
  16. slowpy/control/control_Dripline.py +229 -0
  17. slowpy/control/control_DriplineInterface.py +61 -0
  18. slowpy/control/control_DummyDevice.py +178 -0
  19. slowpy/control/control_Ethernet.py +398 -0
  20. slowpy/control/control_HTTP.py +156 -0
  21. slowpy/control/control_LabJackU.py +466 -0
  22. slowpy/control/control_MQTT.py +246 -0
  23. slowpy/control/control_Microphone.py +198 -0
  24. slowpy/control/control_Modbus.py +172 -0
  25. slowpy/control/control_NanotechMotor.py +528 -0
  26. slowpy/control/control_RabbitMQ.py +596 -0
  27. slowpy/control/control_Redis.py +499 -0
  28. slowpy/control/control_Serial.py +104 -0
  29. slowpy/control/control_Shell.py +95 -0
  30. slowpy/control/control_Slowdash.py +42 -0
  31. slowpy/control/control_UDP.py +119 -0
  32. slowpy/control/control_VISA.py +108 -0
  33. slowpy/control/dummy_device.py +132 -0
  34. slowpy/control/hdl/__init__.py +3 -0
  35. slowpy/control/hdl/hdl.py +177 -0
  36. slowpy/control/netutils.py +127 -0
  37. slowpy/control/node.py +819 -0
  38. slowpy/control/scpi_server.py +344 -0
  39. slowpy/control/system.py +372 -0
  40. slowpy/graphs.py +121 -0
  41. slowpy/histograms.py +316 -0
  42. slowpy/mesh/__init__.py +5 -0
  43. slowpy/mesh/dash.py +81 -0
  44. slowpy/mesh/mesh.py +645 -0
  45. slowpy/mesh/packet.py +107 -0
  46. slowpy/mesh/stdio.py +403 -0
  47. slowpy/mesh/tasklet.py +755 -0
  48. slowpy/mpldata.py +348 -0
  49. slowpy/slowfetch.py +184 -0
  50. slowpy/slowplot.py +540 -0
  51. slowpy/store/__init__.py +9 -0
  52. slowpy/store/blob_storage.py +155 -0
  53. slowpy/store/factory.py +32 -0
  54. slowpy/store/store.py +156 -0
  55. slowpy/store/store_CSV.py +95 -0
  56. slowpy/store/store_HDF5.py +239 -0
  57. slowpy/store/store_InfluxDB2.py +143 -0
  58. slowpy/store/store_Redis.py +163 -0
  59. slowpy/store/store_SQL.py +489 -0
  60. slowpy/treetable.py +71 -0
  61. slowpy/trend.py +200 -0
  62. slowpy-0.4.0.dist-info/METADATA +27 -0
  63. slowpy-0.4.0.dist-info/RECORD +65 -0
  64. slowpy-0.4.0.dist-info/WHEEL +5 -0
  65. slowpy-0.4.0.dist-info/top_level.txt +1 -0
slowpy/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+
2
+ from .basetypes import DataElement, TimeSeries
3
+ from .histograms import Histogram, Histogram2d, HistogramBasicStat, HistogramCountStat, Histogram2dBasicStat
4
+ from .graphs import Graph, GraphYStat
5
+ from .trend import Trend, RateTrend
6
+ from .treetable import Tree, Table
7
+
8
+ from .slowfetch import SlowFetch
9
+
10
+ from .mpldata import slowdashify
11
+ from .slowplot import slowplot
slowpy/basetypes.py ADDED
@@ -0,0 +1,118 @@
1
+ # Created by Sanshiro Enomoto on 17 July 2024 #
2
+
3
+
4
+ import time, json
5
+
6
+
7
+ class DataElement:
8
+ def __init__(self):
9
+ self.attr_values = {}
10
+ self.stat_values = {}
11
+ self.stat_functors = []
12
+
13
+
14
+ def add_attr(self, name, value):
15
+ self.attr_values[name] = value
16
+
17
+
18
+ def add_stat(self, arg0, arg1=None):
19
+ # add_stat(key, value) or add_stat(functor) where functor takes an DataElement and returns a dict
20
+ if callable(arg0):
21
+ self.stat_functors.append(arg0)
22
+ else:
23
+ self.stat_values[arg0] = arg1
24
+
25
+
26
+ def clear(self):
27
+ self.stat_values = {}
28
+
29
+
30
+ def to_json(self):
31
+ for f in self.stat_functors:
32
+ self.stat_values.update(f(self))
33
+
34
+ record = {}
35
+ if any(self.attr_values):
36
+ record.update({ '_attr': self.attr_values })
37
+ if any(self.stat_values):
38
+ record.update({ '_stat': self.stat_values })
39
+ return record
40
+
41
+
42
+ @staticmethod
43
+ def from_json(obj):
44
+ if type(obj) is not dict:
45
+ return obj
46
+
47
+ from .graphs import Graph
48
+ from .histograms import Histogram, Histogram2d
49
+
50
+ if 'bins' in obj:
51
+ return Histogram.from_json(obj)
52
+ elif 'ybins' in obj:
53
+ return Histogram2d.from_json(obj)
54
+ elif 'y' in obj:
55
+ return Graph.from_json(obj)
56
+ return None
57
+
58
+
59
+ def __str__(self):
60
+ return json.dumps(self.to_json())
61
+
62
+
63
+
64
+ class TimeSeries:
65
+ def __init__(self, *, fields:list[str]|None=None, start:float=0, length:float|None=None):
66
+ self.start = start
67
+ self.length = length
68
+ self.fields = fields if fields is not None and len(fields) > 0 else ['x']
69
+ self.t = []
70
+ self.values = [ [] for _ in self.fields ] # self.values[field] is a time-series (array) for the field
71
+
72
+
73
+ def write(self, values, t=None):
74
+ '''
75
+ # add one point to the time-series #
76
+ - values: dict for field name-value pairs, or list for field values, or a value
77
+ where a value can be a number, string, or data-element.
78
+ - time: UNIX time-stamp, if None if given, the current time will be used.
79
+ '''
80
+ if t is None:
81
+ t = time.time()
82
+
83
+ record = [None] * len(self.fields)
84
+ if isinstance(values, dict):
85
+ for i in range(len(record)):
86
+ record[i] = values.get(self.fields[i], None)
87
+ elif isinstance(values, list):
88
+ for i in range(min(len(values), len(record))):
89
+ record[i] = values[i]
90
+ else:
91
+ record[0] = values
92
+
93
+ self.t.append(t - self.start)
94
+ for k in range(len(self.fields)):
95
+ self.values[k].append(record[k])
96
+
97
+
98
+ def to_json(self):
99
+ length = self.length
100
+ if length is None:
101
+ if len(self.t) < 1:
102
+ length = 1
103
+ else:
104
+ length = self.t[-1] + 1
105
+
106
+ data = {
107
+ 'start': self.start,
108
+ 'length': length,
109
+ 't': self.t,
110
+ }
111
+ for k in range(len(self.fields)):
112
+ data[self.fields[k]] = self.values[k]
113
+
114
+ return data
115
+
116
+
117
+ def __str__(self):
118
+ return json.dumps(self.to_json())
@@ -0,0 +1,13 @@
1
+
2
+ from .node import ControlNode, ControlVariableNode, ControlThreadNode, ControlException
3
+ from .system import ControlSystem, ValueNode, control_system
4
+ from .control_Ethernet import EthernetNode, ScpiNode, ScpiCommandNode
5
+ from .control_UDP import UdpSocketNode
6
+ from .control_HTTP import HttpNode
7
+ from .control_Shell import ShellNode
8
+ from .control_DataStore import DataStoreNode
9
+
10
+ from .scpi_server import ScpiServer, ScpiAdapter
11
+ from .netutils import find_ip
12
+
13
+ from .dummy_device import RandomWalkDevice, RandomHitDevice, RandomChargeDevice, RandomTimeDevice
@@ -0,0 +1,492 @@
1
+ # Created by Sanshiro Enomoto on 3 October 2025 #
2
+
3
+ import os, time, datetime, socket, copy, getpass, uuid, json, inspect, asyncio, logging
4
+
5
+ from slowpy.control import ControlNode, ControlVariableNode, control_system as ctrl
6
+ ctrl.import_control_module('AsyncRabbitMQ')
7
+
8
+
9
+
10
+ sender_info = {
11
+ 'exe': __file__,
12
+ 'hostname': socket.gethostname(),
13
+ 'service_name': 'slowdrip',
14
+ 'username': getpass.getuser(),
15
+ 'versions': {
16
+ 'slowdash': {
17
+ 'package': 'slowdash',
18
+ 'version': '0',
19
+ 'commit': '0',
20
+ }
21
+ }
22
+ }
23
+
24
+
25
+ class AsyncDriplineNode(ControlNode):
26
+ _seq = 0
27
+
28
+ def __init__(self, rabbitmq_url:str, name:str=None):
29
+ self.rmq = ctrl.async_rabbitmq(rabbitmq_url)
30
+ self.name = name or f'AsyncSlowDrip_{socket.gethostname()}_{os.getpid()}_{AsyncDriplineNode._seq}'
31
+ self.sender_id = str(uuid.uuid4())
32
+ AsyncDriplineNode._seq += 1
33
+
34
+ self.alerts_exchange = self.rmq.topic_exchange('alerts')
35
+ self.requests_exchange = self.rmq.topic_exchange('requests')
36
+
37
+ self.reply_queue_node = None # to be set by endpoint()
38
+ self.request_queue_node = None # to be set by request()
39
+ self.sensor_value_queue_node = None # to be set by self.sensors_value_queue()
40
+ self.heartbeat_queue_node = None # to be set by self.heartbeat_queue()
41
+ self.status_message_queue_node = None # to be set by self.status_message_queue()
42
+
43
+
44
+ async def aio_close(self):
45
+ if self.rmq is not None:
46
+ await self.rmq.aio_close()
47
+ self.rmq = None
48
+
49
+
50
+ ## child nodes ##
51
+ # dripline().endpoint(name): set/get to other dripline endpoints
52
+ def endpoint(self, name:str, *, specifier:str=None, lockout_key:str=None, timeout=None):
53
+ return EndpointNode(self, name, specifier=specifier, lockout_key=lockout_key, timeout=timeout)
54
+
55
+ # dripline().request(): handles set/get/cmd requests from other dripline services
56
+ def request(self, handler):
57
+ return RequestNode(self, handler)
58
+
59
+ # dripline().sensor_value_alert(): sends sensor_value alert; use aio_set(value)
60
+ def sensor_value_alert(self, name:str=None):
61
+ return SensorValueAlertNode(self, name)
62
+
63
+ # dripline().heartbeat_alert(): sends heartbeat alerts; use aio_set(value)
64
+ def heartbeat_alert(self):
65
+ return HeartbeatAlertNode(self)
66
+
67
+ # dripline().status_message_alert(): sends status_message alerts; use aio_set(value)
68
+ def status_message_alert(self, message_type='notice'):
69
+ return StatusMessageAlertNode(self, message_type)
70
+
71
+ # dripline().sensor_value_queue(): receives sensor_value; use aio_get()
72
+ def sensor_value_queue(self):
73
+ return SensorValuesQueueNode(self)
74
+
75
+ # dripline().heartbeat_queue(): receives heartbeat; use aio_get()
76
+ def heartbeat_queue(self):
77
+ return HeartbeatQueueNode(self)
78
+
79
+ # dripline().status_message_queue(): receives status_messages; use aio_get()
80
+ def status_message_queue(self):
81
+ return StatusMessageQueueNode(self)
82
+
83
+ # dripline().service(server):
84
+ def service(self, server, *, endpoints:list[str]|None=None):
85
+ return ServiceNode(self, server, endpoints=endpoints)
86
+
87
+
88
+ @classmethod
89
+ def _node_creator_method(cls):
90
+ def async_dripline(self, *args, **kwargs):
91
+ if True:
92
+ return AsyncDriplineNode(*args, **kwargs)
93
+
94
+ try:
95
+ self.dripline_node
96
+ except:
97
+ self.dripline_node = AsyncDriplineNode(*args, **kwargs)
98
+
99
+ return self.dripline_node
100
+
101
+ return async_dripline
102
+
103
+
104
+
105
+ class EndpointNode(ControlNode):
106
+ def __init__(self, dripline:AsyncDriplineNode, name:str, *, specifier:str=None, lockout_key:str=None, timeout=None):
107
+ self.specifier = specifier or ''
108
+ self.lockout_key = lockout_key or '00000000-0000-0000-0000-000000000000'
109
+
110
+ if dripline.reply_queue_node is None:
111
+ dripline.reply_queue_node = (
112
+ dripline.requests_exchange.queue(f'{dripline.name}_reply', timeout=(timeout or 5), exclusive=True)
113
+ )
114
+ self.reply_queue_node = dripline.reply_queue_node
115
+ self.routing_key = name
116
+
117
+
118
+ async def aio_set(self, value):
119
+ operation_code = 0 # 0: Set, 1: Get, 9: Command
120
+ if type(value) is dict:
121
+ body = copy.deepcopy(value)
122
+ elif type(value) is list:
123
+ body = { 'values': value }
124
+ else:
125
+ body = { 'values': [value] }
126
+ return await self.aio_do_send_request(operation_code, body)
127
+
128
+
129
+ async def aio_get(self):
130
+ operation_code = 1 # 0: Set, 1: Get, 9: Command
131
+ body = {}
132
+ return await self.aio_do_send_request(operation_code, body)
133
+
134
+
135
+ ## child nodes ##
136
+ # dripline().endpoint(name).value_raw(): aio_get() returns 'value_raw'
137
+ def value_raw(self):
138
+ return RawValueNode(self)
139
+
140
+ # dripline().endpoint(name).value_cal(): aio_get() returns 'value_cal'
141
+ def value_cal(self):
142
+ return CalibratedValueNode(self)
143
+
144
+ # dripline().endpoint(name).command(*args,**kwargs): aio_get() sends a command to other endpoints
145
+ def command(self, *args, **kwargs):
146
+ return EndpointCommandNode(self, *args, **kwargs)
147
+
148
+
149
+ async def aio_do_send_request(self, operation_code:int, body):
150
+ now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
151
+
152
+ headers = {
153
+ 'message_operation': operation_code, # 0: Set, 1: Get, 9: Command
154
+ 'message_type': 3, # 2: Reply, 3: Request, 4: Alert
155
+ 'lockout_key': self.lockout_key,
156
+ 'specifier': self.specifier,
157
+ 'timestamp': now.isoformat().replace('+00:00', 'Z'),
158
+ 'sender_info': sender_info,
159
+ }
160
+ parameters = {
161
+ 'message_id': f'{uuid.uuid4()}/0/1',
162
+ 'content_encoding': 'application/json',
163
+ }
164
+
165
+ try:
166
+ message = await self.reply_queue_node.rpc_call(self.routing_key, headers, body, parameters).aio_get()
167
+ except Exception as e:
168
+ logging.error(f'SlowDrip.Endpoint[{self.routing_key}]: RPC error: {e}')
169
+ return None
170
+ if message is None:
171
+ return None
172
+
173
+ if type(message.body) is dict:
174
+ return message.body
175
+ else:
176
+ return json.loads(message.body or '{}')
177
+
178
+
179
+
180
+ class EndpointCommandNode(ControlVariableNode):
181
+ def __init__(self, endpoint:EndpointNode, *args, **kwargs):
182
+ self.endpoint_node = endpoint
183
+ self.body = {'values': [ arg for arg in args ] }
184
+ self.body.update({ k:v for k,v in kwargs.items() })
185
+
186
+
187
+ async def aio_get(self):
188
+ operation_code = 9 # 0: Set, 1: Get, 9: Command
189
+ return await self.endpoint_node.aio_do_send_request(operation_code, self.body)
190
+
191
+
192
+
193
+ class RawValueNode(ControlVariableNode):
194
+ def __init__(self, endpoint:EndpointNode):
195
+ self.endpoint = endpoint
196
+
197
+ async def aio_set(self, value):
198
+ return await self.endpoint.aio_set(value)
199
+
200
+ async def aio_get(self):
201
+ return ((await self.endpoint.aio_get()) or {}).get('value_raw', None)
202
+
203
+
204
+
205
+ class CalibratedValueNode(ControlNode):
206
+ def __init__(self, endpoint:EndpointNode):
207
+ self.endpoint = endpoint
208
+
209
+ async def aio_get(self):
210
+ return ((await self.endpoint.aio_get()) or {}).get('value_cal', None)
211
+
212
+
213
+
214
+ class RequestNode(ControlNode):
215
+ def __init__(self, dripline:AsyncDriplineNode, handler):
216
+ self.handler = handler
217
+
218
+ if dripline.request_queue_node is None:
219
+ dripline.request_queue_node = (
220
+ dripline.requests_exchange.queue(f'{dripline.name}', exclusive=True)
221
+ )
222
+ self.request_queue_node = dripline.request_queue_node
223
+
224
+
225
+ async def aio_get(self):
226
+ try:
227
+ return await self.request_queue_node.rpc_function(self.handler).aio_get()
228
+ except Exception as e:
229
+ logging(e)
230
+ return None
231
+
232
+
233
+
234
+ class SensorValueAlertNode(ControlNode):
235
+ def __init__(self, dripline:AsyncDriplineNode, name:str=None):
236
+ self.name = name or dripline.name
237
+ self.sender_id = dripline.sender_id
238
+ self.publisher_node = dripline.alerts_exchange.publisher(f'sensor_value.{self.name}')
239
+
240
+
241
+ async def aio_set(self, value):
242
+ now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
243
+
244
+ if type(value) is tuple and len(value) >= 2:
245
+ body = { 'value_raw': value[0], 'value_cal': value[1] }
246
+ elif type(value) is dict:
247
+ body = value
248
+ elif type(value) in [ bool, int, float, str ]:
249
+ body = { 'value_raw': value }
250
+ else:
251
+ # throw an error?
252
+ body = value
253
+
254
+ if True:
255
+ if (type(body) is not dict) or ('value_raw' not in body):
256
+ # throw an error?
257
+ return
258
+ if body['value_raw'] is None:
259
+ # Dripline logger does not like a None value
260
+ return
261
+
262
+ headers = {
263
+ 'message_type': 4, # 2: Reply, 3: Request, 4: Alert
264
+ 'timestamp': now.isoformat().replace('+00:00', 'Z'),
265
+ 'sender_info': sender_info,
266
+ }
267
+ parameters = {
268
+ 'message_id': f'{uuid.uuid4()}/0/1',
269
+ 'content_encoding': 'application/json',
270
+ }
271
+
272
+ return await self.publisher_node.aio_set((headers, body, parameters))
273
+
274
+
275
+
276
+ class HeartbeatAlertNode(ControlNode):
277
+ def __init__(self, dripline:AsyncDriplineNode):
278
+ self.name = dripline.name
279
+ self.sender_id = dripline.sender_id
280
+ self.publisher_node = dripline.alerts_exchange.publisher(f'heartbeat.{self.name}')
281
+
282
+
283
+ async def aio_set(self, value):
284
+ now = datetime.datetime.fromtimestamp(value or time.time(), tz=datetime.timezone.utc)
285
+
286
+ body = {
287
+ 'id': self.sender_id,
288
+ 'name': self.name,
289
+ }
290
+ headers = {
291
+ 'message_type': 4, # 2: Reply, 3: Request, 4: Alert
292
+ 'timestamp': now.isoformat().replace('+00:00', 'Z'),
293
+ 'sender_info': sender_info,
294
+ }
295
+ parameters = {
296
+ 'message_id': f'{uuid.uuid4()}/0/1',
297
+ 'content_encoding': 'application/json',
298
+ }
299
+
300
+ return await self.publisher_node.aio_set((headers, body, parameters))
301
+
302
+
303
+
304
+ class StatusMessageAlertNode(ControlNode):
305
+ def __init__(self, dripline:AsyncDriplineNode, message_type='notice'):
306
+ self.name = dripline.name
307
+ self.sender_id = dripline.sender_id
308
+ self.publisher_node = dripline.alerts_exchange.publisher(f'status_message.{self.name}.{message_type}')
309
+
310
+
311
+ async def aio_set(self, value):
312
+ now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
313
+
314
+ body = {
315
+ 'message': value,
316
+ }
317
+ headers = {
318
+ 'message_type': 4, # 2: Reply, 3: Request, 4: Alert
319
+ 'timestamp': now.isoformat().replace('+00:00', 'Z'),
320
+ 'sender_info': sender_info,
321
+ }
322
+ parameters = {
323
+ 'message_id': f'{uuid.uuid4()}/0/1',
324
+ 'content_encoding': 'application/json',
325
+ }
326
+
327
+ return await self.publisher_node.aio_set((headers, body, parameters))
328
+
329
+
330
+
331
+ class SensorValuesQueueNode(ControlNode):
332
+ def __init__(self, dripline:AsyncDriplineNode):
333
+ if dripline.sensor_value_queue_node is None:
334
+ dripline.sensor_value_queue_node = (
335
+ dripline.alerts_exchange.queue(f'{dripline.name}_sensor_value', routing_key='sensor_value.*', exclusive=True)
336
+ )
337
+ self.queue_node = dripline.sensor_value_queue_node
338
+
339
+
340
+ async def aio_get(self):
341
+ try:
342
+ message = await self.queue_node.aio_get()
343
+ except Exception as e:
344
+ logging.error(e)
345
+ return None
346
+ if message is None:
347
+ return None
348
+
349
+ if message.body is None or type(message.body) is dict:
350
+ return message
351
+ else:
352
+ # Dripline puts content_type in the content_encoding fields, causing unparsed results
353
+ return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
354
+
355
+
356
+
357
+ class HeartbeatQueueNode(ControlNode):
358
+ def __init__(self, dripline:AsyncDriplineNode):
359
+ if dripline.heartbeat_queue_node is None:
360
+ dripline.heartbeat_queue_node = (
361
+ dripline.alerts_exchange.queue(f'{dripline.name}_heartbeat', routing_key='heartbeat.*', exclusive=True)
362
+ )
363
+ self.queue_node = dripline.heartbeat_queue_node
364
+
365
+
366
+ async def aio_get(self):
367
+ try:
368
+ message = await self.queue_node.aio_get()
369
+ except Exception as e:
370
+ logging.error(e)
371
+ return None
372
+ if message is None:
373
+ return None
374
+
375
+ if message.body is None or type(message.body) is dict:
376
+ return message
377
+ else:
378
+ # Dripline puts content_type in the content_encoding fields, causing unparsed results
379
+ return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
380
+
381
+
382
+
383
+ class StatusMessageQueueNode(ControlNode):
384
+ def __init__(self, dripline:AsyncDriplineNode):
385
+ if dripline.status_message_queue_node is None:
386
+ dripline.status_message_queue_node = (
387
+ dripline.alerts_exchange.queue(f'{dripline.name}_status_message', routing_key='status_message.*.*', exclusive=True)
388
+ )
389
+ self.queue_node = dripline.status_message_queue_node
390
+
391
+
392
+ async def aio_get(self):
393
+ try:
394
+ message = await self.queue_node.aio_get()
395
+ except Exception as e:
396
+ logging.error(e)
397
+ return None
398
+ if message is None:
399
+ return None
400
+
401
+ if message.body is None or type(message.body) is dict:
402
+ return message
403
+ else:
404
+ # Dripline puts content_type in the content_encoding fields, causing unparsed results
405
+ return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
406
+
407
+
408
+
409
+ class ServiceNode(ControlNode):
410
+ def __init__(self, dripline:AsyncDriplineNode, server, *, endpoints:list[str]|str|None=None):
411
+ self.dripline_node = dripline
412
+ self.server = server
413
+
414
+ if type(endpoints) is list:
415
+ self.endpoints = list(endpoints)
416
+ else:
417
+ self.endpoints = [endpoints or '*']
418
+
419
+ routing_keys = list(self.endpoints) or '*'
420
+
421
+ self.request_queue_node = dripline.requests_exchange.queue(
422
+ name = dripline.name,
423
+ routing_key = routing_keys,
424
+ exclusive = True,
425
+ )
426
+
427
+ self.heartbeat_alert = dripline.heartbeat_alert()
428
+ self.status_message_alert = dripline.status_message_alert()
429
+
430
+
431
+ async def aio_start(self):
432
+ await asyncio.gather(
433
+ self._handle_requests(),
434
+ self._send_heartbeats()
435
+ )
436
+
437
+
438
+ async def _send_heartbeats(self):
439
+ while not ctrl.is_stop_requested():
440
+ await self.heartbeat_alert.aio_set(time.time())
441
+ await self.status_message_alert.aio_set('I am working')
442
+ await ctrl.aio_sleep(30)
443
+
444
+
445
+ async def _handle_requests(self):
446
+ while not ctrl.is_stop_requested():
447
+ try:
448
+ await self.request_queue_node.rpc_function(self._handle_message).aio_get()
449
+ except Exception as e:
450
+ logging.error(e)
451
+ logging.info('AsyncDripline.aio_start(): resuming in 3 seconds')
452
+ await ctrl.aio_sleep(3)
453
+
454
+
455
+ async def _handle_message(self, message):
456
+ routing_key = message.parameters.get('routing_key')
457
+ operation = message.headers.get('message_operation', -1) # 0: Set, 1: Get, 9: Command
458
+ if operation < 0: # reply message
459
+ return
460
+
461
+ logging.debug(f'REQUEST: key={routing_key}, op={operation}, body={message.body}')
462
+
463
+ reply = None
464
+ if operation == 0:
465
+ if hasattr(self.server, 'on_set') and callable(getattr(self.server, 'on_set')):
466
+ reply = self.server.on_set(message)
467
+ elif operation == 1:
468
+ if hasattr(self.server, 'on_get') and callable(getattr(self.server, 'on_get')):
469
+ reply = self.server.on_get(message)
470
+ elif operation == 9:
471
+ if hasattr(self.server, 'on_command') and callable(getattr(self.server, 'on_command')):
472
+ reply = self.server.on_command(message)
473
+ else:
474
+ logging.warning(f'Dripline: Unknown operation code: {operation}')
475
+
476
+ if inspect.isawaitable(reply):
477
+ reply = await reply
478
+
479
+ if reply is None:
480
+ reply = {'status': 'ERROR: invalid request'}
481
+ logging.warning(f'Dripline: request not handled: key={routing_key}, op={operation}, body={message.body}')
482
+ elif type(reply) is dict:
483
+ pass
484
+ elif type(reply) is tuple and len(reply) >= 2:
485
+ reply = { 'value_raw': reply[0], 'value_cal': reply[1] }
486
+ elif type(reply) in [ bool, int, float, str ]:
487
+ reply = { 'value_raw': reply }
488
+ else:
489
+ # throw an error?
490
+ pass
491
+
492
+ return reply