maxapi-python 2.1.2__tar.gz → 2.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/PKG-INFO +1 -1
  2. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/conf.py +14 -5
  3. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/index.rst +1 -0
  4. maxapi_python-2.1.3/docs/release-2-1-3.rst +48 -0
  5. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/pyproject.toml +1 -1
  6. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/__init__.py +1 -1
  7. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/app.py +18 -4
  8. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/client.py +1 -4
  9. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/connection.py +46 -19
  10. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/photo.py +4 -2
  11. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/logging.py +33 -3
  12. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/payload.py +22 -42
  13. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/protocol.py +2 -8
  14. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/__init__.py +1 -0
  15. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/audio.py +4 -4
  16. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/enums.py +1 -0
  17. maxapi_python-2.1.3/src/pymax/types/domain/attachments/unknown.py +37 -0
  18. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/video.py +2 -2
  19. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/element.py +3 -3
  20. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/message.py +3 -1
  21. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/app/test_app_runtime.py +45 -0
  22. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/connection/test_connection.py +51 -1
  23. maxapi_python-2.1.3/tests/domain/test_message_models.py +105 -0
  24. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/files/test_files_and_formatting.py +6 -0
  25. maxapi_python-2.1.3/tests/test_logging.py +169 -0
  26. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/uv.lock +1 -1
  27. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  29. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
  30. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.github/pull_request_template.md +0 -0
  31. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.github/workflows/publish.yml +0 -0
  32. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.gitignore +0 -0
  33. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/.pre-commit-config.yaml +0 -0
  34. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/LICENSE +0 -0
  35. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/README.md +0 -0
  36. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/_static/.gitkeep +0 -0
  37. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/account.rst +0 -0
  38. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/api/auth.rst +0 -0
  39. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/api/client.rst +0 -0
  40. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/api/files.rst +0 -0
  41. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/api/router.rst +0 -0
  42. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/auth.rst +0 -0
  43. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/chats.rst +0 -0
  44. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/client.rst +0 -0
  45. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/examples.rst +0 -0
  46. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/faq.rst +0 -0
  47. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/files.rst +0 -0
  48. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/formatting.rst +0 -0
  49. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/getting-started.rst +0 -0
  50. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/messages.rst +0 -0
  51. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/release-2-1-0.rst +0 -0
  52. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/release-2-1-1.rst +0 -0
  53. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/release-2-1-2.rst +0 -0
  54. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/router.rst +0 -0
  55. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/troubleshooting.rst +0 -0
  56. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/audio_attachment.rst +0 -0
  57. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/call_attachment.rst +0 -0
  58. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/chat.rst +0 -0
  59. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/contact_attachment.rst +0 -0
  60. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/control_attachment.rst +0 -0
  61. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/element.rst +0 -0
  62. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/enums.rst +0 -0
  63. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/file_attachment.rst +0 -0
  64. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/folder.rst +0 -0
  65. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/folder_list.rst +0 -0
  66. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/folder_update.rst +0 -0
  67. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/index.rst +0 -0
  68. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/inline_keyboard_attachment.rst +0 -0
  69. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/message.rst +0 -0
  70. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/message_delete_event.rst +0 -0
  71. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/name.rst +0 -0
  72. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/photo_attachment.rst +0 -0
  73. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/profile.rst +0 -0
  74. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/reaction_counter.rst +0 -0
  75. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/reaction_info.rst +0 -0
  76. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/read_state.rst +0 -0
  77. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/session.rst +0 -0
  78. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/share_attachment.rst +0 -0
  79. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/sticker_attachment.rst +0 -0
  80. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/sync_overrides.rst +0 -0
  81. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/sync_state.rst +0 -0
  82. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/user.rst +0 -0
  83. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/types/video_attachment.rst +0 -0
  84. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/docs/users.rst +0 -0
  85. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/__init__.py +0 -0
  86. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/auth/__init__.py +0 -0
  87. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/auth/enums.py +0 -0
  88. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/auth/payloads.py +0 -0
  89. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/auth/service.py +0 -0
  90. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/auth/types.py +0 -0
  91. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/bots/__init__.py +0 -0
  92. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/bots/payloads.py +0 -0
  93. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/bots/service.py +0 -0
  94. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/chats/__init__.py +0 -0
  95. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/chats/enums.py +0 -0
  96. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/chats/payloads.py +0 -0
  97. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/chats/service.py +0 -0
  98. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/facade.py +0 -0
  99. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/messages/__init__.py +0 -0
  100. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/messages/enums.py +0 -0
  101. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/messages/payloads.py +0 -0
  102. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/messages/service.py +0 -0
  103. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/models.py +0 -0
  104. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/response.py +0 -0
  105. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/self/__init__.py +0 -0
  106. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/self/enums.py +0 -0
  107. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/self/payloads.py +0 -0
  108. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/self/service.py +0 -0
  109. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/session/__init__.py +0 -0
  110. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/session/enums.py +0 -0
  111. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/session/payloads.py +0 -0
  112. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/session/service.py +0 -0
  113. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/uploads/__init__.py +0 -0
  114. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/uploads/models.py +0 -0
  115. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/uploads/payloads.py +0 -0
  116. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/uploads/service.py +0 -0
  117. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/users/__init__.py +0 -0
  118. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/users/enums.py +0 -0
  119. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/users/payloads.py +0 -0
  120. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/api/users/service.py +0 -0
  121. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/__init__.py +0 -0
  122. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/base.py +0 -0
  123. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/email.py +0 -0
  124. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/models.py +0 -0
  125. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/providers.py +0 -0
  126. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/qr.py +0 -0
  127. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/service.py +0 -0
  128. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/auth/sms.py +0 -0
  129. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/base.py +0 -0
  130. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/client_web.py +0 -0
  131. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/config.py +0 -0
  132. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/__init__.py +0 -0
  133. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/pending.py +0 -0
  134. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/readers/__init__.py +0 -0
  135. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/readers/base.py +0 -0
  136. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/readers/tcp.py +0 -0
  137. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/connection/readers/ws.py +0 -0
  138. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/__init__.py +0 -0
  139. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/dispatcher.py +0 -0
  140. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/enums.py +0 -0
  141. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/mapping.py +0 -0
  142. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/resolvers.py +0 -0
  143. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/dispatch/router.py +0 -0
  144. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/exceptions.py +0 -0
  145. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/__init__.py +0 -0
  146. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/base.py +0 -0
  147. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/file.py +0 -0
  148. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/static.py +0 -0
  149. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/files/video.py +0 -0
  150. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/formatting/__init__.py +0 -0
  151. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/formatting/markdown.py +0 -0
  152. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/__init__.py +0 -0
  153. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/auth.py +0 -0
  154. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/base.py +0 -0
  155. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/bots.py +0 -0
  156. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/chat.py +0 -0
  157. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/message.py +0 -0
  158. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/protocol.py +0 -0
  159. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/self.py +0 -0
  160. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/infra/user.py +0 -0
  161. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/__init__.py +0 -0
  162. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/base.py +0 -0
  163. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/enums.py +0 -0
  164. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/models.py +0 -0
  165. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/__init__.py +0 -0
  166. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/compression.py +0 -0
  167. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/framing.py +0 -0
  168. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/ws/__init__.py +0 -0
  169. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/protocol/ws/protocol.py +0 -0
  170. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/py.typed +0 -0
  171. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/routers.py +0 -0
  172. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/session/__init__.py +0 -0
  173. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/session/models.py +0 -0
  174. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/session/protocol.py +0 -0
  175. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/session/store.py +0 -0
  176. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/telemetry/__init__.py +0 -0
  177. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/telemetry/navigation.py +0 -0
  178. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/telemetry/payloads.py +0 -0
  179. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/telemetry/service.py +0 -0
  180. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/transport/__init__.py +0 -0
  181. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/transport/base.py +0 -0
  182. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/transport/tcp.py +0 -0
  183. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/transport/websocket.py +0 -0
  184. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/__init__.py +0 -0
  185. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/__init__.py +0 -0
  186. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/call.py +0 -0
  187. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/contact.py +0 -0
  188. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/control.py +0 -0
  189. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/file.py +0 -0
  190. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/__init__.py +0 -0
  191. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/inline.py +0 -0
  192. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/photo.py +0 -0
  193. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/share.py +0 -0
  194. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/sticker.py +0 -0
  195. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/auth.py +0 -0
  196. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/base.py +0 -0
  197. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/bots.py +0 -0
  198. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/chat.py +0 -0
  199. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/enums.py +0 -0
  200. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/error.py +0 -0
  201. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/folder.py +0 -0
  202. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/login.py +0 -0
  203. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/member.py +0 -0
  204. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/name.py +0 -0
  205. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/presence.py +0 -0
  206. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/profile.py +0 -0
  207. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/session.py +0 -0
  208. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/sync.py +0 -0
  209. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/domain/user.py +0 -0
  210. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/events/__init__.py +0 -0
  211. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/events/file.py +0 -0
  212. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/events/message.py +0 -0
  213. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/src/pymax/types/events/video.py +0 -0
  214. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/__init__.py +0 -0
  215. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/api/test_auth_service.py +0 -0
  216. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/api/test_chat_user_self_session_services.py +0 -0
  217. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/api/test_message_service.py +0 -0
  218. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/api/test_upload_service.py +0 -0
  219. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/auth/test_auth_flows.py +0 -0
  220. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/conftest.py +0 -0
  221. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/connection/test_readers_and_transports.py +0 -0
  222. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/dispatch/test_dispatcher.py +0 -0
  223. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/domain/test_bound_models.py +0 -0
  224. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/protocol/test_protocols.py +0 -0
  225. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/session/test_store.py +0 -0
  226. {maxapi_python-2.1.2 → maxapi_python-2.1.3}/tests/telemetry/test_telemetry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
