python-trueconf-bot 1.2.3.dev1__tar.gz → 1.4.0b1__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.
- python_trueconf_bot-1.4.0b1/.opencode/plans/2026-05-25-fsm-implementation.md +739 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/PKG-INFO +2 -1
- python_trueconf_bot-1.4.0b1/docs/context7.json +4 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/examples/index.md +182 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/features.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/index.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/learn/enums.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/learn/files.md +1 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/learn/filters.md +1 -1
- python_trueconf_bot-1.4.0b1/docs/en/learn/fsm.md +445 -0
- python_trueconf_bot-1.4.0b1/docs/en/learn/getting-started.md +272 -0
- python_trueconf_bot-1.4.0b1/docs/en/learn/middleware.md +283 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/learn/parse-messages.md +5 -0
- python_trueconf_bot-1.4.0b1/docs/en/learn/restrictions.md +45 -0
- python_trueconf_bot-1.4.0b1/docs/en/learn/send_message.md +224 -0
- python_trueconf_bot-1.4.0b1/docs/en/learn/shortcuts.md +71 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/learn/types.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Dispatcher.md +6 -3
- python_trueconf_bot-1.4.0b1/docs/en/reference/Exceptions.md +5 -0
- python_trueconf_bot-1.4.0b1/docs/en/reference/Formatting.md +5 -0
- python_trueconf_bot-1.4.0b1/docs/en/release_notes.md +260 -0
- python_trueconf_bot-1.4.0b1/docs/overrides/main.html +28 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/examples/index.md +183 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/features.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/index.md +7 -2
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/enums.md +5 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/files.md +1 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/filters.md +1 -1
- python_trueconf_bot-1.4.0b1/docs/ru/learn/fsm.md +445 -0
- python_trueconf_bot-1.4.0b1/docs/ru/learn/getting-started.md +270 -0
- python_trueconf_bot-1.4.0b1/docs/ru/learn/middleware.md +283 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/parse-messages.md +1 -1
- python_trueconf_bot-1.4.0b1/docs/ru/learn/restrictions.md +46 -0
- python_trueconf_bot-1.4.0b1/docs/ru/learn/send_message.md +224 -0
- python_trueconf_bot-1.4.0b1/docs/ru/learn/shortcuts.md +72 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/types.md +5 -0
- python_trueconf_bot-1.4.0b1/docs/ru/release_notes.md +245 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/mkdocs.yml +9 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/pyproject.toml +2 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/PKG-INFO +2 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/SOURCES.txt +33 -1
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/requires.txt +1 -0
- python_trueconf_bot-1.4.0b1/test5.py +70 -0
- python_trueconf_bot-1.4.0b1/tests/test_fsm.py +907 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/__init__.py +2 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/_version.py +3 -3
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/client/bot.py +193 -37
- python_trueconf_bot-1.4.0b1/trueconf/dispatcher/dispatcher.py +149 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/dispatcher/router.py +156 -38
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/exceptions.py +107 -10
- python_trueconf_bot-1.4.0b1/trueconf/fsm/__init__.py +20 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/context.py +57 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/filters.py +31 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/key_builder.py +39 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/manager.py +36 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/middleware.py +24 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/state.py +135 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/__init__.py +4 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/base.py +36 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/memory.py +57 -0
- python_trueconf_bot-1.4.0b1/trueconf/fsm/strategy.py +36 -0
- python_trueconf_bot-1.4.0b1/trueconf/middleware.py +78 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/message.py +57 -21
- python_trueconf_bot-1.4.0b1/trueconf/utils/_ssl.py +44 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_token.py +4 -4
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_version_checker.py +6 -0
- python_trueconf_bot-1.4.0b1/trueconf/utils/formatting/__init__.py +21 -0
- python_trueconf_bot-1.4.0b1/trueconf/utils/formatting/decorations.py +87 -0
- python_trueconf_bot-1.4.0b1/trueconf/utils/formatting/text.py +252 -0
- python_trueconf_bot-1.2.3.dev1/docs/en/learn/getting-started.md +0 -104
- python_trueconf_bot-1.2.3.dev1/docs/en/release_notes.md +0 -56
- python_trueconf_bot-1.2.3.dev1/docs/overrides/main.html +0 -8
- python_trueconf_bot-1.2.3.dev1/docs/ru/learn/getting-started.md +0 -105
- python_trueconf_bot-1.2.3.dev1/docs/ru/release_notes.md +0 -45
- python_trueconf_bot-1.2.3.dev1/trueconf/dispatcher/dispatcher.py +0 -63
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/.github/workflows/docs.yml +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/.github/workflows/release-test.yml +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/.github/workflows/release.yml +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/.gitignore +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/README-PyPI.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/README-ru.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/README.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/assets/head_en.png +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/assets/head_ru.png +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/assets/logo.svg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/img/head.png +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/img/router_scheme_en.svg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Bot.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Enums.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Responses.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Router.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Types.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/en/reference/index.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/hooks/llms_txt.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/img/chatbot.svg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/img/favicon.ico +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/img/logo.svg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/img/head.png +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/ru/img/router_scheme_ru.svg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/docs/stylesheets/extra.css +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/README-ru.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/README.md +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/add_big_list_users_to_channel.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/filters.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/report_bot.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/update_token.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/examples/webhook.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/dependency_links.txt +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/top_level.txt +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/requirements-docs.txt +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/requirements.txt +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/setup.cfg +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/client/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/client/context_controller.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/client/session.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/dispatcher/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/aouth_error.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/chat_participant_role.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/chat_type.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/envelope_author_type.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/file_ready_state.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/incoming_update_method.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/message_type.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/parse_mode.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/survey_type.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/enums/update_type.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/base.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/command.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/instance_of.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/filters/method.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/loggers.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/add_participant_to_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/auth.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/base.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/change_participant_role.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/clear_chat_history.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_channel.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_favorites_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_group_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_p2p_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_chat_avatar.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_chat_title.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_survey.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/forward_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_by_id.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_history.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_participants.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chats.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_file_info.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_file_upload_limits.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_message_by_id.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_user_display_name.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/has_chat_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_participant_from_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_file.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_survey.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/subscribe_file_progress.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/unsubscribe_file_progress.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/methods/upload_file.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/py.typed +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/author_box.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/chat_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/attachment.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/base.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/chat_created.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/document.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/edit_chat_title.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/forward_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/location.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/photo.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/remove_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/sticker.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/survey.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/text.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/video.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/input_file.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/last_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/parser.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/added_chat_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/changed_file_upload_limits.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/changed_participant_role.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/cleared_chat_history.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_channel.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_favorites_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_group_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_personal_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_chat_avatar.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_chat_title.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_chat.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_chat_participant.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_message.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/uploading_progress.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/add_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/api_error.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/auth_response_payload.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/change_participant_role_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/clear_chat_history_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_channel_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_favorites_chat_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_group_chat_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_p2p_chat_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_chat_avatar_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_chat_title_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_message_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_survey_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/forward_message_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_by_id_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_history_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_participants_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chats_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_file_info_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_file_upload_limits_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_message_by_id_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_user_display_name_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/has_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_chat_participant_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_chat_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_message_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_file_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_message_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_survey_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/subscribe_file_progress_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/unsubscribe_file_progress_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/upload_file_response.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/types/update.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/utils/__init__.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_generate_secret_for_survey.py +0 -0
- {python_trueconf_bot-1.2.3.dev1 → python_trueconf_bot-1.4.0b1}/trueconf/utils/split_text.py +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
# FSM Implementation Plan
|
|
2
|
+
|
|
3
|
+
Spec: `docs/superpowers/specs/2026-05-25-fsm-design.md`
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
11 steps. Bottom-up: storage → state → key → context → manager → filter → middleware → dispatcher/router integration → exports → tests → verify.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Step 1: Create `trueconf/fsm/storage/base.py` — BaseStorage ABC
|
|
12
|
+
|
|
13
|
+
**File**: `trueconf/fsm/storage/base.py` (new)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from typing import Any
|
|
18
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseStorage(ABC):
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def get_state(self, key: StorageKey) -> str | None: ...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None: ...
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]: ...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None: ...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def update_data(self, key: StorageKey, updates: dict[str, Any]) -> dict[str, Any]: ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def clear(self, key: StorageKey) -> None: ...
|
|
39
|
+
|
|
40
|
+
async def close(self) -> None:
|
|
41
|
+
pass
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Note**: Imports `StorageKey` from `key_builder` which doesn't exist yet. To avoid circular imports, use `TYPE_CHECKING` guard or create key_builder first. Since key_builder has no dependencies on storage, create key_builder first (Step 3) or use a forward reference.
|
|
45
|
+
|
|
46
|
+
Actually — create `key_builder.py` first (Step 3) since it has no dependencies. Then `storage/base.py` can import from it.
|
|
47
|
+
|
|
48
|
+
**Revised order**: Steps 1 and 3 swap — key_builder before storage/base.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 2: Create `trueconf/fsm/key_builder.py` — StorageKey + KeyBuilder + DefaultKeyBuilder
|
|
53
|
+
|
|
54
|
+
**File**: `trueconf/fsm/key_builder.py` (new)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
from dataclasses import dataclass
|
|
59
|
+
from typing import Any, Protocol, runtime_checkable
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class StorageKey:
|
|
64
|
+
bot_id: str | None
|
|
65
|
+
chat_id: str
|
|
66
|
+
user_id: str
|
|
67
|
+
destiny: str = "default"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@runtime_checkable
|
|
71
|
+
class KeyBuilder(Protocol):
|
|
72
|
+
def build(self, bot: Any, event: Any) -> StorageKey: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DefaultKeyBuilder:
|
|
76
|
+
def build(self, bot: Any, event: Any) -> StorageKey:
|
|
77
|
+
bot_id = getattr(bot, "me_id", None) or getattr(bot, "id", None)
|
|
78
|
+
|
|
79
|
+
chat_id = (
|
|
80
|
+
getattr(event, "chat_id", None)
|
|
81
|
+
or getattr(getattr(event, "chat", None), "id", None)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
user = getattr(event, "from_user", None) or getattr(event, "author", None)
|
|
85
|
+
user_id = getattr(user, "id", None) if user else None
|
|
86
|
+
|
|
87
|
+
if chat_id is None or user_id is None:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
f"Cannot build FSM StorageKey: event of type {type(event).__name__} "
|
|
90
|
+
f"has no chat_id ({chat_id}) or user_id ({user_id}). "
|
|
91
|
+
f"Provide a custom KeyBuilder to Dispatcher.setup_fsm()."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return StorageKey(bot_id=bot_id, chat_id=str(chat_id), user_id=str(user_id))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
No external dependencies. Pure dataclass + protocol.
|
|
98
|
+
|
|
99
|
+
**Note**: `chat_id` and `user_id` are always strings. In TrueConf API, IDs are strings like `"user@server"`. `DefaultKeyBuilder` converts to `str` to normalize input types.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Step 3: Create `trueconf/fsm/storage/base.py` — BaseStorage ABC
|
|
104
|
+
|
|
105
|
+
**File**: `trueconf/fsm/storage/base.py` (new)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from abc import ABC, abstractmethod
|
|
109
|
+
from typing import Any
|
|
110
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BaseStorage(ABC):
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def get_state(self, key: StorageKey) -> str | None: ...
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None: ...
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]: ...
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None: ...
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
async def update_data(self, key: StorageKey, updates: dict[str, Any]) -> dict[str, Any]: ...
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
async def clear(self, key: StorageKey) -> None: ...
|
|
131
|
+
|
|
132
|
+
async def close(self) -> None:
|
|
133
|
+
pass
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Depends on: `key_builder.StorageKey` (Step 2).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Step 4: Create `trueconf/fsm/storage/memory.py` — MemoryStorage
|
|
141
|
+
|
|
142
|
+
**File**: `trueconf/fsm/storage/memory.py` (new)
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from __future__ import annotations
|
|
146
|
+
from dataclasses import dataclass, field
|
|
147
|
+
from typing import Any
|
|
148
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
149
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class _Record:
|
|
154
|
+
state: str | None = None
|
|
155
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class MemoryStorage(BaseStorage):
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
self._records: dict[StorageKey, _Record] = {}
|
|
161
|
+
|
|
162
|
+
def _get_or_create(self, key: StorageKey) -> _Record:
|
|
163
|
+
if key not in self._records:
|
|
164
|
+
self._records[key] = _Record()
|
|
165
|
+
return self._records[key]
|
|
166
|
+
|
|
167
|
+
async def get_state(self, key: StorageKey) -> str | None:
|
|
168
|
+
record = self._records.get(key)
|
|
169
|
+
return record.state if record else None
|
|
170
|
+
|
|
171
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None:
|
|
172
|
+
self._get_or_create(key).state = state
|
|
173
|
+
|
|
174
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]:
|
|
175
|
+
record = self._records.get(key)
|
|
176
|
+
return record.data.copy() if record else {}
|
|
177
|
+
|
|
178
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
|
|
179
|
+
self._get_or_create(key).data = data.copy()
|
|
180
|
+
|
|
181
|
+
async def update_data(self, key: StorageKey, updates: dict[str, Any]) -> dict[str, Any]:
|
|
182
|
+
record = self._get_or_create(key)
|
|
183
|
+
record.data.update(updates)
|
|
184
|
+
return record.data.copy()
|
|
185
|
+
|
|
186
|
+
async def clear(self, key: StorageKey) -> None:
|
|
187
|
+
self._records.pop(key, None)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Depends on: `storage.base.BaseStorage`, `key_builder.StorageKey`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Step 5: Create `trueconf/fsm/storage/__init__.py`
|
|
195
|
+
|
|
196
|
+
**File**: `trueconf/fsm/storage/__init__.py` (new)
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
200
|
+
from trueconf.fsm.storage.memory import MemoryStorage
|
|
201
|
+
|
|
202
|
+
__all__ = ("BaseStorage", "MemoryStorage")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Step 6: Create `trueconf/fsm/state.py` — State + StatesGroup
|
|
208
|
+
|
|
209
|
+
**File**: `trueconf/fsm/state.py` (new)
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from __future__ import annotations
|
|
213
|
+
from typing import Any
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class State:
|
|
217
|
+
def __init__(self, state: str | None = None) -> None:
|
|
218
|
+
self._custom_state = state
|
|
219
|
+
self._name: str | None = None
|
|
220
|
+
self._group: type[StatesGroup] | None = None
|
|
221
|
+
|
|
222
|
+
def bind(self, group: type[StatesGroup], name: str) -> None:
|
|
223
|
+
if self._group is not None or self._name is not None:
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
f"State '{self._name}' is already bound to {self._group}. "
|
|
226
|
+
f"Cannot rebind to {group.__name__}:{name}."
|
|
227
|
+
)
|
|
228
|
+
self._group = group
|
|
229
|
+
self._name = name
|
|
230
|
+
|
|
231
|
+
def __str__(self) -> str:
|
|
232
|
+
if self._custom_state is not None:
|
|
233
|
+
return self._custom_state
|
|
234
|
+
if self._group is None or self._name is None:
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
"State is not bound to a StatesGroup. "
|
|
237
|
+
"Use State inside a StatesGroup class definition, "
|
|
238
|
+
"or pass a custom string: State('my:state')."
|
|
239
|
+
)
|
|
240
|
+
return f"{self._group.__name__}:{self._name}"
|
|
241
|
+
|
|
242
|
+
def __repr__(self) -> str:
|
|
243
|
+
return f"State({str(self)!r})"
|
|
244
|
+
|
|
245
|
+
def __eq__(self, other: object) -> bool:
|
|
246
|
+
if isinstance(other, State):
|
|
247
|
+
return str(self) == str(other)
|
|
248
|
+
if isinstance(other, str):
|
|
249
|
+
return str(self) == other
|
|
250
|
+
return NotImplemented
|
|
251
|
+
|
|
252
|
+
def __hash__(self) -> int:
|
|
253
|
+
return hash(str(self))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class _StatesGroupMeta(type):
|
|
257
|
+
def __new__(
|
|
258
|
+
mcs,
|
|
259
|
+
name: str,
|
|
260
|
+
bases: tuple[type, ...],
|
|
261
|
+
namespace: dict[str, Any],
|
|
262
|
+
) -> _StatesGroupMeta:
|
|
263
|
+
state_names = [
|
|
264
|
+
key for key, value in namespace.items()
|
|
265
|
+
if isinstance(value, State)
|
|
266
|
+
]
|
|
267
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
268
|
+
states: list[State] = []
|
|
269
|
+
for key in state_names:
|
|
270
|
+
state = getattr(cls, key)
|
|
271
|
+
state.bind(cls, key)
|
|
272
|
+
states.append(state)
|
|
273
|
+
cls.__states__ = tuple(states)
|
|
274
|
+
return cls
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class StatesGroup(metaclass=_StatesGroupMeta):
|
|
278
|
+
__states__: tuple[State, ...] = ()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
No external dependencies.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Step 7: Create `trueconf/fsm/context.py` — FSMContext
|
|
286
|
+
|
|
287
|
+
**File**: `trueconf/fsm/context.py` (new)
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from __future__ import annotations
|
|
291
|
+
from typing import Any
|
|
292
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
293
|
+
from trueconf.fsm.state import State
|
|
294
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class FSMContext:
|
|
298
|
+
def __init__(self, storage: BaseStorage, key: StorageKey) -> None:
|
|
299
|
+
self._storage = storage
|
|
300
|
+
self._key = key
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def key(self) -> StorageKey:
|
|
304
|
+
return self._key
|
|
305
|
+
|
|
306
|
+
async def get_state(self) -> str | None:
|
|
307
|
+
return await self._storage.get_state(self._key)
|
|
308
|
+
|
|
309
|
+
async def set_state(self, state: State | str | None) -> None:
|
|
310
|
+
value = str(state) if isinstance(state, State) else state
|
|
311
|
+
await self._storage.set_state(self._key, value)
|
|
312
|
+
|
|
313
|
+
async def get_data(self) -> dict[str, Any]:
|
|
314
|
+
return await self._storage.get_data(self._key)
|
|
315
|
+
|
|
316
|
+
async def set_data(self, data: dict[str, Any]) -> None:
|
|
317
|
+
await self._storage.set_data(self._key, data)
|
|
318
|
+
|
|
319
|
+
async def update_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
320
|
+
return await self._storage.update_data(self._key, kwargs)
|
|
321
|
+
|
|
322
|
+
async def clear(self) -> None:
|
|
323
|
+
await self._storage.clear(self._key)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Depends on: `storage.base.BaseStorage`, `key_builder.StorageKey`, `state.State`.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Step 8: Create `trueconf/fsm/manager.py` — FSMManager
|
|
331
|
+
|
|
332
|
+
**File**: `trueconf/fsm/manager.py` (new)
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from __future__ import annotations
|
|
336
|
+
from typing import Any
|
|
337
|
+
from trueconf.fsm.context import FSMContext
|
|
338
|
+
from trueconf.fsm.key_builder import DefaultKeyBuilder, KeyBuilder
|
|
339
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
340
|
+
from trueconf.fsm.storage.memory import MemoryStorage
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class FSMManager:
|
|
344
|
+
def __init__(
|
|
345
|
+
self,
|
|
346
|
+
storage: BaseStorage | None = None,
|
|
347
|
+
key_builder: KeyBuilder | None = None,
|
|
348
|
+
) -> None:
|
|
349
|
+
self.storage: BaseStorage = storage or MemoryStorage()
|
|
350
|
+
self.key_builder: KeyBuilder = key_builder or DefaultKeyBuilder()
|
|
351
|
+
|
|
352
|
+
def get_context(self, bot: Any, event: Any) -> FSMContext:
|
|
353
|
+
key = self.key_builder.build(bot, event)
|
|
354
|
+
return FSMContext(self.storage, key)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Depends on: `context.FSMContext`, `key_builder.*`, `storage.*`.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Step 9: Create `trueconf/fsm/middleware.py` — FSMMiddleware
|
|
362
|
+
|
|
363
|
+
**File**: `trueconf/fsm/middleware.py` (new)
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
from __future__ import annotations
|
|
367
|
+
from typing import Any
|
|
368
|
+
from trueconf.middleware import BaseMiddleware
|
|
369
|
+
from trueconf.fsm.manager import FSMManager
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class FSMMiddleware(BaseMiddleware):
|
|
373
|
+
def __init__(self, fsm_manager: FSMManager) -> None:
|
|
374
|
+
self._manager = fsm_manager
|
|
375
|
+
|
|
376
|
+
async def __call__(
|
|
377
|
+
self,
|
|
378
|
+
handler: Any,
|
|
379
|
+
event: Any,
|
|
380
|
+
data: dict[str, Any],
|
|
381
|
+
) -> None:
|
|
382
|
+
bot = data.get("bot")
|
|
383
|
+
if bot is not None:
|
|
384
|
+
data["state"] = self._manager.get_context(bot, event)
|
|
385
|
+
return await handler(event, data)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Depends on: `middleware.BaseMiddleware`, `manager.FSMMiddleware`.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Step 10: Create `trueconf/fsm/filters.py` — StateFilter
|
|
393
|
+
|
|
394
|
+
**File**: `trueconf/fsm/filters.py` (new)
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
from __future__ import annotations
|
|
398
|
+
from typing import Any
|
|
399
|
+
from trueconf.fsm.context import FSMContext
|
|
400
|
+
from trueconf.fsm.state import State
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class StateFilter:
|
|
404
|
+
def __init__(self, *states: State | str | None) -> None:
|
|
405
|
+
self._states: set[str | None] = {
|
|
406
|
+
str(s) if isinstance(s, State) else s for s in states
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async def __call__(
|
|
410
|
+
self,
|
|
411
|
+
event: Any,
|
|
412
|
+
*,
|
|
413
|
+
state: FSMContext | None = None,
|
|
414
|
+
) -> bool:
|
|
415
|
+
if state is None:
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
"StateFilter requires FSMContext in data['state'], but it was not found. "
|
|
418
|
+
"Make sure FSMMiddleware is registered as outer middleware "
|
|
419
|
+
"before any StateFilter is evaluated."
|
|
420
|
+
)
|
|
421
|
+
current = await state.get_state()
|
|
422
|
+
return current in self._states
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Depends on: `context.FSMContext`, `state.State`.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Step 11: Create `trueconf/fsm/__init__.py` — public exports
|
|
430
|
+
|
|
431
|
+
**File**: `trueconf/fsm/__init__.py` (new)
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from trueconf.fsm.state import State, StatesGroup
|
|
435
|
+
from trueconf.fsm.context import FSMContext
|
|
436
|
+
from trueconf.fsm.filters import StateFilter
|
|
437
|
+
from trueconf.fsm.manager import FSMManager
|
|
438
|
+
from trueconf.fsm.key_builder import StorageKey, KeyBuilder, DefaultKeyBuilder
|
|
439
|
+
|
|
440
|
+
__all__ = (
|
|
441
|
+
"State",
|
|
442
|
+
"StatesGroup",
|
|
443
|
+
"FSMContext",
|
|
444
|
+
"StateFilter",
|
|
445
|
+
"FSMManager",
|
|
446
|
+
"StorageKey",
|
|
447
|
+
"KeyBuilder",
|
|
448
|
+
"DefaultKeyBuilder",
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Step 12: Modify `trueconf/dispatcher/dispatcher.py` — Dispatcher integration
|
|
455
|
+
|
|
456
|
+
**File**: `trueconf/dispatcher/dispatcher.py` (modify)
|
|
457
|
+
|
|
458
|
+
Changes:
|
|
459
|
+
1. Add imports for FSM types
|
|
460
|
+
2. Extend `__init__` with `storage`, `fsm_manager`, `key_builder` keyword args
|
|
461
|
+
3. Add `setup_fsm()` method
|
|
462
|
+
4. FSMMiddleware is inserted at position 0 of `_outer_middlewares`
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from __future__ import annotations
|
|
466
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List
|
|
467
|
+
from trueconf.filters.base import Event
|
|
468
|
+
from trueconf.dispatcher.router import Router
|
|
469
|
+
|
|
470
|
+
if TYPE_CHECKING:
|
|
471
|
+
from trueconf.fsm.manager import FSMManager
|
|
472
|
+
from trueconf.fsm.key_builder import KeyBuilder
|
|
473
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
474
|
+
|
|
475
|
+
MiddlewareHandler = Callable[[Event, Dict[str, Any]], Awaitable[None]]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class Dispatcher(Router):
|
|
479
|
+
def __init__(
|
|
480
|
+
self,
|
|
481
|
+
*,
|
|
482
|
+
storage: "BaseStorage | None" = None,
|
|
483
|
+
fsm_manager: "FSMManager | None" = None,
|
|
484
|
+
key_builder: "KeyBuilder | None" = None,
|
|
485
|
+
):
|
|
486
|
+
super().__init__(name="dispatcher")
|
|
487
|
+
self.routers: List[Router] = []
|
|
488
|
+
self.fsm: "FSMManager | None" = None
|
|
489
|
+
|
|
490
|
+
if fsm_manager is not None and storage is not None:
|
|
491
|
+
raise ValueError("Pass either fsm_manager or storage, not both")
|
|
492
|
+
|
|
493
|
+
if fsm_manager is not None:
|
|
494
|
+
self.setup_fsm(fsm_manager=fsm_manager)
|
|
495
|
+
elif storage is not None:
|
|
496
|
+
self.setup_fsm(storage=storage, key_builder=key_builder)
|
|
497
|
+
|
|
498
|
+
def setup_fsm(
|
|
499
|
+
self,
|
|
500
|
+
*,
|
|
501
|
+
fsm_manager: "FSMManager | None" = None,
|
|
502
|
+
storage: "BaseStorage | None" = None,
|
|
503
|
+
key_builder: "KeyBuilder | None" = None,
|
|
504
|
+
) -> "FSMManager":
|
|
505
|
+
from trueconf.fsm.manager import FSMManager
|
|
506
|
+
from trueconf.fsm.storage.memory import MemoryStorage
|
|
507
|
+
from trueconf.fsm.key_builder import DefaultKeyBuilder
|
|
508
|
+
from trueconf.fsm.middleware import FSMMiddleware
|
|
509
|
+
|
|
510
|
+
if self.fsm is not None:
|
|
511
|
+
raise RuntimeError(
|
|
512
|
+
"FSM is already configured for this Dispatcher. "
|
|
513
|
+
"Call setup_fsm() only once, or create a new Dispatcher."
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if fsm_manager is None:
|
|
517
|
+
fsm_manager = FSMManager(
|
|
518
|
+
storage=storage or MemoryStorage(),
|
|
519
|
+
key_builder=key_builder or DefaultKeyBuilder(),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
self.fsm = fsm_manager
|
|
523
|
+
self._outer_middlewares.insert(0, FSMMiddleware(fsm_manager))
|
|
524
|
+
return fsm_manager
|
|
525
|
+
|
|
526
|
+
# ... include_router and _feed_update unchanged ...
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Lazy imports** in `setup_fsm()` avoid circular import issues and keep FSM optional — if user never calls `setup_fsm()` or passes `storage=`, the FSM modules are never loaded.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Step 13: Modify `trueconf/dispatcher/router.py` — `_apply_filter` expansion + kwargs merge + State sugar
|
|
534
|
+
|
|
535
|
+
**File**: `trueconf/dispatcher/router.py` (modify)
|
|
536
|
+
|
|
537
|
+
### Change 1: `_register` — auto-wrap `State` with `StateFilter`
|
|
538
|
+
|
|
539
|
+
Add sugar so `@router.message(Form.name)` works as shorthand for `@router.message(StateFilter(Form.name))`:
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
# CURRENT (line 116):
|
|
543
|
+
def _register(self, filters: Tuple[FilterLike, ...]):
|
|
544
|
+
|
|
545
|
+
# NEW:
|
|
546
|
+
def _register(self, filters: Tuple[FilterLike, ...]):
|
|
547
|
+
# Sugar: State instances are auto-wrapped with StateFilter
|
|
548
|
+
from trueconf.fsm.state import State
|
|
549
|
+
from trueconf.fsm.filters import StateFilter
|
|
550
|
+
filters = tuple(
|
|
551
|
+
StateFilter(f) if isinstance(f, State) else f
|
|
552
|
+
for f in filters
|
|
553
|
+
)
|
|
554
|
+
# ... rest unchanged
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
This is backward-compatible — existing filter tuples without `State` are unchanged.
|
|
558
|
+
|
|
559
|
+
### Change 2: `_apply_filter` signature and body
|
|
560
|
+
|
|
561
|
+
```python
|
|
562
|
+
# CURRENT (line 232):
|
|
563
|
+
async def _apply_filter(self, f: Filter | Any, event: Event) -> bool:
|
|
564
|
+
|
|
565
|
+
# NEW:
|
|
566
|
+
async def _apply_filter(self, f: Filter | Any, event: Event, data: dict[str, Any] | None = None) -> bool:
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Full new body:
|
|
570
|
+
|
|
571
|
+
```python
|
|
572
|
+
async def _apply_filter(self, f: Filter | Any, event: Event, data: dict[str, Any] | None = None) -> bool:
|
|
573
|
+
"""Evaluate a filter against the event, passing matching kwargs from data."""
|
|
574
|
+
data = data or {}
|
|
575
|
+
|
|
576
|
+
if isinstance(f, MagicFilter):
|
|
577
|
+
try:
|
|
578
|
+
return bool(f.resolve(event))
|
|
579
|
+
except Exception:
|
|
580
|
+
return False
|
|
581
|
+
|
|
582
|
+
# Resolve which kwargs from data the filter accepts
|
|
583
|
+
kwargs: dict[str, Any] = {}
|
|
584
|
+
has_var_kwargs = False
|
|
585
|
+
try:
|
|
586
|
+
sig = inspect.signature(f)
|
|
587
|
+
for name, param in sig.parameters.items():
|
|
588
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
589
|
+
has_var_kwargs = True
|
|
590
|
+
continue
|
|
591
|
+
if name in data and param.kind in (
|
|
592
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
593
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
594
|
+
):
|
|
595
|
+
kwargs[name] = data[name]
|
|
596
|
+
except (ValueError, TypeError):
|
|
597
|
+
pass
|
|
598
|
+
|
|
599
|
+
# If filter accepts **kwargs, pass all remaining data
|
|
600
|
+
if has_var_kwargs:
|
|
601
|
+
kwargs.update({k: v for k, v in data.items() if k not in kwargs})
|
|
602
|
+
|
|
603
|
+
# Regular filters: let exceptions propagate (config errors must be explicit)
|
|
604
|
+
res = f(event, **kwargs) if kwargs else f(event)
|
|
605
|
+
|
|
606
|
+
if inspect.isawaitable(res):
|
|
607
|
+
res = await res
|
|
608
|
+
|
|
609
|
+
if isinstance(res, (bool, dict)):
|
|
610
|
+
return res
|
|
611
|
+
return bool(res)
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Key changes from original plan:**
|
|
615
|
+
- Regular filter exceptions are **NOT caught** — `RuntimeError` from `StateFilter` (missing FSMContext) propagates explicitly
|
|
616
|
+
- Only `MagicFilter` catches exceptions (attribute resolution failures are normal)
|
|
617
|
+
- `has_var_kwargs` check — filters with `**kwargs` receive all of `data`
|
|
618
|
+
|
|
619
|
+
### Change 3: `_core()` — pass `ctx` to `_apply_filter` and merge data into handler kwargs
|
|
620
|
+
|
|
621
|
+
In `_core()` (around lines 151-192), two changes:
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
# CURRENT line 162:
|
|
625
|
+
result = await self._apply_filter(f, evt)
|
|
626
|
+
|
|
627
|
+
# NEW:
|
|
628
|
+
result = await self._apply_filter(f, evt, ctx)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
# CURRENT lines 175-184:
|
|
633
|
+
if matched:
|
|
634
|
+
handler_found = True
|
|
635
|
+
# ...
|
|
636
|
+
async def _inner_base(ievt: Event, ictx: Dict[str, Any]) -> None:
|
|
637
|
+
self._spawn(handler, ievt, filters_str, **kwargs)
|
|
638
|
+
|
|
639
|
+
# NEW:
|
|
640
|
+
if matched:
|
|
641
|
+
handler_found = True
|
|
642
|
+
# Merge data dict (bot, state, etc.) with filter-returned kwargs
|
|
643
|
+
all_kwargs: dict[str, Any] = {**ctx, **kwargs}
|
|
644
|
+
# ...
|
|
645
|
+
async def _inner_base(ievt: Event, ictx: Dict[str, Any]) -> None:
|
|
646
|
+
self._spawn(handler, ievt, filters_str, **all_kwargs)
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
This allows `state: FSMContext` and `bot: Bot` to reach handlers via the existing signature-inspection in `_register`'s `async_wrapper`.
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## Step 14: Update `trueconf/__init__.py` — export FSM types
|
|
654
|
+
|
|
655
|
+
**File**: `trueconf/__init__.py` (modify)
|
|
656
|
+
|
|
657
|
+
No changes needed to top-level `__init__.py` — FSM types live under `trueconf.fsm` and are imported by users directly:
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
from trueconf.fsm import FSMContext, State, StatesGroup, StateFilter
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
However, if we want to make `StateFilter` available from `trueconf.filters`, add to `trueconf/filters/__init__.py`:
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
# Optional: re-export for convenience
|
|
667
|
+
from trueconf.fsm.filters import StateFilter
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Decision**: Do NOT pollute `trueconf.filters` with FSM types. Users import from `trueconf.fsm`. Keep boundaries clean.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Step 15: Create `tests/test_fsm.py` — comprehensive tests
|
|
675
|
+
|
|
676
|
+
**File**: `tests/test_fsm.py` (new)
|
|
677
|
+
|
|
678
|
+
Tests covering:
|
|
679
|
+
|
|
680
|
+
1. **State declaration** — `str(Form.name) == "Form:name"`, custom state, unbound error, `__states__`
|
|
681
|
+
2. **MemoryStorage** — get/set state, set_state(None) keeps data, clear removes all, update_data merges, different keys independent
|
|
682
|
+
3. **FSMContext** — full lifecycle: get_state → set_state → update_data → get_data → clear; set_state with State object
|
|
683
|
+
4. **StateFilter** — matches current state, rejects wrong state, matches None, multiple states, raises without FSMContext, State object as arg
|
|
684
|
+
5. **FSMMiddleware** — injects state into data, no bot skips injection
|
|
685
|
+
6. **Dispatcher integration** — setup_fsm registers middleware, middleware is first in chain, StateFilter works in pipeline, rejects wrong state
|
|
686
|
+
7. **DefaultKeyBuilder** — builds key from Message event, raises on missing fields
|
|
687
|
+
8. **StorageKey** — equality, hashing, frozen
|
|
688
|
+
|
|
689
|
+
Use existing test patterns from `tests/test_middleware.py` (pytest.mark.anyio, `_make_message` helper).
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Step 16: Run tests and lint
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
cd /Users/baadzianton/TrueConf/python-trueconf-bot
|
|
697
|
+
python -m pytest tests/test_fsm.py -v
|
|
698
|
+
python -m pytest tests/ -v # ensure no regressions
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
If the project has a lint/typecheck command (check pyproject.toml), run it too.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Execution Order Summary
|
|
706
|
+
|
|
707
|
+
| Step | File | Action |
|
|
708
|
+
|------|------|--------|
|
|
709
|
+
| 1 | `trueconf/fsm/__init__.py` | create (empty initially) |
|
|
710
|
+
| 2 | `trueconf/fsm/key_builder.py` | create |
|
|
711
|
+
| 3 | `trueconf/fsm/storage/base.py` | create |
|
|
712
|
+
| 4 | `trueconf/fsm/storage/memory.py` | create |
|
|
713
|
+
| 5 | `trueconf/fsm/storage/__init__.py` | create |
|
|
714
|
+
| 6 | `trueconf/fsm/state.py` | create |
|
|
715
|
+
| 7 | `trueconf/fsm/context.py` | create |
|
|
716
|
+
| 8 | `trueconf/fsm/manager.py` | create |
|
|
717
|
+
| 9 | `trueconf/fsm/middleware.py` | create |
|
|
718
|
+
| 10 | `trueconf/fsm/filters.py` | create |
|
|
719
|
+
| 11 | `trueconf/fsm/__init__.py` | update with exports |
|
|
720
|
+
| 12 | `trueconf/dispatcher/dispatcher.py` | modify |
|
|
721
|
+
| 13 | `trueconf/dispatcher/router.py` | modify |
|
|
722
|
+
| 14 | `tests/test_fsm.py` | create |
|
|
723
|
+
| 15 | run tests | verify |
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Risk Areas
|
|
728
|
+
|
|
729
|
+
1. **Circular imports**: `fsm/storage/base.py` imports from `fsm/key_builder.py`. Both are in `trueconf.fsm.*`. No issue — `key_builder` has no imports from `fsm`. `fsm/middleware.py` imports `BaseMiddleware` from `trueconf.middleware` — no issue. `dispatcher.py` uses lazy imports in `setup_fsm()` to avoid circular deps.
|
|
730
|
+
|
|
731
|
+
2. **`_apply_filter` backward compatibility**: Existing filters (`Command`, `InstanceOfFilter`, `MethodFilter`, `MessageFilter`, `MagicFilter`) accept only `(event)`. The new signature inspection will find no matching kwargs in `data` for them, so `f(event)` is called — identical to current behavior. Exception: existing filters that raise exceptions will now propagate instead of being swallowed. This is intentional — configuration errors must be explicit.
|
|
732
|
+
|
|
733
|
+
3. **Handler kwargs collision**: If `data` contains `"bot"` and a filter also returns `{"bot": ...}`, the filter value wins (`{**ctx, **kwargs}`). This is correct — filter-returned values are more specific.
|
|
734
|
+
|
|
735
|
+
4. **`asyncio.create_task` in `_spawn`**: Handlers run as fire-and-forget tasks. Middleware post-processing runs after task creation, not completion. This is existing behavior and doesn't change with FSM.
|
|
736
|
+
|
|
737
|
+
5. **Thread safety of MemoryStorage**: MemoryStorage uses a plain dict. In a single asyncio event loop (which is the intended usage), this is safe. No thread safety needed for v1.
|
|
738
|
+
|
|
739
|
+
6. **`setup_fsm()` idempotency**: Guard prevents double-registration of FSMMiddleware. Calling `setup_fsm()` twice raises `RuntimeError`. This is intentional for v1 — if dynamic reconfiguration is needed later, it can be added.
|