xaal.lib 0.7.7__py3-none-any.whl → 0.7.8__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.
xaal/lib/devices.py CHANGED
@@ -18,127 +18,156 @@
18
18
  # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
19
  #
20
20
 
21
+ import logging
22
+ import time
23
+ import typing
24
+ from typing import Any, Optional, Union, Callable, Awaitable
25
+
26
+ from tabulate import tabulate
21
27
 
22
- from . import config
23
- from . import tools
24
- from . import bindings
28
+ from . import bindings, config, tools
25
29
  from .exceptions import DeviceError
26
30
 
27
- from tabulate import tabulate
28
- import logging
31
+ if typing.TYPE_CHECKING:
32
+ from .aioengine import AsyncEngine
33
+ from .engine import Engine
34
+ from .core import EngineMixin
35
+
36
+ # Funtion types with any arguments and return a dict or None (device methods)
37
+ MethodT = Union[Callable[..., Union[dict, None]], Callable[..., Awaitable[Union[dict, None]]]]
38
+
29
39
  logger = logging.getLogger(__name__)
30
40
 
31
- import time
32
41
 
33
42
  class Attribute(object):
34
-
35
- def __init__(self, name, dev=None, default=None):
43
+ def __init__(self, name, dev: Optional['Device'] = None, default: Any = None):
36
44
  self.name = name
37
45
  self.default = default
38
- self.device = dev
46
+ self.device: Optional['Device'] = dev
39
47
  self.__value = default
40
48
 
41
49
  @property
42
- def value(self):
50
+ def value(self) -> Any:
43
51
  return self.__value
44
52
 
45
53
  @value.setter
46
- def value(self, value):
54
+ def value(self, value: Any):
47
55
  if value != self.__value and self.device:
48
56
  eng = self.device.engine
49
57
  if eng:
50
58
  eng.add_attributes_change(self)
51
- logger.debug("Attr change %s %s=%s" % (self.device.address,self.name,value))
59
+ logger.debug("Attr change %s %s=%s" % (self.device.address, self.name, value))
52
60
  self.__value = value
53
61
 
54
- def __repr__(self): # pragma: no cover
62
+ def __repr__(self) -> str: # pragma: no cover
55
63
  return f"<{self.__module__}.Attribute {self.name} at 0x{id(self):x}>"
56
64
 
57
65
 
58
66
  class Attributes(list):
59
67
  """Devices owns a attributes list. This list also have dict-like access"""
60
68
 
61
- def __getitem__(self,value):
62
- if isinstance(value,int):
63
- return list.__getitem__(self,value)
69
+ def __getitem__(self, value):
70
+ if isinstance(value, int):
71
+ return list.__getitem__(self, value)
64
72
  for k in self:
65
- if (value == k.name):
73
+ if value == k.name:
66
74
  return k.value
67
75
  raise KeyError(value)
68
76
 
69
- def __setitem__(self,name,value):
70
- if isinstance(name,int):
71
- return list.__setitem__(self,name,value)
77
+ def __setitem__(self, name, value):
78
+ if isinstance(name, int):
79
+ return list.__setitem__(self, name, value)
72
80
  for k in self:
73
- if (name == k.name):
81
+ if name == k.name:
74
82
  k.value = value
75
83
  return
76
84
  raise KeyError(name)
77
85
 
78
- class Device(object):
79
-
80
- __slots__ = ['__dev_type','__address','group_id',
81
- 'vendor_id','product_id','hw_id',
82
- '__version','__url','schema','info',
83
- 'unsupported_attributes','unsupported_methods','unsupported_notifications',
84
- 'alive_period','next_alive',
85
- '__attributes','methods','engine']
86
86
 