6
6
  Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
@@ -78,6 +78,7 @@ release = __version__
78
78
 
79
79
  extensions = [
80
80
  "sphinx.ext.autodoc",
81
+ "sphinx.ext.autosummary",
81
82
  "sphinx.ext.napoleon",
82
83
  "sphinx.ext.viewcode",
83
84
  "sphinx.ext.intersphinx",
@@ -86,6 +87,8 @@ extensions = [
86
87
 
87
88
  templates_path = ["_templates"]
88
89
 
90
+ autosummary_generate = True
91
+
89
92
  exclude_patterns = [
90
93
  "_build",
91
94
  "Thumbs.db",
@@ -99,20 +102,24 @@ language = "ru"
99
102
  autodoc_default_options = {
100
103
  "members": True,
101
104
  "undoc-members": False,
102
- "show-inheritance": False,
105
+ "show-inheritance": True,
103
106
  "private-members": False,
104
107
  "special-members": False,
108
+ "member-order": "bysource",
105
109
  "exclude-members": (
106
- "dict,json,parse_obj,parse_raw,schema,model_dump,model_validate,"
107
- "model_json_schema,model_construct"
110
+ "dict,json,parse_obj,parse_raw,schema,schema_json,"
111
+ "copy,construct,from_orm,update_forward_refs,validate,"
112
+ "model_dump,model_dump_json,model_validate,model_validate_json,"
113
+ "model_validate_strings,model_json_schema,model_construct,"
114
+ "model_copy,model_rebuild,model_post_init,model_parametrized_name"
108
115
  ),
109
116
  }
110
117
 
118
+
119
+ autodoc_typehints_format = "short"
111
120
  autodoc_member_order = "bysource"
112
121
  autodoc_typehints = "description"
113
- autodoc_typehints_format = "short"
114
122
  autodoc_class_signature = "separated"
115
-
116
123
  # -- Napoleon ----------------------------------------------------------------
117
124
 
118
125
  napoleon_google_docstring = True
@@ -131,7 +138,9 @@ intersphinx_mapping = {
131
138
 
132
139
  # -- HTML --------------------------------------------------------------------
133
140
 
141
+ # html_theme = "shibuya"
134
142
  html_theme = "furo"
143
+
135
144
  html_title = "PyMax"
136
145
  html_static_path = ["_static"]
137
146
 
@@ -22,6 +22,7 @@ PyMax - асинхронная Python-библиотека для Max API. Он
22
22
  :maxdepth: 1
23
23
  :caption: Новости
24
24
 
25
+ release-2-1-3
25
26
  release-2-1-2
26
27
  release-2-1-1
27
28
  release-2-1-0
@@ -0,0 +1,48 @@
1
+ PyMax 2.1.3
2
+ ===========
3
+
4
+ Изменения относительно ``2.1.2``.
5
+
6
+ Добавлено
7
+ ---------
8
+
9
+ * ``UnknownAttachment`` для вложений с неизвестным ``_type``. Такие вложения
10
+ больше не ломают парсинг ``Message`` и сохраняют дополнительные поля
11
+ payload-а.
12
+
13
+ Исправлено
14
+ ----------
15
+
16
+ * ``Message`` больше не падает на неизвестных типах вложений вроде
17
+ ``UNSUPPORTED``.
18
+ * ``AudioAttachment`` принимает payload без ``duration`` и ``audioId``.
19
+ * ``VideoAttachment`` принимает payload без ``duration``.
20
+ * ``ElementAttributes.url`` и ``Element.length`` стали необязательными для
21
+ элементов, где Max не присылает эти поля.
22
+ * ``Photo(url=...)`` корректно определяет расширение и MIME type, если в URL
23
+ есть query string.
24
+ * При потере соединения ``App.started`` сбрасывается, ping-task отменяется, а
25
+ pending API-запросы очищаются без ``Future exception was never retrieved``.
26
+ * Reconnect/close после штатного сетевого обрыва стало меньше шуметь
27
+ exception-логами.
28
+
29
+ Изменилось
30
+ ----------
31
+
32
+ * ``configure_logging()`` теперь уважает уже настроенный logging
33
+ host-приложения: PyMax не очищает чужие handler-ы и не добавляет свой
34
+ stderr-handler, если logging уже сконфигурирован.
35
+ * Если logging не настроен, PyMax по-прежнему включает pretty-логи из коробки.
36
+ * Для принудительного включения pretty-логов PyMax добавлен аргумент
37
+ ``configure_logging(..., force=True)``.
38
+ * TCP msgpack decoder стал проще и подробнее логирует payload при ошибках
39
+ декодирования.
40
+
41
+ Миграция
42
+ --------
43
+
44
+ * Код на ``Client`` и ``WebClient`` обычно менять не нужно.
45
+ * Если приложение рассчитывало, что ``configure_logging()`` всегда заменяет
46
+ существующие handler-ы ``pymax``, передайте ``force=True``.
47
+ * Если код обрабатывал ``ValidationError`` для неизвестных вложений, теперь
48
+ вместо ошибки придет ``UnknownAttachment``.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "maxapi-python"
3
- version = "2.1.2"
3
+ version = "2.1.3"
4
4
  description = "Python wrapper для API мессенджера Max"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
- __version__ = "2.1.2"
1
+ __version__ = "2.1.3"
2
2
 
3
3
 
4
4
  from .auth import (
@@ -49,6 +49,7 @@ class App(Generic[ClientT]):
49
49
  self._telemetry = TelemetryService(self) if config.telemetry else None
50
50
 
51
51
  self.connection.on_event = self.on_event
52
+ self.connection.on_close = self.on_connection_lost
52
53
  logger.debug(
53
54
  "app initialized session=%s work_dir=%s auth_flow=%s",
54
55
  config.session_name,
@@ -174,6 +175,7 @@ class App(Generic[ClientT]):
174
175
  await self.dispatcher.stop_startup_tasks()
175
176
  await self.connection.close()
176
177
  await self.store.close()
178
+
177
179
  self.started = False
178
180
 
179
181
  async def invoke(
@@ -203,9 +205,7 @@ class App(Generic[ClientT]):
203
205
  payload_keys,
204
206
  )
205
207
  logger.debug("Request data=%s", frame.model_dump())
206
- request_timeout = (
207
- self.config.request_timeout if timeout is None else timeout
208
- )
208
+ request_timeout = self.config.request_timeout if timeout is None else timeout
209
209
  response = await self.connection.request(frame, timeout=request_timeout)
210
210
  response_keys = sorted(response.payload.keys()) if response.payload else []
211
211
  logger.debug(
@@ -231,9 +231,23 @@ class App(Generic[ClientT]):
231
231
  except asyncio.CancelledError:
232
232
  raise
233
233
  except Exception as e:
234
- logger.exception("ping loop failed; closing transport")
234
+ logger.warning("ping loop failed; closing transport: %s", e)
235
235
  await self.connection.fail(ConnectionError(f"Ping failed: {e}"))
236
236
 
237
+ def on_connection_lost(self, exc: Exception | None = None) -> None:
238
+ if self.started:
239
+ logger.warning("connection lost; marking app as stopped: %s", exc)
240
+
241
+ self.started = False
242
+
243
+ task = self._ping_task
244
+ if task is None or task.done():
245
+ return
246
+
247
+ current_task = asyncio.current_task()
248
+ if task is not current_task:
249
+ task.cancel()
250
+
237
251
  def _build_api_error(self, response: InboundFrame) -> ApiError:
238
252
  try:
239
253
  error = MaxApiError.model_validate(response.payload)
@@ -66,10 +66,7 @@ class Client(BaseClient["Client"]):
66
66
 
67
67
  self._config = self._build_config(
68
68
  phone=phone,
69
- user_agent=(
70
- self.extra_config.user_agent
71
- or self.extra_config.generate_user_agent()
72
- ),
69
+ user_agent=(self.extra_config.user_agent or self.extra_config.generate_user_agent()),
73
70
  )
74
71
 
75
72
  if auth_flow is None:
@@ -20,16 +20,19 @@ class ConnectionManager:
20
20
  transport: Transport,
21
21
  protocol: BaseProtocol,
22
22
  on_event: Callable[[InboundFrame], Awaitable[None]] | None = None,
23
+ on_close: Callable[[Exception | None], None] | None = None,
23
24
  ) -> None:
24
25
  self.reader = reader
25
26
  self.transport = transport
26
27
  self.protocol = protocol
27
28
  self.on_event = on_event
29
+ self.on_close = on_close
28
30
 
29
31
  self.requests = PendingRequests()
30
32
 
31
33
  self._is_open = False
32
34
  self._connection_lost = False
35
+ self._close_reported = False
33
36
  self._seq = -1
34
37
 
35
38
  self._recv_task: asyncio.Task[None] | None = None
@@ -44,6 +47,7 @@ class ConnectionManager:
44
47
  await self.transport.connect()
45
48
  self._is_open = True
46
49
  self._connection_lost = False
50
+ self._close_reported = False
47
51
 
48
52
  self._recv_task = asyncio.create_task(self._recv_loop())
49
53
  logger.debug("receive loop started")
@@ -80,7 +84,7 @@ class ConnectionManager:
80
84
  self._connection_lost = True
81
85
  self.requests.cancel_all(exc=exc)
82
86
  await self.transport.close()
83
- self._is_open = False
87
+ self._mark_closed(exc)
84
88
 
85
89
  async def send(self, frame: OutboundFrame) -> None:
86
90
  if not self._is_open:
@@ -116,20 +120,38 @@ class ConnectionManager:
116
120
  )
117
121
  await self.transport.send(raw)
118
122
  return await asyncio.wait_for(future, timeout)
119
- except Exception as e:
123
+ except asyncio.CancelledError:
124
+ self.requests.discard(frame.seq)
125
+ raise
126
+ except (ConnectionError, EOFError, OSError, TimeoutError) as e:
127
+ logger.warning(
128
+ "request failed seq=%s opcode=%s error=%s",
129
+ frame.seq,
130
+ frame.opcode,
131
+ e,
132
+ )
133
+ self.requests.discard(frame.seq)
134
+ raise
135
+ except Exception:
120
136
  logger.exception(
121
137
  "request failed seq=%s opcode=%s",
122
138
  frame.seq,
123
139
  frame.opcode,
124
140
  )
125
- self.requests.reject(frame.seq, e)
141
+ self.requests.discard(frame.seq)
126
142
  raise
127
143
 
128
144
  async def wait_closed(self) -> None:
129
145
  if not self._recv_task:
130
146
  return
131
147
 
132
- await self._recv_task
148
+ try:
149
+ await self._recv_task
150
+ except Exception as e:
151
+ if self._connection_lost:
152
+ raise ConnectionError("Connection lost") from e
153
+ raise
154
+
133
155
  if self._connection_lost:
134
156
  raise ConnectionError("Connection lost")
135
157
 
@@ -147,27 +169,23 @@ class ConnectionManager:
147
169
  await self._handle_inbound(model)
148
170
 
149
171
  except EOFError:
172
+ exc = ConnectionError("Connection closed by the server")
150
173
  logger.warning("connection closed by server")
151
- self.requests.cancel_all(
152
- exc=ConnectionError("Connection closed by the server")
153
- )
174
+ self.requests.cancel_all(exc=exc)
154
175
  self._connection_lost = True
155
- self._is_open = False
156
- except TimeoutError as e:
157
- logger.exception("connection timed out")
158
- self.requests.cancel_all(
159
- exc=ConnectionError("Connection timed out")
160
- )
176
+ self._mark_closed(exc)
177
+ except (ConnectionError, OSError, TimeoutError) as e:
178
+ exc = ConnectionError(f"Connection error: {e}")
179
+ logger.warning("connection closed while reading payload: %s", e)
180
+ self.requests.cancel_all(exc=exc)
161
181
  self._connection_lost = True
162
- self._is_open = False
163
- raise e
182
+ self._mark_closed(exc)
164
183
  except Exception as e:
184
+ exc = ConnectionError(f"Connection error: {e}")
165
185
  logger.exception("connection receive loop failed")
166
- self.requests.cancel_all(
167
- exc=ConnectionError(f"Connection error: {e}")
168
- )
186
+ self.requests.cancel_all(exc=exc)
169
187
  self._connection_lost = True
170
- self._is_open = False
188
+ self._mark_closed(exc)
171
189
  raise e
172
190
 
173
191
  async def _handle_inbound(self, frame: InboundFrame) -> None:
@@ -210,6 +228,15 @@ class ConnectionManager:
210
228
  self._seq = (self._seq + 1) % 0x10000
211
229
  return self._seq
212
230
 
231
+ def _mark_closed(self, exc: Exception | None = None) -> None:
232
+ self._is_open = False
233
+ if self._close_reported:
234
+ return
235
+
236
+ self._close_reported = True
237
+ if self.on_close:
238
+ self.on_close(exc)
239
+
213
240
  @property
214
241
  def is_open(self) -> bool:
215
242
  return self._is_open
@@ -1,6 +1,7 @@
1
1
  import mimetypes
2
2
  from collections.abc import AsyncGenerator
3
3
  from pathlib import Path
4
+ from urllib.parse import urlsplit
4
5
 
5
6
  from .base import BaseFile
6
7
  from .static import ALLOWED_EXTENSIONS
@@ -66,12 +67,13 @@ class Photo(BaseFile):
66
67
  raise ValueError(msg)
67
68
  return (extension[1:], ("image/" + extension[1:]).lower())
68
69
  if self.url:
69
- extension = Path(self.url).suffix.lower()
70
+ url_path = urlsplit(self.url).path
71
+ extension = Path(url_path).suffix.lower()
70
72
  if extension not in ALLOWED_EXTENSIONS:
71
73
  msg = f"Invalid photo extension: {extension}. Allowed: {ALLOWED_EXTENSIONS}"
72
74
  raise ValueError(msg)
73
75
 
74
- mime_type = mimetypes.guess_type(self.url)[0]
76
+ mime_type = mimetypes.guess_type(url_path)[0]
75
77
 
76
78
  if not mime_type or not mime_type.startswith("image/"):
77
79
  msg = f"URL does not appear to be an image: {self.url}"
@@ -4,6 +4,7 @@ import sys
4
4
  from typing import TextIO
5
5
 
6
6
  DATE_FORMAT = "%H:%M:%S"
7
+ PYMAX_HANDLER_ATTR = "_pymax_pretty_handler"
7
8
 
8
9
  RESET = "\x1b[0m"
9
10
  DIM = "\x1b[2m"
@@ -56,17 +57,22 @@ def configure_logging(
56
57
  *,
57
58
  stream: TextIO | None = None,
58
59
  use_colors: bool | None = None,
60
+ force: bool = False,
59
61
  ) -> None:
60
62
  """Настраивает pretty-логи для logger-а ``pymax``.
61
63
 
62
64
  Обычно уровень логов задают через ``ExtraConfig(log_level="DEBUG")``.
63
- Вызывайте эту функцию вручную, если хотите управлять stream или цветами.
65
+ PyMax ставит свой handler только если приложение еще не настроило logging.
66
+ Вызывайте эту функцию с ``force=True``, если хотите принудительно включить
67
+ pretty-логи PyMax.
64
68
 
65
69
  Args:
66
70
  level: Уровень логирования: строка вроде ``"DEBUG"`` или число из
67
71
  модуля ``logging``.
68
72
  stream: Поток для вывода. По умолчанию ``sys.stderr``.
69
73
  use_colors: Включить ANSI-цвета. Если ``None``, определяется по TTY.
74
+ force: Заменить существующие handler-ы logger-а ``pymax`` на pretty
75
+ handler PyMax.
70
76
 
71
77
  Returns:
72
78
  ``None``.
@@ -84,17 +90,25 @@ def configure_logging(
84
90
  use_colors = hasattr(stream, "isatty") and stream.isatty()
85
91
 
86
92
  logger = logging.getLogger("pymax")
93
+ level_value = _normalize_level(level)
94
+ logger.setLevel(level_value)
95
+
96
+ if not force and _logging_already_configured(logger):
97
+ if logging.getLogger().handlers and not _has_non_null_handlers(logger):
98
+ logger.propagate = True
99
+ return
100
+
87
101
  logger.handlers.clear()
88
- logger.setLevel(_normalize_level(level))
89
102
  logger.propagate = False
90
103
 
91
104
  handler = logging.StreamHandler(stream)
92
- handler.setLevel(_normalize_level(level))
105
+ handler.setLevel(level_value)
93
106
  handler.setFormatter(
94
107
  PrettyFormatter(
95
108
  use_colors=use_colors,
96
109
  )
97
110
  )
111
+ setattr(handler, PYMAX_HANDLER_ATTR, True)
98
112
 
99
113
  logger.addHandler(handler)
100
114
 
@@ -126,4 +140,20 @@ def _strip_ansi(text: str) -> str:
126
140
  return re.sub(r"\x1b\[[0-9;]*m", "", text)
127
141
 
128
142
 
143
+ def _logging_already_configured(logger: logging.Logger) -> bool:
144
+ return bool(logging.getLogger().handlers or _has_external_handlers(logger))
145
+
146
+
147
+ def _has_non_null_handlers(logger: logging.Logger) -> bool:
148
+ return any(not isinstance(handler, logging.NullHandler) for handler in logger.handlers)
149
+
150
+
151
+ def _has_external_handlers(logger: logging.Logger) -> bool:
152
+ return any(
153
+ not isinstance(handler, logging.NullHandler)
154
+ and not getattr(handler, PYMAX_HANDLER_ATTR, False)
155
+ for handler in logger.handlers
156
+ )
157
+
158
+
129
159
  logging.getLogger("pymax").addHandler(logging.NullHandler())
@@ -15,10 +15,7 @@ class MsgpackPayloadCodec:
15
15
  if isinstance(value, Enum):
16
16
  return value.value
17
17
  if isinstance(value, dict):
18
- return {
19
- self._to_msgpack_value(k): self._to_msgpack_value(v)
20
- for k, v in value.items()
21
- }
18
+ return {self._to_msgpack_value(k): self._to_msgpack_value(v) for k, v in value.items()}
22
19
  if isinstance(value, list):
23
20
  return [self._to_msgpack_value(item) for item in value]
24
21
  if isinstance(value, tuple):
@@ -28,56 +25,42 @@ class MsgpackPayloadCodec:
28
25
  def encode(self, payload: object) -> bytes:
29
26
  if payload is None:
30
27
  return b""
31
- return (
32
- msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True)
33
- or b""
34
- )
28
+ return msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True) or b""
35
29
 
36
- def _unpack_stream(self, payload_bytes: bytes, *, raw: bool) -> list[Any]:
30
+ def _unpack_stream(
31
+ self, payload_bytes: bytes, *, raw: bool
32
+ ) -> list[Any]: # TODO: deprecate? idk
37
33
  unpacker = msgpack.Unpacker(raw=raw, strict_map_key=False)
38
34
  unpacker.feed(payload_bytes)
39
35
  return list(unpacker)
40
36
 
41
- def decode(self, payload_bytes: bytes) -> dict[Any, Any]:
37
+ def decode(self, payload_bytes: bytes) -> Any:
42
38
  if not payload_bytes:
43
39
  return {}
44
40
 
45
41
  try:
46
42
  return msgpack.unpackb(
47
- payload_bytes, raw=False, strict_map_key=False
43
+ payload_bytes,
44
+ raw=False,
45
+ strict_map_key=False,
48
46
  )
49
- except msgpack.exceptions.ExtraData as e:
50
- if isinstance(e.unpacked, dict):
51
- logger.debug(
52
- "msgpack payload has trailing data unpacked_type=%s extra_bytes=%s extra_head=%s",
53
- type(e.unpacked).__name__,
54
- len(e.extra),
55
- e.extra[:16].hex(),
56
- )
57
- return e.unpacked
58
47
 
59
- try:
60
- values = self._unpack_stream(payload_bytes, raw=False)
61
- except UnicodeDecodeError:
62
- values = self._unpack_stream(payload_bytes, raw=True)
48
+ except msgpack.exceptions.ExtraData as e:
63
49
  logger.debug(
64
- "msgpack payload has extra data objects=%s extra_bytes=%s",
65
- [type(value).__name__ for value in values],
50
+ "msgpack extra data: unpacked_type=%s extra_len=%s extra_head=%s payload_head=%s",
51
+ type(e.unpacked).__name__,
66
52
  len(e.extra),
53
+ e.extra[:64].hex(),
54
+ payload_bytes[:128].hex(),
67
55
  )
68
- for value in values:
69
- if isinstance(value, dict):
70
- return value
71
- raise
72
- except UnicodeDecodeError:
73
- values = self._unpack_stream(payload_bytes, raw=True)
74
- logger.debug(
75
- "msgpack payload decoded with raw bytes objects=%s",
76
- [type(value).__name__ for value in values],
56
+ return e.unpacked
57
+
58
+ except Exception:
59
+ logger.exception(
60
+ "msgpack decode failed: payload_len=%s payload_head=%s",
61
+ len(payload_bytes),
62
+ payload_bytes[:128].hex(),
77
63
  )
78
- for value in values:
79
- if isinstance(value, dict):
80
- return value
81
64
  raise
82
65
 
83
66
 
@@ -93,10 +76,7 @@ class TcpPayloadDecoder:
93
76
 
94
77
  def _normalize_keys(self, obj: Any) -> Any:
95
78
  if isinstance(obj, dict):
96
- return {
97
- self._normalize_key(k): self._normalize_keys(v)
98
- for k, v in obj.items()
99
- }
79
+ return {self._normalize_key(k): self._normalize_keys(v) for k, v in obj.items()}
100
80
  if isinstance(obj, list):
101
81
  return [self._normalize_keys(item) for item in obj]
102
82
  if isinstance(obj, tuple):
@@ -25,11 +25,7 @@ class TcpProtocol(BaseProtocol):
25
25
  )
26
26
 
27
27
  def encode(self, frame: OutboundFrame) -> bytes:
28
- payload_bytes = (
29
- self.serializer.encode(frame.payload)
30
- if frame.payload is not None
31
- else b""
32
- )
28
+ payload_bytes = self.serializer.encode(frame.payload) if frame.payload is not None else b""
33
29
 
34
30
  flags = 0
35
31
 
@@ -52,9 +48,7 @@ class TcpProtocol(BaseProtocol):
52
48
 
53
49
  packed_packet = self.framer.unpack(raw)
54
50
  if not packed_packet:
55
- return InboundFrame(
56
- opcode=0, cmd=0, seq=None, payload=None, raw=None
57
- )
51
+ return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
58
52
 
59
53
  logger.debug(
60
54
  "tcp frame decoded header ver=%s cmd=%s seq=%s opcode=%s flags=%s payload_len=%s",
@@ -8,4 +8,5 @@ from .keyboards import InlineKeyboardAttachment
8
8
  from .photo import PhotoAttachment
9
9
  from .share import ShareAttachment
10
10
  from .sticker import StickerAttachment
11
+ from .unknown import UnknownAttachment
11
12
  from .video import VideoAttachment, VideoRequest
@@ -11,9 +11,9 @@ class AudioAttachment(CamelModel):
11
11
  """Аудио-вложение сообщения.
12
12
 
13
13
  :ivar duration: Длительность аудио.
14
- :vartype duration: int
14
+ :vartype duration: int | None
15
15
  :ivar audio_id: ID аудио.
16
- :vartype audio_id: int
16
+ :vartype audio_id: int | None
17
17
  :ivar wave: Данные waveform.
18
18
  :vartype wave: str | None
19
19
  :ivar transcription_status: Статус транскрибации.
@@ -26,8 +26,8 @@ class AudioAttachment(CamelModel):
26
26
  :vartype token: str | None
27
27
  """
28
28
 
29
- duration: int
30
- audio_id: int
29
+ duration: int | None = None
30
+ audio_id: int | None = None
31
31
  wave: str | None = None
32
32
  transcription_status: TranscriptionStatus | None = None
33
33
  url: str | None = None
@@ -14,6 +14,7 @@ class AttachmentType(str, Enum):
14
14
  CALL = "CALL"
15
15
  SHARE = "SHARE"
16
16
  INLINE_KEYBOARD = "INLINE_KEYBOARD"
17
+ UNKNOWN = "UNKNOWN"
17
18
 
18
19
 
19
20
  class TranscriptionStatus(str, Enum):
@@ -0,0 +1,37 @@
1
+ from typing import Any
2
+
3
+ from pydantic import Field, model_validator
4
+
5
+ from pymax.types.domain.base import CamelModel
6
+
7
+ from .enums import AttachmentType
8
+
9
+ KNOWN_ATTACHMENT_TYPES = {
10
+ attachment_type.value
11
+ for attachment_type in AttachmentType
12
+ if attachment_type != AttachmentType.UNKNOWN
13
+ }
14
+
15
+
16
+ class UnknownAttachment(CamelModel):
17
+ """Вложение неизвестного типа.
18
+
19
+ :ivar type: Тип вложения.
20
+ :vartype type: str
21
+ """
22
+
23
+ type: str = Field(alias="_type")
24
+
25
+ @model_validator(mode="before")
26
+ @classmethod
27
+ def reject_known_attachment_type(cls, value: Any) -> Any:
28
+ if not isinstance(value, dict):
29
+ return value
30
+
31
+ attachment_type = value.get("_type", value.get("type"))
32
+ if attachment_type in KNOWN_ATTACHMENT_TYPES:
33
+ raise ValueError(
34
+ "Known attachment type should be parsed by its own model"
35
+ )
36
+
37
+ return value
@@ -31,7 +31,7 @@ class VideoAttachment(CamelModel):
31
31
  :ivar video_id: ID видео.
32
32
  :vartype video_id: int
33
33
  :ivar duration: Длительность видео.
34
- :vartype duration: int
34
+ :vartype duration: int | None
35
35
  :ivar preview_data: Данные превью.
36
36
  :vartype preview_data: bytes
37
37
  :ivar type: Тип вложения.
@@ -47,7 +47,7 @@ class VideoAttachment(CamelModel):
47
47
  height: int
48
48
  width: int
49
49
  video_id: int
50
- duration: int
50
+ duration: int | None = None
51
51
  preview_data: bytes
52
52
  type: Literal[AttachmentType.VIDEO] = Field(alias="_type")
53
53
  thumbnail: str