|
1 | | -from __future__ import annotations |
2 | | - |
| 1 | +import dataclasses |
3 | 2 | import datetime |
4 | 3 | import json |
5 | 4 | import logging |
6 | 5 | import re |
| 6 | +import types |
7 | 7 | from dataclasses import asdict, dataclass, field |
8 | 8 | from datetime import timezone |
9 | 9 | from enum import Enum |
|
95 | 95 | _LOGGER = logging.getLogger(__name__) |
96 | 96 |
|
97 | 97 |
|
98 | | -def camelize(s: str): |
| 98 | +def _camelize(s: str): |
99 | 99 | first, *others = s.split("_") |
100 | 100 | if len(others) == 0: |
101 | 101 | return s |
102 | 102 | return "".join([first.lower(), *map(str.title, others)]) |
103 | 103 |
|
104 | 104 |
|
105 | | -def decamelize(s: str): |
| 105 | +def _decamelize(s: str): |
106 | 106 | return re.sub("([A-Z]+)", "_\\1", s).lower() |
107 | 107 |
|
108 | 108 |
|
109 | | -def decamelize_obj(d: dict | list, ignore_keys: list[str]): |
110 | | - if isinstance(d, RoborockBase): |
111 | | - d = d.as_dict() |
112 | | - if isinstance(d, list): |
113 | | - return [decamelize_obj(i, ignore_keys) if isinstance(i, dict | list) else i for i in d] |
114 | | - return { |
115 | | - (decamelize(a) if a not in ignore_keys else a): decamelize_obj(b, ignore_keys) |
116 | | - if isinstance(b, dict | list) |
117 | | - else b |
118 | | - for a, b in d.items() |
119 | | - } |
120 | | - |
121 | | - |
122 | 109 | @dataclass |
123 | 110 | class RoborockBase: |
124 | 111 | _ignore_keys = [] # type: ignore |
125 | | - is_cached = False |
126 | 112 |
|
127 | 113 | @staticmethod |
128 | | - def convert_to_class_obj(type, value): |
129 | | - try: |
130 | | - class_type = eval(type) |
131 | | - if get_origin(class_type) is list: |
132 | | - return_list = [] |
133 | | - cls_type = get_args(class_type)[0] |
134 | | - for obj in value: |
135 | | - if issubclass(cls_type, RoborockBase): |
136 | | - return_list.append(cls_type.from_dict(obj)) |
137 | | - elif cls_type in {str, int, float}: |
138 | | - return_list.append(cls_type(obj)) |
139 | | - else: |
140 | | - return_list.append(cls_type(**obj)) |
141 | | - return return_list |
142 | | - if issubclass(class_type, RoborockBase): |
143 | | - converted_value = class_type.from_dict(value) |
144 | | - else: |
145 | | - converted_value = class_type(value) |
146 | | - return converted_value |
147 | | - except NameError as err: |
148 | | - _LOGGER.exception(err) |
149 | | - except ValueError as err: |
150 | | - _LOGGER.exception(err) |
151 | | - except Exception as err: |
152 | | - _LOGGER.exception(err) |
153 | | - raise Exception("Fail") |
| 114 | + def _convert_to_class_obj(class_type: type, value): |
| 115 | + if get_origin(class_type) is list: |
| 116 | + sub_type = get_args(class_type)[0] |
| 117 | + return [RoborockBase._convert_to_class_obj(sub_type, obj) for obj in value] |
| 118 | + if get_origin(class_type) is dict: |
| 119 | + _, value_type = get_args(class_type) # assume keys are only basic types |
| 120 | + return {k: RoborockBase._convert_to_class_obj(value_type, v) for k, v in value.items()} |
| 121 | + if issubclass(class_type, RoborockBase): |
| 122 | + return class_type.from_dict(value) |
| 123 | + if class_type is Any: |
| 124 | + return value |
| 125 | + return class_type(value) # type: ignore[call-arg] |
154 | 126 |
|
155 | 127 | @classmethod |
156 | 128 | def from_dict(cls, data: dict[str, Any]): |
157 | | - if isinstance(data, dict): |
158 | | - ignore_keys = cls._ignore_keys |
159 | | - data = decamelize_obj(data, ignore_keys) |
160 | | - cls_annotations: dict[str, str] = {} |
161 | | - for base in reversed(cls.__mro__): |
162 | | - cls_annotations.update(getattr(base, "__annotations__", {})) |
163 | | - remove_keys = [] |
164 | | - for key, value in data.items(): |
165 | | - if key not in cls_annotations: |
166 | | - remove_keys.append(key) |
167 | | - continue |
168 | | - if value == "None" or value is None: |
169 | | - data[key] = None |
170 | | - continue |
171 | | - field_type: str = cls_annotations[key] |
172 | | - if "|" in field_type: |
173 | | - # It's a union |
174 | | - types = field_type.split("|") |
175 | | - for type in types: |
176 | | - if "None" in type or "Any" in type: |
177 | | - continue |
178 | | - try: |
179 | | - data[key] = RoborockBase.convert_to_class_obj(type, value) |
180 | | - break |
181 | | - except Exception: |
182 | | - ... |
183 | | - else: |
| 129 | + """Create an instance of the class from a dictionary.""" |
| 130 | + if not isinstance(data, dict): |
| 131 | + return None |
| 132 | + field_types = {field.name: field.type for field in dataclasses.fields(cls)} |
| 133 | + result: dict[str, Any] = {} |
| 134 | + for key, value in data.items(): |
| 135 | + key = _decamelize(key) |
| 136 | + if (field_type := field_types.get(key)) is None: |
| 137 | + continue |
| 138 | + if value == "None" or value is None: |
| 139 | + result[key] = None |
| 140 | + continue |
| 141 | + if isinstance(field_type, types.UnionType): |
| 142 | + for subtype in get_args(field_type): |
| 143 | + if subtype is types.NoneType: |
| 144 | + continue |
184 | 145 | try: |
185 | | - data[key] = RoborockBase.convert_to_class_obj(field_type, value) |
| 146 | + result[key] = RoborockBase._convert_to_class_obj(subtype, value) |
| 147 | + break |
186 | 148 | except Exception: |
187 | | - ... |
188 | | - for key in remove_keys: |
189 | | - del data[key] |
190 | | - return cls(**data) |
| 149 | + _LOGGER.exception(f"Failed to convert {key} with value {value} to type {subtype}") |
| 150 | + continue |
| 151 | + else: |
| 152 | + try: |
| 153 | + result[key] = RoborockBase._convert_to_class_obj(field_type, value) |
| 154 | + except Exception: |
| 155 | + _LOGGER.exception(f"Failed to convert {key} with value {value} to type {field_type}") |
| 156 | + continue |
| 157 | + |
| 158 | + return cls(**result) |
191 | 159 |
|
192 | 160 | def as_dict(self) -> dict: |
193 | 161 | return asdict( |
194 | 162 | self, |
195 | 163 | dict_factory=lambda _fields: { |
196 | | - camelize(key): value.value if isinstance(value, Enum) else value |
| 164 | + _camelize(key): value.value if isinstance(value, Enum) else value |
197 | 165 | for (key, value) in _fields |
198 | 166 | if value is not None |
199 | 167 | }, |
|
0 commit comments