87
- def __init__(self,dev_type,addr=None,engine=None):
87
+ class Device(object):
88
+ __slots__ = [
89
+ '__dev_type',
90
+ '__address',
91
+ 'group_id',
92
+ 'vendor_id',
93
+ 'product_id',
94
+ 'hw_id',
95
+ '__version',
96
+ '__url',
97
+ 'schema',
98
+ 'info',
99
+ 'unsupported_attributes',
100
+ 'unsupported_methods',
101
+ 'unsupported_notifications',
102
+ 'alive_period',
103
+ 'next_alive',
104
+ '__attributes',
105
+ 'methods',
106
+ 'engine',
107
+ ]
108
+
109
+ def __init__(
110
+ self,
111
+ dev_type: str,
112
+ addr: Optional[bindings.UUID] = None,
113
+ engine: Union['AsyncEngine', 'Engine', 'EngineMixin', None] = None,
114
+ ):
88
115
  # xAAL internal attributes for a device
89
- self.dev_type = dev_type # xaal dev_type
90
- self.address = addr # xaal addr
91
- self.group_id = None # group devices
92
- self.vendor_id = None # vendor ID ie : ACME
93
- self.product_id = None # product ID
94
- self.hw_id = None # hardware info
95
- self.__version = None # product release
96
- self.__url = None # product URL
97
- self.schema = None # schema URL
98
- self.info = None # additionnal info
116
+ self.dev_type = dev_type # xaal dev_type
117
+ self.address = addr # xaal addr
118
+ self.group_id: Optional[bindings.UUID] = None # group devices
119
+ self.vendor_id: Optional[str] = None # vendor ID ie : ACME
120
+ self.product_id: Optional[str] = None # product ID
121
+ self.hw_id: Optional[str] = None # hardware info
122
+ self.version = None # product release
123
+ self.url = None # product URL
124
+ self.schema: Optional[str] = None # schema URL
125
+ self.info: Optional[str] = None # additionnal info
99
126
 
100
127
  # Unsupported stuffs
101
128
  self.unsupported_attributes = []
102
129
  self.unsupported_methods = []
103
130
  self.unsupported_notifications = []
104
131
  # Alive management
105
- self.alive_period = config.alive_timer # time in sec between two alive
132
+ self.alive_period = config.alive_timer # time in sec between two alive
106
133
  self.next_alive = 0
107
134
  # Default attributes & methods
108
135
  self.__attributes = Attributes()
109
- self.methods = {'get_attributes' : self._get_attributes,
110
- 'get_description': self._get_description }
136
+ self.methods: dict[str, MethodT] = {
137
+ 'get_attributes': self._get_attributes,
138
+ 'get_description': self._get_description,
139
+ }
111
140
  self.engine = engine
112
141
 
113
142
  @property
114
- def dev_type(self):
143
+ def dev_type(self) -> str:
115
144
  return self.__dev_type
116
145
 
117
146
  @dev_type.setter
118
- def dev_type(self, value):
147
+ def dev_type(self, value: str):
119
148
  if not tools.is_valid_dev_type(value):
120
149
  raise DeviceError(f"The dev_type {value} is not valid")
121
150
  self.__dev_type = value
122
151
 
123
152
  @property
124
- def version(self):
153
+ def version(self) -> Optional[str]:
125
154
  return self.__version
126
155
 
127
156
  @version.setter
128
- def version(self, value):
129
- # version must be a string
157
+ def version(self, value: Any):
158
+ # version must be a string
130
159
  if value:
131
160
  self.__version = "%s" % value
132
161
  else:
133
162
  self.__version = None
134
163
 
135
164
  @property
136
- def address(self):
165
+ def address(self) -> Optional[bindings.UUID]:
137
166
  return self.__address
138
167
 
139
168
  @address.setter
140
- def address(self, value):
141
- if value == None:
169
+ def address(self, value: Optional[bindings.UUID]):
170
+ if value is None:
142
171
  self.__address = None
143
172
  return
144
173
  if not tools.is_valid_address(value):
@@ -146,61 +175,65 @@ class Device(object):
146
175
  self.__address = value
147
176
 
148
177
  @property
149
- def url(self):
178
+ def url(self) -> Optional[bindings.URL]:
150
179
  return self.__url
151
180
 
152
181
  @url.setter
153
- def url(self,value):
154
- if value == None:
182
+ def url(self, value: Optional[str]):
183
+ if value is None:
155
184
  self.__url = None
156
185
  else:
157
186
  self.__url = bindings.URL(value)
158
187
 
159
188
  # attributes
