python-trueconf-bot 1.3.0__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.
Files changed (239) hide show
  1. python_trueconf_bot-1.4.0b1/.opencode/plans/2026-05-25-fsm-implementation.md +739 -0
  2. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/PKG-INFO +1 -1
  3. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/examples/index.md +176 -0
  4. python_trueconf_bot-1.4.0b1/docs/en/learn/fsm.md +445 -0
  5. python_trueconf_bot-1.4.0b1/docs/en/learn/middleware.md +283 -0
  6. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Dispatcher.md +6 -3
  7. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/release_notes.md +12 -0
  8. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/examples/index.md +177 -0
  9. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/index.md +2 -2
  10. python_trueconf_bot-1.4.0b1/docs/ru/learn/fsm.md +445 -0
  11. python_trueconf_bot-1.4.0b1/docs/ru/learn/middleware.md +283 -0
  12. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/release_notes.md +14 -0
  13. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/mkdocs.yml +2 -0
  14. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/PKG-INFO +1 -1
  15. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/SOURCES.txt +19 -0
  16. python_trueconf_bot-1.4.0b1/test5.py +70 -0
  17. python_trueconf_bot-1.4.0b1/tests/test_fsm.py +907 -0
  18. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/__init__.py +2 -0
  19. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/_version.py +3 -3
  20. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/client/bot.py +16 -10
  21. python_trueconf_bot-1.4.0b1/trueconf/dispatcher/dispatcher.py +149 -0
  22. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/dispatcher/router.py +156 -38
  23. python_trueconf_bot-1.4.0b1/trueconf/fsm/__init__.py +20 -0
  24. python_trueconf_bot-1.4.0b1/trueconf/fsm/context.py +57 -0
  25. python_trueconf_bot-1.4.0b1/trueconf/fsm/filters.py +31 -0
  26. python_trueconf_bot-1.4.0b1/trueconf/fsm/key_builder.py +39 -0
  27. python_trueconf_bot-1.4.0b1/trueconf/fsm/manager.py +36 -0
  28. python_trueconf_bot-1.4.0b1/trueconf/fsm/middleware.py +24 -0
  29. python_trueconf_bot-1.4.0b1/trueconf/fsm/state.py +135 -0
  30. python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/__init__.py +4 -0
  31. python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/base.py +36 -0
  32. python_trueconf_bot-1.4.0b1/trueconf/fsm/storage/memory.py +57 -0
  33. python_trueconf_bot-1.4.0b1/trueconf/fsm/strategy.py +36 -0
  34. python_trueconf_bot-1.4.0b1/trueconf/middleware.py +78 -0
  35. python_trueconf_bot-1.3.0/trueconf/dispatcher/dispatcher.py +0 -63
  36. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/.github/workflows/docs.yml +0 -0
  37. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/.github/workflows/release-test.yml +0 -0
  38. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/.github/workflows/release.yml +0 -0
  39. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/.gitignore +0 -0
  40. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/LICENSE +0 -0
  41. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/README-PyPI.md +0 -0
  42. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/README-ru.md +0 -0
  43. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/README.md +0 -0
  44. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/assets/head_en.png +0 -0
  45. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/assets/head_ru.png +0 -0
  46. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/assets/logo.svg +0 -0
  47. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/context7.json +0 -0
  48. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/features.md +0 -0
  49. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/img/head.png +0 -0
  50. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/img/router_scheme_en.svg +0 -0
  51. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/index.md +0 -0
  52. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/enums.md +0 -0
  53. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/files.md +0 -0
  54. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/filters.md +0 -0
  55. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/getting-started.md +0 -0
  56. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/parse-messages.md +0 -0
  57. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/restrictions.md +0 -0
  58. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/send_message.md +0 -0
  59. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/shortcuts.md +0 -0
  60. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/learn/types.md +0 -0
  61. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Bot.md +0 -0
  62. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Enums.md +0 -0
  63. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Exceptions.md +0 -0
  64. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Formatting.md +0 -0
  65. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Responses.md +0 -0
  66. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Router.md +0 -0
  67. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/Types.md +0 -0
  68. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/en/reference/index.md +0 -0
  69. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/hooks/llms_txt.py +0 -0
  70. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/img/chatbot.svg +0 -0
  71. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/img/favicon.ico +0 -0
  72. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/img/logo.svg +0 -0
  73. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/overrides/main.html +0 -0
  74. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/features.md +0 -0
  75. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/img/head.png +0 -0
  76. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/img/router_scheme_ru.svg +0 -0
  77. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/enums.md +0 -0
  78. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/files.md +0 -0
  79. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/filters.md +0 -0
  80. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/getting-started.md +0 -0
  81. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/parse-messages.md +0 -0
  82. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/restrictions.md +0 -0
  83. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/send_message.md +0 -0
  84. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/shortcuts.md +0 -0
  85. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/ru/learn/types.md +0 -0
  86. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/docs/stylesheets/extra.css +0 -0
  87. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/README-ru.md +0 -0
  88. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/README.md +0 -0
  89. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/add_big_list_users_to_channel.py +0 -0
  90. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/filters.py +0 -0
  91. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/report_bot.py +0 -0
  92. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/update_token.py +0 -0
  93. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/examples/webhook.py +0 -0
  94. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/pyproject.toml +0 -0
  95. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/dependency_links.txt +0 -0
  96. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/requires.txt +0 -0
  97. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/python_trueconf_bot.egg-info/top_level.txt +0 -0
  98. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/requirements-docs.txt +0 -0
  99. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/requirements.txt +0 -0
  100. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/setup.cfg +0 -0
  101. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/client/__init__.py +0 -0
  102. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/client/context_controller.py +0 -0
  103. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/client/session.py +0 -0
  104. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/dispatcher/__init__.py +0 -0
  105. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/__init__.py +0 -0
  106. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/aouth_error.py +0 -0
  107. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/chat_participant_role.py +0 -0
  108. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/chat_type.py +0 -0
  109. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/envelope_author_type.py +0 -0
  110. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/file_ready_state.py +0 -0
  111. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/incoming_update_method.py +0 -0
  112. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/message_type.py +0 -0
  113. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/parse_mode.py +0 -0
  114. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/survey_type.py +0 -0
  115. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/enums/update_type.py +0 -0
  116. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/exceptions.py +0 -0
  117. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/__init__.py +0 -0
  118. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/base.py +0 -0
  119. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/command.py +0 -0
  120. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/instance_of.py +0 -0
  121. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/message.py +0 -0
  122. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/filters/method.py +0 -0
  123. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/loggers.py +0 -0
  124. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/__init__.py +0 -0
  125. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/add_participant_to_chat.py +0 -0
  126. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/auth.py +0 -0
  127. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/base.py +0 -0
  128. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/change_participant_role.py +0 -0
  129. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/clear_chat_history.py +0 -0
  130. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_channel.py +0 -0
  131. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_favorites_chat.py +0 -0
  132. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_group_chat.py +0 -0
  133. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/create_p2p_chat.py +0 -0
  134. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_chat_avatar.py +0 -0
  135. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_chat_title.py +0 -0
  136. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_message.py +0 -0
  137. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/edit_survey.py +0 -0
  138. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/forward_message.py +0 -0
  139. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_by_id.py +0 -0
  140. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_history.py +0 -0
  141. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_participant.py +0 -0
  142. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chat_participants.py +0 -0
  143. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_chats.py +0 -0
  144. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_file_info.py +0 -0
  145. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_file_upload_limits.py +0 -0
  146. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_message_by_id.py +0 -0
  147. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/get_user_display_name.py +0 -0
  148. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/has_chat_participant.py +0 -0
  149. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_chat.py +0 -0
  150. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_message.py +0 -0
  151. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/remove_participant_from_chat.py +0 -0
  152. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_file.py +0 -0
  153. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_message.py +0 -0
  154. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/send_survey.py +0 -0
  155. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/subscribe_file_progress.py +0 -0
  156. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/unsubscribe_file_progress.py +0 -0
  157. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/methods/upload_file.py +0 -0
  158. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/py.typed +0 -0
  159. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/__init__.py +0 -0
  160. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/author_box.py +0 -0
  161. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/chat_participant.py +0 -0
  162. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/__init__.py +0 -0
  163. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/attachment.py +0 -0
  164. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/base.py +0 -0
  165. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/chat_created.py +0 -0
  166. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/document.py +0 -0
  167. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/edit_chat_title.py +0 -0
  168. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/forward_message.py +0 -0
  169. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/location.py +0 -0
  170. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/photo.py +0 -0
  171. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/remove_participant.py +0 -0
  172. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/sticker.py +0 -0
  173. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/survey.py +0 -0
  174. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/text.py +0 -0
  175. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/content/video.py +0 -0
  176. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/input_file.py +0 -0
  177. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/last_message.py +0 -0
  178. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/message.py +0 -0
  179. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/parser.py +0 -0
  180. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/__init__.py +0 -0
  181. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/added_chat_participant.py +0 -0
  182. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/changed_file_upload_limits.py +0 -0
  183. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/changed_participant_role.py +0 -0
  184. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/cleared_chat_history.py +0 -0
  185. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_channel.py +0 -0
  186. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_favorites_chat.py +0 -0
  187. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_group_chat.py +0 -0
  188. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/created_personal_chat.py +0 -0
  189. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_chat_avatar.py +0 -0
  190. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_chat_title.py +0 -0
  191. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/edited_message.py +0 -0
  192. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_chat.py +0 -0
  193. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_chat_participant.py +0 -0
  194. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/removed_message.py +0 -0
  195. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/requests/uploading_progress.py +0 -0
  196. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/__init__.py +0 -0
  197. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/add_chat_participant_response.py +0 -0
  198. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/api_error.py +0 -0
  199. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/auth_response_payload.py +0 -0
  200. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/change_participant_role_response.py +0 -0
  201. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/clear_chat_history_response.py +0 -0
  202. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_channel_response.py +0 -0
  203. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_favorites_chat_response.py +0 -0
  204. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_group_chat_response.py +0 -0
  205. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/create_p2p_chat_response.py +0 -0
  206. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_chat_avatar_response.py +0 -0
  207. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_chat_title_response.py +0 -0
  208. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_message_response.py +0 -0
  209. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/edit_survey_response.py +0 -0
  210. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/forward_message_response.py +0 -0
  211. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_by_id_response.py +0 -0
  212. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_history_response.py +0 -0
  213. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_participant_response.py +0 -0
  214. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chat_participants_response.py +0 -0
  215. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_chats_response.py +0 -0
  216. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_file_info_response.py +0 -0
  217. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_file_upload_limits_response.py +0 -0
  218. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_message_by_id_response.py +0 -0
  219. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/get_user_display_name_response.py +0 -0
  220. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/has_chat_participant_response.py +0 -0
  221. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_chat_participant_response.py +0 -0
  222. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_chat_response.py +0 -0
  223. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/remove_message_response.py +0 -0
  224. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_file_response.py +0 -0
  225. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_message_response.py +0 -0
  226. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/send_survey_response.py +0 -0
  227. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/subscribe_file_progress_response.py +0 -0
  228. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/unsubscribe_file_progress_response.py +0 -0
  229. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/responses/upload_file_response.py +0 -0
  230. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/types/update.py +0 -0
  231. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/__init__.py +0 -0
  232. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_generate_secret_for_survey.py +0 -0
  233. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_ssl.py +0 -0
  234. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_token.py +0 -0
  235. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/_version_checker.py +0 -0
  236. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/formatting/__init__.py +0 -0
  237. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/formatting/decorations.py +0 -0
  238. {python_trueconf_bot-1.3.0 → python_trueconf_bot-1.4.0b1}/trueconf/utils/formatting/text.py +0 -0
  239. {python_trueconf_bot-1.3.0 → 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.