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:
:show-inheritance:
mitel\_ommclient2.messages.getppuser module
-------------------------------------------
.. automodule:: mitel_ommclient2.messages.getppuser
:members:
:undoc-members:
:show-inheritance:
mitel\_ommclient2.messages.open module
--------------------------------------

View File

@ -15,7 +15,7 @@ to establish a transport to the API.
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`.
@ -45,8 +45,8 @@ You hand over a Request object and receive a response object.
.. code-block:: python
>>> request = mitel_ommclient2.messages.Ping()
>>> r = conn.request(request)
>>> m = mitel_ommclient2.messages.Ping()
>>> r = conn.request(m)
>>> r.name
'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`
and events aren't supported yet.
There are several subclasses for each messages type, but they are just just overlays
to provide a conveinient interface to message content using attributes.
There are several subclasses for each messages type, which provide a conveinient
interface to message content using attributes.
Each message provides three attributes that define the central interfaces:
* name
* attrs
* childs
For each message you can access each field directly as class attributes.
There are two special attributes:
* name: returns the message name
* childs: allowes you to access childs by the child name as class attributes
Using messages
--------------
@ -27,27 +26,20 @@ and hand it over to :func:`mitel_ommclient2.client.OMMClient2.request` or
my_time = int(time.time())
request = mitel_ommclient2.messages.Ping(timeStamp=my_time)
r = c.request(request)
m = mitel_ommclient2.messages.Ping()
m.timeStamp = my_time
r = c.request(m)
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
message yourself.
This demonstrates how to access message childs.
.. code-block:: python
import time
my_time = int(time.time())
request = mitel_ommclient2.messages.Request("Ping")
request.attrs = {
"timeStamp": my_time,
}
r = c.request(request)
ping = r.attrs["timeStamp"] - my_time
m = messages.GetAccount()
m.id = id
r = self.connection.request(m)
return r.childs.account[0]

View File