160
- def new_attribute(self,name,default=None):
161
- attr = Attribute(name,self,default)
189
+ def new_attribute(self, name: str, default: Any = None):
190
+ attr = Attribute(name, self, default)
162
191
  self.add_attribute(attr)
163
192
  return attr
164
193
 
165
- def add_attribute(self, attr):
194
+ def add_attribute(self, attr: Attribute):
166
195
  if attr:
167
196
  self.__attributes.append(attr)
168
197
  attr.device = self
169
198
 
170
- def del_attribute(self,attr):
199
+ def del_attribute(self, attr: Attribute):
171
200
  if attr:
172
201
  attr.device = None
173
202
  self.__attributes.remove(attr)
174
203
 
175
- def get_attribute(self,name):
204
+ def get_attribute(self, name: str) -> Optional[Attribute]:
176
205
  for attr in self.__attributes:
177
206
  if attr.name == name:
178
207
  return attr
179
208
  return None
180
209
 
181
210
  @property
182
- def attributes(self):
211
+ def attributes(self) -> Attributes:
183
212
  return self.__attributes
184
213
 
185
214
  @attributes.setter
186
- def attributes(self,values):
187
- if isinstance(values,Attributes):
215
+ def attributes(self, values: Attributes):
216
+ if isinstance(values, Attributes):
188
217
  self.__attributes = values
189
218
  else:
190
219
  raise DeviceError("Invalid attributes list, use class Attributes)")
191
220
 
192
- def add_method(self,name,func):
193
- self.methods.update({name:func})
221
+ def add_method(self, name: str, func: MethodT):
222
+ self.methods.update({name: func})
223
+
224
+ def del_method(self, name: str):
225
+ if name in self.methods:
226
+ del self.methods[name]
194
227
 
195
- def get_methods(self):
228
+ def get_methods(self) -> dict[str, MethodT]:
196
229
  return self.methods
197
230
 
198
231
  def update_alive(self):
199
- """ update the alive timimg"""
232
+ """update the alive timimg"""
200
233
  self.next_alive = time.time() + self.alive_period
201
234
 
202
- def get_timeout(self):
203
- """ return Alive timeout used for isAlive msg"""
235
+ def get_timeout(self) -> int:
236
+ """return Alive timeout used for isAlive msg"""
204
237
  return 2 * self.alive_period
205
238
 
206
239
  #####################################################
@@ -210,46 +243,56 @@ class Device(object):
210
243
  print("= Device: %s" % self)
211
244
  # info & description
212
245
  r = []
213
- r.append(['dev_type',self.dev_type])
214
- r.append(['address',self.address])
215
- for k,v in self._get_description().items():
216
- r.append([k,v])
217
- print(tabulate(r,tablefmt="fancy_grid"))
246
+ r.append(['dev_type', self.dev_type])
247
+ r.append(['address', self.address])
248
+ for k, v in self._get_description().items():
249
+ r.append([k, v])
250
+ print(tabulate(r, tablefmt='fancy_grid'))
218
251
 
219
252
  # attributes
220
253
  if len(self._get_attributes()) > 0:
221
254
  r = []
222
- for k,v in self._get_attributes().items():
223
- r.append([k,str(v)])
224
- print(tabulate(r,tablefmt="fancy_grid"))
255
+ for k, v in self._get_attributes().items():
256
+ r.append([k, str(v)])
257
+ print(tabulate(r, tablefmt='fancy_grid'))
225
258
 
226
259
  # methods
227
260
  if len(self.methods) > 0:
228
261
  r = []
229
- for k,v in self.methods.items():
230
- r.append([k,v.__name__])
231
- print(tabulate(r,tablefmt="fancy_grid"))
232
-
262
+ for k, v in self.methods.items():
263
+ r.append([k, v.__name__])
264
+ print(tabulate(r, tablefmt='fancy_grid'))
233
265
 
234
- def __repr__(self):
266
+ def __repr__(self) -> str:
235
267
  return f"<xaal.Device {id(self):x} {self.address} {self.dev_type}>"
236
268
 
237
269
  #####################################################
238
270
  # default public methods
