Refactor message classes

Message classes allow dynamic attribute access to message fields. Childs
get exposed through a dedicated class by attributes too. Message type
fields and childs have types that get enforces. None type is allowed
too while transitioning.
This commit is contained in:
clerie 2022-05-01 22:32:08 +02:00
parent 1752a9151d
commit cf3c16c66a
10 changed files with 215 additions and 221 deletions

View File

@ -20,6 +20,14 @@ mitel\_ommclient2.messages.getppdev module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
mitel\_ommclient2.messages.getppuser module
-------------------------------------------
.. automodule:: mitel_ommclient2.messages.getppuser
:members:
:undoc-members:
:show-inheritance:
mitel\_ommclient2.messages.open module mitel\_ommclient2.messages.open module
-------------------------------------- --------------------------------------

View File

@ -15,7 +15,7 @@ to establish a transport to the API.
import mitel_ommclient2 import mitel_ommclient2
conn = mitel_ommclient2..connection.Connection("omm.local") conn = mitel_ommclient2.connection.Connection("omm.local")
To actually connect to the OMM, you need to call :func:`mitel_ommclient2.connection.Connection.connect`. To actually connect to the OMM, you need to call :func:`mitel_ommclient2.connection.Connection.connect`.
@ -45,8 +45,8 @@ You hand over a Request object and receive a response object.
.. code-block:: python .. code-block:: python
>>> request = mitel_ommclient2.messages.Ping() >>> m = mitel_ommclient2.messages.Ping()
>>> r = conn.request(request) >>> r = conn.request(m)
>>> r.name >>> r.name
'PingResp' 'PingResp'

View File

@ -5,14 +5,13 @@ The API consists of three main message types: request, response and event. They
are represented by :class:`mitel_ommclient2.messages.Request`, :class:`mitel_ommclient2.messages.Response` are represented by :class:`mitel_ommclient2.messages.Request`, :class:`mitel_ommclient2.messages.Response`
and events aren't supported yet. and events aren't supported yet.
There are several subclasses for each messages type, but they are just just overlays There are several subclasses for each messages type, which provide a conveinient
to provide a conveinient interface to message content using attributes. interface to message content using attributes.
Each message provides three attributes that define the central interfaces: For each message you can access each field directly as class attributes.
There are two special attributes:
* name * name: returns the message name
* attrs * childs: allowes you to access childs by the child name as class attributes
* childs
Using messages Using messages
-------------- --------------
@ -27,27 +26,20 @@ and hand it over to :func:`mitel_ommclient2.client.OMMClient2.request` or
my_time = int(time.time()) my_time = int(time.time())
request = mitel_ommclient2.messages.Ping(timeStamp=my_time) m = mitel_ommclient2.messages.Ping()
r = c.request(request) m.timeStamp = my_time
r = c.request(m)
ping = r.timeStamp - my_time ping = r.timeStamp - my_time
Crafting your own messages A more complex example
-------------------------- ----------------------
It some cases your message class isn't implemented yet. Then you can craft the This demonstrates how to access message childs.
message yourself.
.. code-block:: python .. code-block:: python
import time m = messages.GetAccount()
m.id = id
my_time = int(time.time()) r = self.connection.request(m)
return r.childs.account[0]
request = mitel_ommclient2.messages.Request("Ping")
request.attrs = {
"timeStamp": my_time,
}
r = c.request(request)
ping = r.attrs["timeStamp"] - my_time

View File

