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.
- maxapi_python-2.1.3/.github/workflows/publish.yml +64 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/PKG-INFO +1 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/conf.py +14 -5
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/index.rst +2 -0
- maxapi_python-2.1.3/docs/release-2-1-2.rst +32 -0
- maxapi_python-2.1.3/docs/release-2-1-3.rst +48 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/pyproject.toml +1 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/__init__.py +1 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/payloads.py +1 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/service.py +3 -7
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/app.py +26 -19
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/client.py +1 -4
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/connection.py +46 -19
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/photo.py +4 -2
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/logging.py +33 -3
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/payload.py +22 -42
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/protocol.py +2 -8
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/__init__.py +1 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/audio.py +4 -4
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/enums.py +1 -0
- maxapi_python-2.1.3/src/pymax/types/domain/attachments/unknown.py +37 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/video.py +2 -2
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/element.py +3 -3
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/folder.py +0 -6
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/login.py +7 -19
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/message.py +3 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_chat_user_self_session_services.py +1 -1
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/app/test_app_runtime.py +63 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/connection/test_connection.py +51 -1
- maxapi_python-2.1.3/tests/domain/test_message_models.py +105 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/files/test_files_and_formatting.py +6 -0
- maxapi_python-2.1.3/tests/test_logging.py +169 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/uv.lock +1 -1
- maxapi_python-2.1.1/.github/workflows/publish.yml +0 -84
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.github/pull_request_template.md +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.gitignore +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/.pre-commit-config.yaml +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/LICENSE +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/README.md +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/_static/.gitkeep +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/account.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/auth.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/client.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/files.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/api/router.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/auth.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/chats.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/client.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/examples.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/faq.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/files.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/formatting.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/getting-started.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/messages.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/release-2-1-0.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/release-2-1-1.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/router.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/troubleshooting.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/audio_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/call_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/chat.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/contact_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/control_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/element.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/enums.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/file_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder_list.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/folder_update.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/index.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/inline_keyboard_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/message.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/message_delete_event.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/name.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/photo_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/profile.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/reaction_counter.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/reaction_info.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/read_state.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/session.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/share_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sticker_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sync_overrides.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/sync_state.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/user.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/types/video_attachment.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/docs/users.rst +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/auth/types.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/bots/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/chats/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/facade.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/messages/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/response.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/self/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/session/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/uploads/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/api/users/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/email.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/providers.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/qr.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/auth/sms.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/client_web.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/config.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/pending.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/tcp.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/connection/readers/ws.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/dispatcher.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/mapping.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/resolvers.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/dispatch/router.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/exceptions.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/file.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/static.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/files/video.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/formatting/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/formatting/markdown.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/auth.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/bots.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/chat.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/message.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/protocol.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/self.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/infra/user.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/compression.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/tcp/framing.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/ws/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/protocol/ws/protocol.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/py.typed +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/routers.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/protocol.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/session/store.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/navigation.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/payloads.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/telemetry/service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/tcp.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/transport/websocket.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/call.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/contact.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/control.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/file.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/keyboards/inline.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/photo.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/share.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/attachments/sticker.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/auth.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/base.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/bots.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/chat.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/enums.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/error.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/member.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/name.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/presence.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/profile.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/session.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/sync.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/domain/user.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/file.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/message.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/src/pymax/types/events/video.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/__init__.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_auth_service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_message_service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/api/test_upload_service.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/auth/test_auth_flows.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/conftest.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/connection/test_readers_and_transports.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/dispatch/test_dispatcher.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/domain/test_bound_models.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/protocol/test_protocols.py +0 -0
- {maxapi_python-2.1.1 → maxapi_python-2.1.3}/tests/session/test_store.py +0 -0
- {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
|
|
@@ -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":
|
|
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,
|
|
107
|
-
"
|
|
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
|
|
|
@@ -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``.
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
156
|
-
except TimeoutError as e:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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())
|