239
271
  #####################################################
240
- def _get_description(self):
272
+ def _get_description(self) -> dict:
241
273
  result = {}
242
- if self.vendor_id: result['vendor_id'] = self.vendor_id
243
- if self.product_id: result['product_id'] = self.product_id
244
- if self.version: result['version'] = self.version
245
- if self.url: result['url'] = self.url
246
- if self.schema: result['schema'] = self.schema
247
- if self.info: result['info'] = self.info
248
- if self.hw_id: result['hw_id'] = self.hw_id
249
- if self.group_id: result['group_id'] = self.group_id
250
- if self.unsupported_methods: result['unsupported_methods'] = self.unsupported_methods
251
- if self.unsupported_notifications: result['unsupported_notifications'] = self.unsupported_notifications
252
- if self.unsupported_attributes: result['unsupported_attributes'] = self.unsupported_attributes
274
+ if self.vendor_id:
275
+ result['vendor_id'] = self.vendor_id
276
+ if self.product_id:
277
+ result['product_id'] = self.product_id
278
+ if self.version:
279
+ result['version'] = self.version
280
+ if self.url:
281
+ result['url'] = self.url
282
+ if self.schema:
283
+ result['schema'] = self.schema
284
+ if self.info:
285
+ result['info'] = self.info
286
+ if self.hw_id:
287
+ result['hw_id'] = self.hw_id
288
+ if self.group_id:
289
+ result['group_id'] = self.group_id
290
+ if self.unsupported_methods:
291
+ result['unsupported_methods'] = self.unsupported_methods
292
+ if self.unsupported_notifications:
293
+ result['unsupported_notifications'] = self.unsupported_notifications
294
+ if self.unsupported_attributes:
295
+ result['unsupported_attributes'] = self.unsupported_attributes
253
296
  return result
254
297
 
255
298
  def _get_attributes(self, _attributes=None):
@@ -279,7 +322,7 @@ class Device(object):
279
322
  result.update({attr.name: attr.value})
280
323
  return result
281
324
 
282
- def send_notification(self,notification,body={}):
283
- """ queue an notification, this is just a method helper """
325
+ def send_notification(self, notification: str, body: dict = {}):
326
+ """queue an notification, this is just a method helper"""
284
327
  if self.engine:
285
- self.engine.send_notification(self,notification,body)
328
+ self.engine.send_notification(self, notification, body)
xaal/lib/engine.py CHANGED
@@ -18,33 +18,44 @@
18
18
  # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
19
  #
20
20
 
21
- from . import core
22
- from .network import NetworkConnector
23
- from .exceptions import *
24
- from . import config
25
-
26
- import time
27
21
  import collections
22
+ import logging
23
+ import time
24
+ import typing
28
25
  from enum import Enum
26
+ from typing import Optional
27
+
28
+
29
+
30
+ from .config import config
31
+ from . import core
32
+ from .exceptions import CallbackError, MessageParserError, XAALError
33
+ from .network import NetworkConnector
34
+
35
+ if typing.TYPE_CHECKING:
36
+ from .devices import Device
37
+ from .messages import Message
38
+
29
39
 
30
- import logging
31
40
  logger = logging.getLogger(__name__)
32
41
 
33
- class EngineState(Enum):
34
- started = 1
35
- running = 2
36
- halted = 3
37
42
 
38
- class Engine(core.EngineMixin):
43
+ class EngineState(Enum):
44
+ started = 1
45
+ running = 2
46
+ halted = 3
39
47
 
40
- __slots__ = ['__last_timer','__txFifo','state','network']
41
48
 
42
- def __init__(self,address=config.address,port=config.port,hops=config.hops,key=config.key):
43
- core.EngineMixin.__init__(self,address,port,hops,key)
49
+ class Engine(core.EngineMixin):
50
+ __slots__ = ['__last_timer', '__txFifo', 'state', 'network']
44
51
 
45
- self.__last_timer = 0 # last timer check
46
- self.__txFifo = collections.deque() # tx msg fifo
52
+ def __init__(
53
+ self, address: str = config.address, port: int = config.port, hops: int = config.hops, key: bytes = config.key
54
+ ):
55
+ core.EngineMixin.__init__(self, address, port, hops, key)
47
56
 