@ -44,7 +44,11 @@ class OMMClient2:
self.connection.connect()
# 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()
def get_account(self, id):
@ -54,11 +58,13 @@ class OMMClient2:
: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()
if r.account is None:
if r.childs.account is None:
return None
return r.account[0]
return r.childs.account[0]
def get_device(self, ppn):
"""
@ -66,11 +72,14 @@ class OMMClient2:
: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()
if r.pp is None:
if r.childs.pp is None:
return None
return r.pp[0]
return r.childs.pp[0]
def get_devices(self):
"""
@ -78,7 +87,10 @@ class OMMClient2:
"""
next_ppn = 0
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:
r.raise_on_error()
except exceptions.ENoEnt:
@ -86,7 +98,7 @@ class OMMClient2:
break
# Output all found devices
for pp in r.pp:
for pp in r.childs.pp:
yield pp
# Determine next possible ppn
@ -98,11 +110,13 @@ class OMMClient2:
: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()
if r.user is None:
if r.childs.user is None:
return None
return r.user[0]
return r.childs.user[0]
def get_users(self):
"""
@ -110,7 +124,10 @@ class OMMClient2:
"""
next_uid = 0
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:
r.raise_on_error()
except exceptions.ENoEnt:
@ -118,7 +135,7 @@ class OMMClient2:
break
# Output all found devices
for user in r.user:
for user in r.childs.user:
yield user
# Determine next possible ppn

View File

@ -5,45 +5,98 @@ from xml.dom.minidom import getDOMImplementation, parseString
from ..exceptions import exception_classes, OMResponseException
class Request:
class Message:
"""
Request 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
Base message class
:param name: Name of the message
:param attrs: Message attributes
: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.attrs = attrs
self.childs = childs
if not self.name:
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):
"""
@ -62,25 +115,17 @@ class Response:
if self.errCode is not None:
raise exception_classes.get(self.errCode, OMResponseException)(response=self)
@property
def seq(self):
return int(self.attrs.get("seq"))
@property
def errCode(self):
return self.attrs.get("errCode")
REQUEST_TYPES = {}
RESPONSE_TYPES = {}
@property
def info(self):
return self.attrs.get("info")
def request_type(c):
REQUEST_TYPES[c.__name__] = c
return c
@property
def bad(self):
return self.attrs.get("bad")
@property
def maxLen(self):
return self.attrs.get("maxLen")
def response_type(c):
RESPONSE_TYPES[c.__name__] = c
return c
from .getaccount import GetAccount, GetAccountResp
from .getppdev import GetPPDev, GetPPDevResp
@ -96,11 +141,11 @@ def construct(request):
message = impl.createDocument(None, request.name, None)
root = message.documentElement
for k, v in request.attrs.items():
for k, v in request._attrs.items():
root.setAttribute(str(k), str(v))
for k, v in request.childs.items():
for k, v in request._childs.items():
child = message.createElement(k)
if v is not None:
for c_k, c_v in v.items():
@ -108,18 +153,6 @@ def construct(request):
root.appendChild(child)
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):
message = parseString(message)
root = message.documentElement
@ -128,8 +161,14 @@ def parse(message):
attrs = {}
childs = {}
response_type = RESPONSE_TYPES.get(name)
fields = response_type.FIELDS | response_type.BASE_FIELDS
for i in range(0, root.attributes.length):
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
child = root.firstChild
@ -148,4 +187,4 @@ def parse(message):
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
from . import Request, Response
from . import Request, Response, request_type, response_type
@request_type
class GetAccount(Request):
def __init__(self, id, maxRecords=None, **kwargs):
super().__init__("GetAccount", **kwargs)
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")
FIELDS = {
"id": int,
"maxRecords": int,
}
@response_type
class GetAccountResp(Response):
@property
def account(self):
return self.childs.get("account")
CHILDS = {
"account": None,
}

View File

@ -1,27 +1,18 @@
#!/usr/bin/env python3
from . import Request, Response
from . import Request, Response, request_type, response_type
@request_type
class GetPPDev(Request):
def __init__(self, ppn, maxRecords=None, **kwargs):
super().__init__("GetPPDev", **kwargs)
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")
FIELDS = {
"ppn": int,
"maxRecords": int,
}
@response_type
class GetPPDevResp(Response):
@property
def pp(self):
return self.childs.get("pp")
CHILDS = {
"pp": None,
}

View File

@ -1,27 +1,18 @@
#!/usr/bin/env python3
from . import Request, Response
from . import Request, Response, request_type, response_type
@request_type
class GetPPUser(Request):
def __init__(self, uid, maxRecords=None, **kwargs):
super().__init__("GetPPUser", **kwargs)
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")
FIELDS = {
"uid": int,
"maxRecords": int,
}
@response_type
class GetPPUserResp(Response):
@property
def user(self):
return self.childs.get("user")
CHILDS = {
"user": None,
}

View File

@ -1,55 +1,23 @@
#!/usr/bin/env python3
from . import Request, Response
from . import Request, Response, request_type, response_type
@request_type
class Open(Request):
"""
Authenticate Message
FIELDS = {
"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):
@property
def protocolVersion(self):
return self.attrs.get("protocolVersion")
@property
def minPPSwVersion1(self):
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")
FIELDS = {
"protocolVersion": None,
"minPPSwVersion1": None,
"minPPSwVersion2": None,
"ommStbState": None,
"publicKey": None,
}

View File

@ -1,20 +1,17 @@
#!/usr/bin/env python3
from . import Request, Response
from . import Request, Response, request_type, response_type
@request_type
class Ping(Request):
def __init__(self, timeStamp=None, **kwargs):
super().__init__("Ping", **kwargs)
FIELDS = {
"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):
@property
def timeStamp(self):
return self.attrs.get("timeStamp")
FIELDS = {
"timeStamp": int,
}