@ -44,7 +44,11 @@ class OMMClient2:
self.connection.connect() self.connection.connect()
# Login # Login
r = self.connection.request(messages.Open(self._username, self._password, UserDeviceSyncClient=self._ommsync)) m = messages.Open()
m.username = self._username
m.password = self._password
m.UserDeviceSyncClient = self._ommsync
r = self.connection.request(m)
r.raise_on_error() r.raise_on_error()
def get_account(self, id): def get_account(self, id):
@ -54,11 +58,13 @@ class OMMClient2:
:param id: User id :param id: User id
""" """
r = self.connection.request(messages.GetAccount(id)) m = messages.GetAccount()
m.id = id
r = self.connection.request(m)
r.raise_on_error() r.raise_on_error()
if r.account is None: if r.childs.account is None:
return None return None
return r.account[0] return r.childs.account[0]
def get_device(self, ppn): def get_device(self, ppn):
""" """
@ -66,11 +72,14 @@ class OMMClient2:
:param ppn: Device id :param ppn: Device id
""" """
r = self.connection.request(messages.GetPPDev(ppn))
m = messages.GetPPDev()
m.ppn = ppn
r = self.connection.request(m)
r.raise_on_error() r.raise_on_error()
if r.pp is None: if r.childs.pp is None:
return None return None
return r.pp[0] return r.childs.pp[0]
def get_devices(self): def get_devices(self):
""" """
@ -78,7 +87,10 @@ class OMMClient2:
""" """
next_ppn = 0 next_ppn = 0
while True: while True:
r = self.connection.request(messages.GetPPDev(next_ppn, maxRecords=20)) m = messages.GetPPDev()
m.ppn = next_ppn
m.maxRecords = 20
r = self.connection.request(m)
try: try:
r.raise_on_error() r.raise_on_error()
except exceptions.ENoEnt: except exceptions.ENoEnt:
@ -86,7 +98,7 @@ class OMMClient2:
break break
# Output all found devices # Output all found devices
for pp in r.pp: for pp in r.childs.pp:
yield pp yield pp
# Determine next possible ppn # Determine next possible ppn
@ -98,11 +110,13 @@ class OMMClient2:
:param uid: User id :param uid: User id
""" """
r = self.connection.request(messages.GetPPUser(uid)) m = messages.GetPPUser()
m.uid = uid
r = self.connection.request(m)
r.raise_on_error() r.raise_on_error()
if r.user is None: if r.childs.user is None:
return None return None
return r.user[0] return r.childs.user[0]
def get_users(self): def get_users(self):
""" """
@ -110,7 +124,10 @@ class OMMClient2:
""" """
next_uid = 0 next_uid = 0
while True: while True:
r = self.connection.request(messages.GetPPUser(next_uid, maxRecords=20)) m = messages.GetPPUser()
m.uid = next_uid
m.maxRecords = 20
r = self.connection.request(m)
try: try:
r.raise_on_error() r.raise_on_error()
except exceptions.ENoEnt: except exceptions.ENoEnt:
@ -118,7 +135,7 @@ class OMMClient2:
break break
# Output all found devices # Output all found devices
for user in r.user: for user in r.childs.user:
yield user yield user
# Determine next possible ppn # Determine next possible ppn

View File