57
+ self.__last_timer = 0 # last timer check
58
+ self.__txFifo = collections.deque() # tx msg fifo
48
59
  # message receive workflow
49
60
  self.subscribe(self.handle_request)
50
61
  # ready to go
@@ -56,16 +67,16 @@ class Engine(core.EngineMixin):
56
67
  # xAAL messages Tx handling
57
68
  #####################################################
58
69
  # Fifo for msg to send
59
- def queue_msg(self, msg):
70
+ def queue_msg(self, msg: bytes):
60
71
  """queue an encoded / cyphered message"""
61
72
  self.__txFifo.append(msg)
62
73
 
63
- def send_msg(self, msg):
74
+ def send_msg(self, msg: bytes):
64
75
  """Send an encoded message to the bus, use queue_msg instead"""
65
76
  self.network.send(msg)
66
77
 
67
78
  def process_tx_msg(self):
68
- """ Process (send) message in tx queue called from the loop()"""
79
+ """Process (send) message in tx queue called from the loop()"""
69
80
  cnt = 0
70
81
  while self.__txFifo:
71
82
  temp = self.__txFifo.popleft()
@@ -79,13 +90,13 @@ class Engine(core.EngineMixin):
79
90
  #####################################################
80
91
  # xAAL messages subscribers
81
92
  #####################################################
82
- def receive_msg(self):
93
+ def receive_msg(self) -> Optional['Message']:
83
94
  """return new received message or None"""
84
95
  result = None
85
96
  data = self.network.get_data()
86
97
  if data:
87
98
  try:
88
- msg = self.msg_factory.decode_msg(data,self.msg_filter)
99
+ msg = self.msg_factory.decode_msg(data, self.msg_filter)
89
100
  except MessageParserError as e:
90
101
  logger.warning(e)
91
102
  msg = None
@@ -99,35 +110,37 @@ class Engine(core.EngineMixin):
99
110
  for func in self.subscribers:
100
111
  func(msg)
101
112
  self.process_attributes_change()
102
-
103
- def handle_request(self, msg):
113
+
114
+ def handle_request(self, msg: 'Message'):
104
115
  """
105
116
  Filter msg for devices according default xAAL API then process the
106
117
  request for each targets identied in the engine
107
118
  """
108
119
  if not msg.is_request():
109
- return
110
-
120
+ return
121
+
111
122
  targets = core.filter_msg_for_devices(msg, self.devices)
112
123
  for target in targets:
113
- if msg.action == 'is_alive':
124
+ if msg.is_request_isalive():
114
125
  self.send_alive(target)
115
126
  else:
116
127
  self.handle_action_request(msg, target)
117
128
 
118
- def handle_action_request(self, msg, target):
129
+ def handle_action_request(self, msg: 'Message', target: 'Device'):
119
130
  """
120
131
  Run method (xAAL exposed method) on device:
121
- - None is returned if device method do not return anything
122
132
  - result is returned if device method gives a response
123
133
  - Errors are raised if an error occured:
124
134
  * Internal error
125
135
  * error returned on the xAAL bus
126
136
  """
137
+ if msg.action is None:
138
+ return # should not happen, but pyright need this check
139
+
127
140
  try:
128
141
  result = run_action(msg, target)
129
- if result != None:
130
- self.send_reply(dev=target,targets=[msg.source],action=msg.action,body=result)
142
+ if result:
143
+ self.send_reply(dev=target, targets=[msg.source], action=msg.action, body=result)
131
144
  except CallbackError as e:
132
145
  self.send_error(target, e.code, e.description)
133
146
  except XAALError as e:
@@ -139,12 +152,13 @@ class Engine(core.EngineMixin):
139
152
  def process_timers(self):
140
153
  """Process all timers to find out which ones should be run"""
141
154
  expire_list = []
142
-
143
- if len(self.timers)!=0 :
155
+
156
+ if len(self.timers) != 0:
144
157
  now = time.time()
145
158
  # little hack to avoid to check timer to often.
