maxapi-python 2.1.1__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 (227) hide show
  1. maxapi_python-2.1.3/.github/workflows/publish.yml +64 -0
  2. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/PKG-INFO +1 -1
  3. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/conf.py +14 -5
  4. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/index.rst +2 -0
  5. maxapi_python-2.1.3/docs/release-2-1-2.rst +32 -0
  6. maxapi_python-2.1.3/docs/release-2-1-3.rst +48 -0
  7. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/pyproject.toml +1 -1
  8. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/__init__.py +1 -1
  9. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/payloads.py +1 -1
  10. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/service.py +3 -7
  11. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/app.py +26 -19
  12. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/client.py +1 -4
  13. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/connection.py +46 -19
  14. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/photo.py +4 -2
  15. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/logging.py +33 -3
  16. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/payload.py +22 -42
  17. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/protocol.py +2 -8
  18. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/__init__.py +1 -0
  19. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/audio.py +4 -4
  20. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/enums.py +1 -0
  21. maxapi_python-2.1.3/src/pymax/types/domain/attachments/unknown.py +37 -0
  22. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/video.py +2 -2
  23. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/element.py +3 -3
  24. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/folder.py +0 -6
  25. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/login.py +7 -19
  26. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/message.py +3 -1
  27. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_chat_user_self_session_services.py +1 -1
  28. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/app/test_app_runtime.py +63 -0
  29. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/connection/test_connection.py +51 -1
  30. maxapi_python-2.1.3/tests/domain/test_message_models.py +105 -0
  31. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/files/test_files_and_formatting.py +6 -0
  32. maxapi_python-2.1.3/tests/test_logging.py +169 -0
  33. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/uv.lock +1 -1
  34. maxapi_python-2.1.1/.github/workflows/publish.yml +0 -84
  35. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  36. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  37. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
  38. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/pull_request_template.md +0 -0
  39. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.gitignore +0 -0
  40. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.pre-commit-config.yaml +0 -0
  41. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/LICENSE +0 -0
  42. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/README.md +0 -0
  43. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/_static/.gitkeep +0 -0
  44. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/account.rst +0 -0
  45. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/auth.rst +0 -0
  46. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/client.rst +0 -0
  47. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/files.rst +0 -0
  48. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/router.rst +0 -0
  49. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/auth.rst +0 -0
  50. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/chats.rst +0 -0
  51. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/client.rst +0 -0
  52. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/examples.rst +0 -0
  53. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/faq.rst +0 -0
  54. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/files.rst +0 -0
  55. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/formatting.rst +0 -0
  56. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/getting-started.rst +0 -0
  57. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/messages.rst +0 -0
  58. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/release-2-1-0.rst +0 -0
  59. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/release-2-1-1.rst +0 -0
  60. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/router.rst +0 -0
  61. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/troubleshooting.rst +0 -0
  62. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/audio_attachment.rst +0 -0
  63. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/call_attachment.rst +0 -0
  64. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/chat.rst +0 -0
  65. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/contact_attachment.rst +0 -0
  66. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/control_attachment.rst +0 -0
  67. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/element.rst +0 -0
  68. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/enums.rst +0 -0
  69. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/file_attachment.rst +0 -0
  70. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder.rst +0 -0
  71. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder_list.rst +0 -0
  72. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder_update.rst +0 -0
  73. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/index.rst +0 -0
  74. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/inline_keyboard_attachment.rst +0 -0
  75. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/message.rst +0 -0
  76. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/message_delete_event.rst +0 -0
  77. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/name.rst +0 -0
  78. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/photo_attachment.rst +0 -0
  79. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/profile.rst +0 -0
  80. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/reaction_counter.rst +0 -0
  81. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/reaction_info.rst +0 -0
  82. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/read_state.rst +0 -0
  83. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/session.rst +0 -0
  84. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/share_attachment.rst +0 -0
  85. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sticker_attachment.rst +0 -0
  86. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sync_overrides.rst +0 -0
  87. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sync_state.rst +0 -0
  88. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/user.rst +0 -0
  89. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/video_attachment.rst +0 -0
  90. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/users.rst +0 -0
  91. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/__init__.py +0 -0
  92. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/__init__.py +0 -0
  93. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/enums.py +0 -0
  94. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/payloads.py +0 -0
  95. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/service.py +0 -0
  96. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/types.py +0 -0
  97. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/__init__.py +0 -0
  98. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/__init__.py +0 -0
  99. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/enums.py +0 -0
  100. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/payloads.py +0 -0
  101. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/service.py +0 -0
  102. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/facade.py +0 -0
  103. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/__init__.py +0 -0
  104. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/enums.py +0 -0
  105. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/payloads.py +0 -0
  106. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/service.py +0 -0
  107. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/models.py +0 -0
  108. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/response.py +0 -0
  109. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/__init__.py +0 -0
  110. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/enums.py +0 -0
  111. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/payloads.py +0 -0
  112. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/service.py +0 -0
  113. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/__init__.py +0 -0
  114. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/enums.py +0 -0
  115. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/payloads.py +0 -0
  116. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/service.py +0 -0
  117. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/__init__.py +0 -0
  118. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/models.py +0 -0
  119. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/payloads.py +0 -0
  120. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/service.py +0 -0
  121. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/__init__.py +0 -0
  122. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/enums.py +0 -0
  123. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/payloads.py +0 -0
  124. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/service.py +0 -0
  125. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/__init__.py +0 -0
  126. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/base.py +0 -0
  127. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/email.py +0 -0
  128. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/models.py +0 -0
  129. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/providers.py +0 -0
  130. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/qr.py +0 -0
  131. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/service.py +0 -0
  132. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/sms.py +0 -0
  133. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/base.py +0 -0
  134. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/client_web.py +0 -0
  135. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/config.py +0 -0
  136. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/__init__.py +0 -0
  137. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/pending.py +0 -0
  138. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/__init__.py +0 -0
  139. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/base.py +0 -0
  140. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/tcp.py +0 -0
  141. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/ws.py +0 -0
  142. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/__init__.py +0 -0
  143. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/dispatcher.py +0 -0
  144. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/enums.py +0 -0
  145. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/mapping.py +0 -0
  146. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/resolvers.py +0 -0
  147. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/router.py +0 -0
  148. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/exceptions.py +0 -0
  149. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/__init__.py +0 -0
  150. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/base.py +0 -0
  151. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/file.py +0 -0
  152. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/static.py +0 -0
  153. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/video.py +0 -0
  154. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/formatting/__init__.py +0 -0
  155. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/formatting/markdown.py +0 -0
  156. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/__init__.py +0 -0
  157. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/auth.py +0 -0
  158. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/base.py +0 -0
  159. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/bots.py +0 -0
  160. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/chat.py +0 -0
  161. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/message.py +0 -0
  162. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/protocol.py +0 -0
  163. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/self.py +0 -0
  164. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/user.py +0 -0
  165. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/__init__.py +0 -0
  166. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/base.py +0 -0
  167. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/enums.py +0 -0
  168. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/models.py +0 -0
  169. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/__init__.py +0 -0
  170. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/compression.py +0 -0
  171. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/framing.py +0 -0
  172. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/ws/__init__.py +0 -0
  173. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/ws/protocol.py +0 -0
  174. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/py.typed +0 -0
  175. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/routers.py +0 -0
  176. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/__init__.py +0 -0
  177. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/models.py +0 -0
  178. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/protocol.py +0 -0
  179. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/store.py +0 -0
  180. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/__init__.py +0 -0
  181. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/navigation.py +0 -0
  182. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/payloads.py +0 -0
  183. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/service.py +0 -0
  184. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/__init__.py +0 -0
  185. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/base.py +0 -0
  186. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/tcp.py +0 -0
  187. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/websocket.py +0 -0
  188. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/__init__.py +0 -0
  189. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/__init__.py +0 -0
  190. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/call.py +0 -0
  191. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/contact.py +0 -0
  192. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/control.py +0 -0
  193. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/file.py +0 -0
  194. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/__init__.py +0 -0
  195. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/inline.py +0 -0
  196. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/photo.py +0 -0
  197. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/share.py +0 -0
  198. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/sticker.py +0 -0
  199. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/auth.py +0 -0
  200. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/base.py +0 -0
  201. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/bots.py +0 -0
  202. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/chat.py +0 -0
  203. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/enums.py +0 -0
  204. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/error.py +0 -0
  205. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/member.py +0 -0
  206. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/name.py +0 -0
  207. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/presence.py +0 -0
  208. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/profile.py +0 -0
  209. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/session.py +0 -0
  210. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/sync.py +0 -0
  211. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/user.py +0 -0
  212. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/__init__.py +0 -0
  213. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/file.py +0 -0
  214. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/message.py +0 -0
  215. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/video.py +0 -0
  216. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/__init__.py +0 -0
  217. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_auth_service.py +0 -0
  218. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_message_service.py +0 -0
  219. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_upload_service.py +0 -0
  220. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/auth/test_auth_flows.py +0 -0
  221. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/conftest.py +0 -0
  222. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/connection/test_readers_and_transports.py +0 -0
  223. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/dispatch/test_dispatcher.py +0 -0
  224. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/domain/test_bound_models.py +0 -0
  225. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/protocol/test_protocols.py +0 -0
  226. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/session/test_store.py +0 -0
  227. {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/telemetry/test_telemetry.py +0 -0
@@ -0,0 +1,64 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: release-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ package:
17
+ name: Build package
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@v6
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v8.1.0
26
+ with:
27
+ enable-cache: true
28
+
29
+ - name: Build package distributions
30
+ run: uv build
31
+
32
+ - name: Validate package distributions
33
+ run: uv run twine check dist/*
34
+
35
+ - name: Upload package distributions
36
+ uses: actions/upload-artifact@v4
37
+ with:
38
+ name: package-distributions
39
+ path: dist/
40
+
41
+ publish:
42
+ name: Publish to PyPI
43
+ runs-on: ubuntu-latest
44
+ needs: [package]
45
+
46
+ environment:
47
+ name: pypi
48
+
49
+ permissions:
50
+ contents: read
51
+ id-token: write
52
+
53
+ steps:
54
+ - name: Download package distributions
55
+ uses: actions/download-artifact@v4
56
+ with:
57
+ name: package-distributions
58
+ path: dist/
59
+
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@v8.1.0
62
+
63
+ - name: Publish package to PyPI
64
+ run: uv publish
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 2.1.1
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,8 @@ PyMax - асинхронная Python-библиотека для Max API. Он
22
22
  :maxdepth: 1
23
23
  :caption: Новости
24
24
 
25
+ release-2-1-3
26
+ release-2-1-2
25
27
  release-2-1-1
26
28
  release-2-1-0
27
29
 
@@ -0,0 +1,32 @@
1
+ PyMax 2.1.2
2
+ ===========
3
+
4
+ Изменения относительно ``2.1.1``.
5
+
6
+ Добавлено
7
+ ---------
8
+
9
+ * ``get_bot_init_data()`` теперь можно вызвать без ``chat_id`` для сценариев,
10
+ где Max запускает web app вне конкретного чата.
11
+
12
+ Исправлено
13
+ ----------
14
+
15
+ * ``ExtraConfig.request_timeout`` снова применяется к API-запросам по
16
+ умолчанию, а явный ``timeout`` в низкоуровневом вызове сохраняет приоритет.
17
+ * Login-ответ без нового ``token`` больше не ломает запуск клиента и не
18
+ перезаписывает сохраненный токен пустым значением.
19
+ * ``FolderList`` больше не переопределяет pydantic-итератор и не ломает
20
+ ``dict(...)`` / стандартную сериализацию модели.
21
+
22
+ Изменилось
23
+ ----------
24
+
25
+ * Publish workflow упрощен: сборка, проверка дистрибутивов и публикация в PyPI
26
+ теперь разделены на понятные шаги с artifact handoff.
27
+
28
+ Миграция
29
+ --------
30
+
31
+ * Если код итерировал ``FolderList`` напрямую, замените это на
32
+ ``folder_list.folders``.
@@ -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.1"
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.1"
1
+ __version__ = "2.1.3"
2
2
 
3
3
 
4
4
  from .auth import (
@@ -3,5 +3,5 @@ from pymax.api.models import CamelModel
3
3
 
4
4
  class RequestInitDataPayload(CamelModel):
5
5
  bot_id: int
6
- chat_id: int
6
+ chat_id: int | None = None
7
7
  start_param: str | None = None
@@ -23,13 +23,9 @@ class BotsService:
23
23
  async def get_init_data(
24
24
  self,
25
25
  bot_id: int,
26
- chat_id: int,
26
+ chat_id: int | None = None,
27
27
  start_param: str | None = None,
28
28
  ) -> InitData:
29
- frame = RequestInitDataPayload(
30
- bot_id=bot_id, chat_id=chat_id, start_param=start_param
31
- )
32
- response = await self.app.invoke(
33
- Opcode.WEB_APP_INIT_DATA, frame.to_payload()
34
- )
29
+ frame = RequestInitDataPayload(bot_id=bot_id, chat_id=chat_id, start_param=start_param)
30
+ response = await self.app.invoke(Opcode.WEB_APP_INIT_DATA, frame.to_payload())
35
31
  return require_payload_model(response, InitData)
@@ -33,9 +33,7 @@ class App(Generic[ClientT]):
33
33
  self.dispatcher: Dispatcher[ClientT] = Dispatcher(self, root_router)
34
34
  self.api = ApiFacade(self)
35
35
  self.config = config
36
- self.store = self.config.store or SessionStore(
37
- config.work_dir, config.session_name
38
- )
36
+ self.store = self.config.store or SessionStore(config.work_dir, config.session_name)
39
37
  self.auth_flow = auth_flow
40
38
 
41
39
  self.me: Profile | None = None
@@ -51,6 +49,7 @@ class App(Generic[ClientT]):
51
49
  self._telemetry = TelemetryService(self) if config.telemetry else None
52
50
 
53
51
  self.connection.on_event = self.on_event
52
+ self.connection.on_close = self.on_connection_lost
54
53
  logger.debug(
55
54
  "app initialized session=%s work_dir=%s auth_flow=%s",
56
55
  config.session_name,
@@ -76,18 +75,14 @@ class App(Generic[ClientT]):
76
75
  await self.connection.open()
77
76
 
78
77
  handshake_device_id = (
79
- session_data.device_id
80
- if session_data
81
- else self.config.device.device_id
78
+ session_data.device_id if session_data else self.config.device.device_id
82
79
  )
83
80
  logger.debug("running handshake")
84
81
  await self.handshake(handshake_device_id)
85
82
  except (ConnectionError, EOFError, OSError, TimeoutError) as e:
86
83
  logger.exception("failed to connect or handshake")
87
84
  await self.connection.close()
88
- raise ConnectionError(
89
- f"Failed to connect and handshake: {e}"
90
- ) from e
85
+ raise ConnectionError(f"Failed to connect and handshake: {e}") from e
91
86
 
92
87
  self._ping_task = asyncio.create_task(self._ping_loop())
93
88
 
@@ -108,9 +103,7 @@ class App(Generic[ClientT]):
108
103
 
109
104
  if not auth_result.token:
110
105
  logger.error("authentication finished without token")
111
- raise RuntimeError(
112
- "Authentication failed: no token received"
113
- )
106
+ raise RuntimeError("Authentication failed: no token received")
114
107
 
115
108
  await self.store.save_session(
116
109
  session_data := SessionInfo(
@@ -135,7 +128,7 @@ class App(Generic[ClientT]):
135
128
  self.config.device.user_agent,
136
129
  )
137
130
 
138
- if response.token != self.session.token:
131
+ if response.token is not None and response.token != self.session.token:
139
132
  await self.store.update_token(self.session.token, response.token)
140
133
  self.session.token = response.token
141
134
 
@@ -182,6 +175,7 @@ class App(Generic[ClientT]):
182
175
  await self.dispatcher.stop_startup_tasks()
183
176
  await self.connection.close()
184
177
  await self.store.close()
178
+
185
179
  self.started = False
186
180
 
187
181
  async def invoke(
@@ -189,7 +183,7 @@ class App(Generic[ClientT]):
189
183
  opcode: int,
190
184
  payload: dict[str, Any],
191
185
  cmd: int = Command.REQUEST,
192
- timeout: float | None = 30.0,
186
+ timeout: float | None = None,
193
187
  compress: bool = False,
194
188
  ) -> InboundFrame:
195
189
  seq = self.connection.next_seq()
@@ -211,10 +205,9 @@ class App(Generic[ClientT]):
211
205
  payload_keys,
212
206
  )
213
207
  logger.debug("Request data=%s", frame.model_dump())
214
- response = await self.connection.request(frame, timeout=timeout)
215
- response_keys = (
216
- sorted(response.payload.keys()) if response.payload else []
217
- )
208
+ request_timeout = self.config.request_timeout if timeout is None else timeout
209
+ response = await self.connection.request(frame, timeout=request_timeout)
210
+ response_keys = sorted(response.payload.keys()) if response.payload else []
218
211
  logger.debug(
219
212
  "response opcode=%s cmd=%s seq=%s payload_keys=%s",
220
213
  response.opcode,
@@ -238,9 +231,23 @@ class App(Generic[ClientT]):
238
231
  except asyncio.CancelledError:
239
232
  raise
240
233
  except Exception as e:
241
- logger.exception("ping loop failed; closing transport")
234
+ logger.warning("ping loop failed; closing transport: %s", e)
242
235
  await self.connection.fail(ConnectionError(f"Ping failed: {e}"))
243
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
+
244
251
  def _build_api_error(self, response: InboundFrame) -> ApiError:
245
252
  try:
246
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())