@ -5,45 +5,98 @@ from xml.dom.minidom import getDOMImplementation, parseString
from ..exceptions import exception_classes, OMResponseException from ..exceptions import exception_classes, OMResponseException
class Request: class Message:
""" """
Request message class Base message class
:param name: Name of the message
:param seq: Unique sequence number to associate responses
Usage::
>>> req = Request("Ping")
"""
def __init__(self, name, seq=None):
self.name = name
self.attrs = {}
self.childs = {}
if seq is not None:
self.attrs["seq"] = seq
@property
def seq(self):
return self.attrs.get("seq")
@seq.setter
def seq(self, seq):
self.attrs["seq"] = seq
class Response:
"""
Response message class
:param name: Name of the message :param name: Name of the message
:param attrs: Message attributes :param attrs: Message attributes
:param childs: Message children :param childs: Message children
""" """
def __init__(self, name, attrs={}, childs={}):
# Fields defined by the base type class
BASE_FIELDS = {}
# Fields defined by subclasses
FIELDS = {}
# Child types
CHILDS = {}
# Fields dicts consist of the field name as name and the field type as value
# Use None if the field type is unknown, any type is allowed then
class Childs:
"""
Contains message childs
"""
CHILDS = {}
def __init__(self, child_types, child_dict):
self.CHILDS = child_types
self._child_dict = child_dict
def __getattr__(self, name):
if name in self.CHILDS.keys():
return self._child_dict.get(name)
else:
raise AttributeError()
def __setattr__(self, name, value):
if name in self.CHILDS.keys():
if self.CHILDS[name] is not None and type(value) != self.CHILDS[name]:
raise TypeError()
self._child_dict[name] = value
else:
object.__setattr__(self, name, value)
def __init__(self, name=None, attrs={}, childs={}):
self.name = name self.name = name
self.attrs = attrs if not self.name:
self.childs = childs self.name = self.__class__.__name__
self._attrs = attrs
self._childs = childs
self.childs = self.Childs(self.CHILDS, self._childs)
def __getattr__(self, name):
fields = self.FIELDS | self.BASE_FIELDS
if name in fields.keys():
return self._attrs.get(name)
else:
raise AttributeError()
def __setattr__(self, name, value):
fields = self.FIELDS | self.BASE_FIELDS
if name in fields.keys():
if fields[name] is not None and type(value) != fields[name]:
raise TypeError()
self._attrs[name] = value
else:
object.__setattr__(self, name, value)
class Request(Message):
"""
Request message type class
"""
BASE_FIELDS = {
"seq": int,
}
class Response(Message):
"""
Response message type class
"""
BASE_FIELDS = {
"seq": int,
"errCode": None,
"info": None,
"bad": None,
"maxLen": None,
}
def raise_on_error(self): def raise_on_error(self):
""" """
@ -62,25 +115,17 @@ class Response:
if self.errCode is not None: if self.errCode is not None:
raise exception_classes.get(self.errCode, OMResponseException)(response=self) raise exception_classes.get(self.errCode, OMResponseException)(response=self)
@property
def seq(self):
return int(self.attrs.get("seq"))
@property REQUEST_TYPES = {}
def errCode(self): RESPONSE_TYPES = {}
return self.attrs.get("errCode")
@property def request_type(c):
def info(self): REQUEST_TYPES[c.__name__] = c
return self.attrs.get("info") return c
@property def response_type(c):
def bad(self): RESPONSE_TYPES[c.__name__] = c
return self.attrs.get("bad") return c
@property
def maxLen(self):
return self.attrs.get("maxLen")
from .getaccount import GetAccount, GetAccountResp from .getaccount import GetAccount, GetAccountResp
from .getppdev import GetPPDev, GetPPDevResp from .getppdev import GetPPDev, GetPPDevResp
@ -96,11 +141,11 @@ def construct(request):
message = impl.createDocument(None, request.name, None) message = impl.createDocument(None, request.name, None)
root = message.documentElement root = message.documentElement
for k, v in request.attrs.items(): for k, v in request._attrs.items():
root.setAttribute(str(k), str(v)) root.setAttribute(str(k), str(v))
for k, v in request.childs.items(): for k, v in request._childs.items():
child = message.createElement(k) child = message.createElement(k)
if v is not None: if v is not None:
for c_k, c_v in v.items(): for c_k, c_v in v.items():
@ -108,18 +153,6 @@ def construct(request):
root.appendChild(child) root.appendChild(child)
return root.toxml() return root.toxml()
def _response_type_by_name(name):
response_types = [
GetAccountResp,
GetPPDevResp,
GetPPUserResp,
PingResp,
]
response_types_dict = {r.__name__: r for r in response_types}
return response_types_dict.get(name, Response)
def parse(message): def parse(message):
message = parseString(message) message = parseString(message)
root = message.documentElement root = message.documentElement
@ -128,8 +161,14 @@ def parse(message):
attrs = {} attrs = {}
childs = {} childs = {}
response_type = RESPONSE_TYPES.get(name)
fields = response_type.FIELDS | response_type.BASE_FIELDS
for i in range(0, root.attributes.length): for i in range(0, root.attributes.length):
item = root.attributes.item(i) item = root.attributes.item(i)
if fields.get(item.name) is not None:
attrs[item.name] = fields[item.name](item.value)
else:
attrs[item.name] = item.value attrs[item.name] = item.value
child = root.firstChild child = root.firstChild
@ -148,4 +187,4 @@ def parse(message):
child = child.nextSibling child = child.nextSibling
return _response_type_by_name(name)(name, attrs, childs) return response_type(name, attrs, childs)

View File

