base.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import dataclasses
  2. import keyword
  3. import logging
  4. from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast
  5. from typing_extensions import Self, TypeGuard
  6. from yandex_music.utils import model
  7. if TYPE_CHECKING:
  8. from yandex_music import Client, ClientAsync
  9. ujson: bool = False
  10. try:
  11. import ujson as json
  12. ujson = True
  13. except ImportError:
  14. import json
  15. reserved_names = keyword.kwlist
  16. logger = logging.getLogger(__name__)
  17. new_issue_by_template_url = 'https://bit.ly/3dsFxyH'
  18. JSONType = Union[Dict[str, 'JSONType'], Sequence['JSONType'], str, int, float, bool, None]
  19. ClientType = Union['Client', 'ClientAsync']
  20. ModelFieldType = Union[
  21. Dict[str, 'ModelFieldType'], Sequence['ModelFieldType'], 'YandexMusicModel', str, int, float, bool, None
  22. ]
  23. ModelFieldMap = Dict[str, 'ModelFieldType']
  24. MapTypeToDeJson = Dict[str, Callable[['JSONType', 'ClientType'], Optional['YandexMusicModel']]]
  25. class YandexMusicObject:
  26. """Базовый класс для всех классов библиотеки."""
  27. @model
  28. class YandexMusicModel(YandexMusicObject):
  29. """Базовый класс для всех моделей библиотеки."""
  30. def __str__(self) -> str:
  31. return str(self.to_dict())
  32. def __repr__(self) -> str:
  33. return str(self)
  34. def __getitem__(self, item: str) -> Any:
  35. return self.__dict__[item]
  36. @staticmethod
  37. def report_unknown_fields_callback(klass: type, unknown_fields: JSONType) -> None:
  38. """Обратный вызов для обработки неизвестных полей."""
  39. logger.warning(
  40. f'Found unknown fields received from API! Please copy warn message '
  41. f'and send to {new_issue_by_template_url} (GitHub issue), thank you!'
  42. )
  43. logger.warning(f'Type: {klass.__module__}.{klass.__name__}; fields: {unknown_fields}')
  44. @staticmethod
  45. def is_dict_model_data(data: JSONType) -> TypeGuard[Dict[str, JSONType]]:
  46. """Проверка на соответствие данных словарю.
  47. Args:
  48. data (:obj:`JSONType`): Данные для проверки.
  49. Returns:
  50. :obj:`bool`: Валидны ли данные.
  51. """
  52. return bool(data) and isinstance(data, dict)
  53. @staticmethod
  54. def valid_client(client: Optional['ClientType']) -> TypeGuard['Client']:
  55. """Проверка что клиент передан и является синхронным.
  56. Args:
  57. client (:obj:`Optional['ClientType']`): Клиент для проверки.
  58. Returns:
  59. :obj:`bool`: Синхронный ли клиент.
  60. """
  61. from yandex_music import Client
  62. return isinstance(client, Client)
  63. @staticmethod
  64. def valid_async_client(client: Optional['ClientType']) -> TypeGuard['ClientAsync']:
  65. """Проверка что клиент передан и является асинхронным.
  66. Args:
  67. client (:obj:`Optional['ClientType']`): Клиент для проверки.
  68. Returns:
  69. :obj:`bool`: Асинхронный ли клиент.
  70. """
  71. from yandex_music import ClientAsync
  72. return isinstance(client, ClientAsync)
  73. @staticmethod
  74. def is_array_model_data(data: JSONType) -> TypeGuard[List[Dict[str, JSONType]]]:
  75. """Проверка на соответствие данных массиву словарей.
  76. Args:
  77. data (:obj:`JSONType`): Данные для проверки.
  78. Returns:
  79. :obj:`bool`: Валидны ли данные.
  80. """
  81. return bool(data) and isinstance(data, list) and all(isinstance(item, dict) for item in data)
  82. @classmethod
  83. def cleanup_data(cls, data: JSONType, client: Optional['ClientType']) -> ModelFieldMap:
  84. """Удаляет незадекларированные поля для текущей модели из сырых данных.
  85. Note:
  86. Фильтрует только словарь поле:значение. Иначе вернёт пустой :obj:`dict`.
  87. Args:
  88. data (:obj:`JSONType`): Поля и значения десериализуемого объекта.
  89. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  90. Returns:
  91. :obj:`ModelFieldMap`: Отфильтрованные данные.
  92. """
  93. if not YandexMusicModel.is_dict_model_data(data):
  94. return {}
  95. data = data.copy()
  96. fields = {f.name for f in dataclasses.fields(cls)}
  97. cleaned_data: Dict[str, JSONType] = {}
  98. unknown_data: Dict[str, JSONType] = {}
  99. for k, v in data.items():
  100. if k in fields:
  101. cleaned_data[k] = v
  102. else:
  103. unknown_data[k] = v
  104. if client and client.report_unknown_fields and unknown_data:
  105. cls.report_unknown_fields_callback(cls, unknown_data)
  106. return cleaned_data
  107. @classmethod
  108. def de_json(cls, data: 'JSONType', client: 'ClientType') -> Optional[Self]:
  109. """Десериализация объекта.
  110. Note:
  111. Переопределяется в дочерних классах когда есть вложенные объекты.
  112. Args:
  113. data (:obj:`JSONType`): Поля и значения десериализуемого объекта.
  114. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  115. Returns:
  116. :obj:`yandex_music.YandexMusicModel`: Десериализованный объект.
  117. """
  118. if not cls.is_dict_model_data(data):
  119. return None
  120. return cls(client=client, **cls.cleanup_data(data, client))
  121. @classmethod
  122. def de_list(cls, data: JSONType, client: 'ClientType') -> Sequence[Self]:
  123. """Десериализация списка объектов.
  124. Note:
  125. Переопределяется в дочерних классах, если необходимо.
  126. Например, в сложных объектах где есть вариации подтипов.
  127. Args:
  128. data (:obj:`JSONType`): Список словарей с полями и значениями десериализуемого объекта.
  129. client (:obj:`yandex_music.Client`, optional): Клиент Yandex Music.
  130. Returns:
  131. :obj:`list` из :obj:`yandex_music.YandexMusicModel`: Список десериализованных объектов.
  132. """
  133. if not cls.is_array_model_data(data):
  134. return []
  135. items = [cls.de_json(item, client) for item in data]
  136. return [item for item in items if item is not None]
  137. def to_json(self, for_request: bool = False) -> str:
  138. """Сериализация объекта.
  139. Args:
  140. for_request (:obj:`bool`): Подготовить ли объект для отправки в теле запроса.
  141. Returns:
  142. :obj:`str`: Сериализованный в JSON объект.
  143. """
  144. return json.dumps(self.to_dict(for_request), ensure_ascii=not ujson)
  145. def to_dict(self, for_request: bool = False) -> JSONType:
  146. """Рекурсивная сериализация объекта.
  147. Args:
  148. for_request (:obj:`bool`): Перевести ли обратно все поля в camelCase и игнорировать зарезервированные слова.
  149. Note:
  150. Исключает из сериализации `client` и `_id_attrs` необходимые в `__eq__`.
  151. К зарезервированным словам добавляет "_" в конец.
  152. Returns:
  153. :obj:`dict`: Сериализованный в dict объект.
  154. """
  155. def parse(val: Union['YandexMusicModel', JSONType]) -> Any: # noqa: ANN401
  156. if isinstance(val, YandexMusicModel):
  157. return val.to_dict(for_request)
  158. if isinstance(val, list):
  159. return [parse(it) for it in val]
  160. if isinstance(val, dict):
  161. return {key: parse(value) for key, value in val.items()}
  162. return val
  163. data = self.__dict__.copy()
  164. data.pop('client', None)
  165. data.pop('_id_attrs', None)
  166. if for_request:
  167. for k, v in data.copy().items():
  168. camel_case = ''.join(word.title() for word in k.split('_'))
  169. camel_case = camel_case[0].lower() + camel_case[1:]
  170. data.pop(k)
  171. data.update({camel_case: v})
  172. else:
  173. for k, v in data.copy().items():
  174. if k.lower() in reserved_names:
  175. data.pop(k)
  176. data.update({f'{k}_': v})
  177. return parse(data)
  178. def _get_id_attrs(self) -> Tuple[str]:
  179. """Получение ключевых атрибутов объекта.
  180. Returns:
  181. :obj:`tuple`: Ключевые атрибуты объекта для сравнения.
  182. """
  183. return cast(Tuple[str], getattr(self, '_id_attrs', ()))
  184. def __eq__(self, other: Any) -> bool: # noqa: ANN401
  185. """Проверка на равенство двух объектов.
  186. Note:
  187. Проверка осуществляется по определённым атрибутам классов, перечисленных в множестве `_id_attrs`.
  188. Returns:
  189. :obj:`bool`: Одинаковые ли объекты (по содержимому).
  190. """
  191. if isinstance(other, self.__class__):
  192. return self._get_id_attrs() == other._get_id_attrs()
  193. return super(YandexMusicModel, self).__eq__(other)
  194. def __hash__(self) -> int:
  195. """Реализация хеш-функции на основе ключевых атрибутов.
  196. Note:
  197. Так как перечень ключевых атрибутов хранится в виде множества, для вычисления хеша он замораживается.
  198. Returns:
  199. :obj:`int`: Хеш объекта.
  200. """
  201. id_attrs = self._get_id_attrs()
  202. if not id_attrs:
  203. return super(YandexMusicModel, self).__hash__()
  204. frozen_attrs = tuple(frozenset(attr) if isinstance(attr, list) else attr for attr in id_attrs)
  205. return hash((self.__class__, frozen_attrs))