1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
"""Presentation of an API method."""
import json
import logging
from pprint import pformat as pf
from typing import Dict, Optional, Set, Union
import attr
from .common import SongpalException
_LOGGER = logging.getLogger(__name__)
@attr.s
class MethodSignature:
"""Method signature."""
@staticmethod
def return_type(x: str) -> Union[type, str]:
"""Return a python type for a string presentation if possible."""
if x == "string":
return str
if x == "Boolean":
return bool
if x == "int":
return int
return x
@staticmethod
def parse_json_types(x) -> Union[type, str, Dict[str, type]]:
"""Parse JSON signature. Used to parse input and output parameters."""
try:
if x.endswith("*"): # TODO handle arrays properly
# _LOGGER.debug("got an array %s: %s" % (self.name, x))
x = x.rstrip("*")
obj = json.loads(x)
obj = {x: MethodSignature.return_type(obj[x]) for x in obj}
except json.JSONDecodeError as ex:
try:
return MethodSignature.return_type(x)
except Exception:
raise SongpalException("Unknown return type: %s" % x) from ex
return obj
@staticmethod
def from_payload(name, inputs, outputs, version):
"""Construct a method signature."""
ins = None
outs = None
if len(inputs) != 0:
ins = MethodSignature.parse_json_types(inputs.pop())
if len(outputs) != 0:
outs = MethodSignature.parse_json_types(outputs.pop())
return MethodSignature(name=name, input=ins, output=outs, version=version)
def _serialize_types(self, x):
"""Convert type to string."""
if x is None:
return x
def serialize(x):
if isinstance(x, str):
return x
return x.__name__
if isinstance(x, dict):
serialized_dict = {k: serialize(v) for k, v in x.items()}
return serialized_dict
serialized = serialize(x)
return serialized
def serialize(self):
"""Serialize the signature for JSON output."""
return {
"name": self.name,
"input": self._serialize_types(self.input),
"output": self._serialize_types(self.output),
"version": self.version,
}
name = attr.ib()
input = attr.ib()
output = attr.ib()
version = attr.ib()
class Method:
"""A Method (int. API) represents a single API method.
This class implements __call__() for calling the method, which can be used to
invoke the method.
"""
def __init__(self, service, signature: MethodSignature, debug=0):
"""Construct a method."""
self._supported_versions: Set[str] = set()
self.signatures: Dict[str, MethodSignature] = {}
self.name = signature.name
self.service = service
self.signatures[signature.version] = signature
self.debug = debug
self._version = signature.version
def asdict(self) -> Dict[str, Union[Dict, Union[str, Dict]]]:
"""Return a dictionary describing the method.
This can be used to dump the information into a JSON file.
"""
return {
"service": self.service.name,
**self.signatures[self._version].serialize(),
}
async def __call__(self, *args, **kwargs):
"""Call the method with given parameters.
On error this call will raise a :class:SongpalException:. If the error is
reported by the device (e.g. not a problem doing the request), the exception
will contain `error` attribute containing the device-reported error message.
"""
try:
res = await self.service.call_method(self, *args, **kwargs)
except Exception as ex:
raise SongpalException("Unable to make a request: %s" % ex) from ex
if self.debug > 1:
_LOGGER.debug("got payload: %s", res)
if "error" in res:
_LOGGER.debug(self)
raise SongpalException(
"Got an error for {}: {}".format(self.name, res["error"]),
error=res["error"],
)
if self.debug > 0:
_LOGGER.debug("got res: %s", pf(res))
if "result" not in res:
_LOGGER.error("No result in response, how to handle? %s", res)
return
res = res["result"]
if len(res) > 1:
_LOGGER.warning("Got a response with len > 1: %s", res)
return res
elif len(res) < 1:
_LOGGER.debug("Got no response, assuming success")
return True
return res[0]
@property
def inputs(self) -> Dict[str, type]:
"""Input parameters for this method version."""
return self.signatures[self._version].input or {}
@property
def latest_supported_version(self) -> Optional[str]:
"""Latest version supported by this method."""
return max(self._supported_versions) if self._supported_versions else None
@property
def outputs(self) -> Dict[str, type]:
"""Output parameters for this method version."""
return self.signatures[self._version].output or {}
@property
def supported_versions(self) -> Set[str]:
"""List of supported version numbers for this method."""
return self._supported_versions
@property
def version(self) -> str:
"""Method signature version number."""
return self._version
def add_supported_version(self, version: str):
"""Add a supported version number for this method."""
self._supported_versions.add(version)
def supports_version(self, version: str) -> bool:
"""Is this method version supported."""
return version in self._supported_versions
def use_version(self, version: str):
"""Specify method signature version to use."""
self._version = version
def __repr__(self):
return "<Method {}.{}({}) -> {} version {}>".format(
self.service.name,
self.name,
pf(self.inputs),
pf(self.outputs),
self.version,
)
|