@ -1,27 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from . import Request, Response from . import Request, Response, request_type, response_type
@request_type
class GetAccount(Request): class GetAccount(Request):
def __init__(self, id, maxRecords=None, **kwargs): FIELDS = {
super().__init__("GetAccount", **kwargs) "id": int,
"maxRecords": int,
self.attrs["id"] = id }
if maxRecords is not None:
self.attrs["maxRecords"] = maxRecords
@property
def id(self):
return self.attrs.get("id")
@property
def maxRecords(self):
return self.attrs.get("maxRecords")
@response_type
class GetAccountResp(Response): class GetAccountResp(Response):
@property CHILDS = {
def account(self): "account": None,
return self.childs.get("account") }

View File

@ -1,27 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from . import Request, Response from . import Request, Response, request_type, response_type
@request_type
class GetPPDev(Request): class GetPPDev(Request):
def __init__(self, ppn, maxRecords=None, **kwargs): FIELDS = {
super().__init__("GetPPDev", **kwargs) "ppn": int,
"maxRecords": int,
self.attrs["ppn"] = ppn }
if maxRecords is not None:
self.attrs["maxRecords"] = maxRecords
@property
def ppn(self):
return self.attrs.get("ppn")
@property
def maxRecords(self):
return self.attrs.get("maxRecords")
@response_type
class GetPPDevResp(Response): class GetPPDevResp(Response):
@property CHILDS = {
def pp(self): "pp": None,
return self.childs.get("pp") }

View File

@ -1,27 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from . import Request, Response from . import Request, Response, request_type, response_type
@request_type
class GetPPUser(Request): class GetPPUser(Request):
def __init__(self, uid, maxRecords=None, **kwargs): FIELDS = {
super().__init__("GetPPUser", **kwargs) "uid": int,
"maxRecords": int,
self.attrs["uid"] = uid }
if maxRecords is not None:
self.attrs["maxRecords"] = maxRecords
@property
def ppn(self):
return self.attrs.get("ppn")
@property
def maxRecords(self):
return self.attrs.get("maxRecords")
@response_type
class GetPPUserResp(Response): class GetPPUserResp(Response):
@property CHILDS = {
def user(self): "user": None,
return self.childs.get("user") }

View File

@ -1,55 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from . import Request, Response from . import Request, Response, request_type, response_type
@request_type
class Open(Request): class Open(Request):
""" FIELDS = {
Authenticate Message "username": None,
"password": None,
"UserDeviceSyncClient": None,
}
Needs to be the first message on a new connection.
:param username: Username
:param password: Password
:param UserDeviceSyncClient: If True login as OMM-Sync client. Some operations in OMM-Sync mode might lead to destroy DECT paring.
"""
def __init__(self, username, password, UserDeviceSyncClient=False, **kwargs):
super().__init__("Open", **kwargs)
self.attrs["username"] = username
self.attrs["password"] = password
if UserDeviceSyncClient:
self.attrs["UserDeviceSyncClient"] = "true"
@property
def username(self):
return self.attrs.get("username")
@property
def password(self):
return self.attrs.get("password")
@property
def UserDeviceSyncClient(self):
return self.attrs.get("UserDeviceSyncClient")
@response_type
class OpenResp(Response): class OpenResp(Response):
@property FIELDS = {
def protocolVersion(self): "protocolVersion": None,
return self.attrs.get("protocolVersion") "minPPSwVersion1": None,
"minPPSwVersion2": None,
@property "ommStbState": None,
def minPPSwVersion1(self): "publicKey": None,
return self.attrs.get("minPPSwVersion1") }
@property
def minPPSwVersion2(self):
return self.attrs.get("minPPSwVersion2")
@property
def ommStbState(self):
return self.attrs.get("ommStbState")
@property
def publicKey(self):
return self.attrs.get("publicKey")

View File

@ -1,20 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from . import Request, Response from . import Request, Response, request_type, response_type
@request_type
class Ping(Request): class Ping(Request):
def __init__(self, timeStamp=None, **kwargs): FIELDS = {
super().__init__("Ping", **kwargs) "timeStamp": int,
}
if timeStamp is not None:
self.attrs["timeStamp"] = timeStamp
@property
def timeStamp(self):
return self.attrs.get("timeStamp")
@response_type
class PingResp(Response): class PingResp(Response):
@property FIELDS = {
def timeStamp(self): "timeStamp": int,
return self.attrs.get("timeStamp") }