File: method.py

package info (click to toggle)
python-songpal 0.16.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,516 kB
  • sloc: python: 2,097; makefile: 7
file content (202 lines) | stat: -rw-r--r-- 6,323 bytes parent folder | download
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,
        )