146
159
  # w/ this enable timer precision is bad, but far enougth
147
- if (now - self.__last_timer) < 0.4: return
160
+ if (now - self.__last_timer) < 0.4:
161
+ return
148
162
 
149
163
  for t in self.timers:
150
164
  if t.deadline < now:
@@ -153,14 +167,14 @@ class Engine(core.EngineMixin):
153
167
  except CallbackError as e:
154
168
  logger.error(e.description)
155
169
  if t.counter != -1:
156
- t.counter-= 1
170
+ t.counter -= 1
157
171
  if t.counter == 0:
158
172
  expire_list.append(t)
159
173
  t.deadline = now + t.period
160
174
  # delete expired timers
161
175
  for t in expire_list:
162
176
  self.remove_timer(t)
163
-
177
+
164
178
  self.__last_timer = now
165
179
 
166
180
  #####################################################
@@ -187,7 +201,7 @@ class Engine(core.EngineMixin):
187
201
 
188
202
  def start(self):
189
203
  """Start the core engine: send queue alive msg"""
190
- if self.state in [EngineState.started,EngineState.running]:
204
+ if self.state in [EngineState.started, EngineState.running]:
191
205
  return
192
206
  self.network.connect()
193
207
  for dev in self.devices:
@@ -206,13 +220,14 @@ class Engine(core.EngineMixin):
206
220
  while self.state == EngineState.running:
207
221
  self.loop()
208
222
 
209
- def is_running(self):
223
+ def is_running(self) -> bool:
210
224
  if self.state == EngineState.running:
211
225
  return True
212
226
  return False
213
227
 
214
- def run_action(msg,device):
215
- """
228
+
229
+ def run_action(msg: 'Message', device: 'Device') -> Optional[dict]:
230
+ """
216
231
  Extract an action & launch it
217
232
  Return:
218
233
  - action result
@@ -220,12 +235,15 @@ def run_action(msg,device):
220
235
 
221
236
  Note: If an exception raised, it's logged, and raise an XAALError.
222
237
  """
223
- method,params = core.search_action(msg,device)
238
+ method, params = core.search_action(msg, device)
224
239
  result = None
225
240
  try:
226
241
  result = method(**params)
227
242
  except Exception as e:
228
243
  logger.error(e)
229
- raise XAALError("Error in method:%s params:%s" % (msg.action,params))
244
+ raise XAALError("Error in method:%s params:%s" % (msg.action, params))
245
+ # Here result should be None or a dict, and we need to enforce that. This will cause issue
246
+ # in send_reply otherwise.
247
+ if result is not None and not isinstance(result, dict):
248
+ raise XAALError("Method %s should return a dict or None" % msg.action)
230
249
  return result
231
-
xaal/lib/exceptions.py CHANGED
@@ -1,20 +1,36 @@
1
-
2
1
  # devices.py
3
- class DeviceError(Exception):pass
2
+ class DeviceError(Exception):
3
+ pass
4
4
 
5
5
  # core.py
6
- class EngineError(Exception):pass
7
- class XAALError(Exception):pass
6
+ class EngineError(Exception):
7
+ pass
8
+
9
+ class XAALError(Exception):
10
+ pass
11
+
8
12
  class CallbackError(Exception):
9
13
  def __init__(self, code, desc):
10
14
  self.code = code
11
15
  self.description = desc
12
16
 
13
17
  # messages.py
14
- class MessageParserError(Exception):pass
15
- class MessageError(Exception):pass
18
+ class MessageParserError(Exception):
19
+ pass
20
+
21
+ class MessageError(Exception):
22
+ pass
16
23
 
17
24
  # binding.py
18
- class UUIDError(Exception):pass
25
+ class UUIDError(Exception):
26
+ pass
19
27
 
20
- __all__ = ["DeviceError","EngineError","XAALError","CallbackError","MessageParserError","MessageError","UUIDError"]
28
+ __all__ = [
29
+ 'DeviceError',
30
+ 'EngineError',
31
+ 'XAALError',
32
+ 'CallbackError',
33
+ 'MessageParserError',
34
+ 'MessageError',
35
+ 'UUIDError',
36
+ ]