python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.4.0__py3-none-any.whl

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.
@@ -1,63 +1,149 @@
1
1
  from __future__ import annotations
2
- from typing import List
2
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List
3
3
  from trueconf.filters.base import Event
4
4
  from trueconf.dispatcher.router import Router
5
5
 
6
+ MiddlewareHandler = Callable[[Event, Dict[str, Any]], Awaitable[None]]
6
7
 
7
- class Dispatcher:
8
- """
9
- Central event dispatcher for processing and routing incoming events.
8
+ if TYPE_CHECKING:
9
+ from trueconf.fsm.key_builder import KeyBuilder
10
+ from trueconf.fsm.manager import FSMManager
11
+ from trueconf.fsm.storage.base import BaseStorage
12
+ from trueconf.fsm.strategy import FSMStrategy
10
13
 
11
- The `Dispatcher` aggregates one or more `Router` instances and feeds each
12
- incoming event through them. The routers are traversed recursively via their
13
- `subrouters` (using `_iter_all()`), and each event is passed to `_feed()` of
14
- each router in order until it is handled.
15
14
 
16
- Typical usage includes registering routers with handlers and then calling
17
- `feed_update()` with incoming events.
15
+ class Dispatcher(Router):
16
+ """Central dispatcher for routing incoming events.
18
17
 
19
- Examples:
20
- >>> dispatcher = Dispatcher()
21
- >>> dispatcher.include_router(my_router)
18
+ The dispatcher is the root router of an application. It receives incoming
19
+ events, applies its own outer middleware chain, and then passes each event
20
+ to the included root routers in order. Processing stops when a router handles
21
+ the event, unless that router allows propagation to its child routers.
22
22
 
23
- Attributes:
24
- routers (List[Router]): List of root routers included in the dispatcher.
23
+ `Dispatcher` inherits from `Router`, so it supports the same handler,
24
+ middleware, and subrouter registration APIs.
25
25
 
26
- """
26
+ Example:
27
+ ```python
28
+ dispatcher = Dispatcher()
29
+ dispatcher.include_router(router)
30
+ ```
31
+
32
+ FSM example:
33
+ ```python
34
+ from trueconf.fsm.storage.memory import MemoryStorage
35
+
36
+ storage = MemoryStorage()
37
+ dp = Dispatcher(storage=storage)
38
+ ```
39
+
40
+ Args:
41
+ storage: Storage backend used to create an FSM manager. Cannot be used
42
+ together with `fsm_manager`.
43
+ fsm_manager: Existing FSM manager instance. Cannot be used together
44
+ with `storage`.
45
+ key_builder: Key builder used when creating an FSM manager from
46
+ `storage`. Ignored when `fsm_manager` is passed.
47
+ strategy: FSM strategy used when creating an FSM manager from `storage`.
48
+ Ignored when `fsm_manager` is passed.
27
49
 
28
- def __init__(self):
29
- """Initializes an empty dispatcher with no routers."""
50
+ Attributes:
51
+ routers: Root routers included in the dispatcher.
52
+ fsm: FSM manager configured for the dispatcher, or `None` if FSM support
53
+ has not been enabled.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ *,
59
+ storage: BaseStorage | None = None,
60
+ fsm_manager: FSMManager | None = None,
61
+ key_builder: KeyBuilder | None = None,
62
+ strategy: FSMStrategy | None = None,
63
+ ):
64
+ super().__init__(name="dispatcher")
30
65
  self.routers: List[Router] = []
66
+ self.fsm: FSMManager | None = None
31
67
 
32
- def include_router(self, router: Router):
33
- """
34
- Includes a router to be used by the dispatcher.
68
+ if fsm_manager is not None and storage is not None:
69
+ raise ValueError("Pass either fsm_manager or storage, not both")
70
+
71
+ if fsm_manager is not None:
72
+ self.setup_fsm(fsm_manager=fsm_manager)
73
+ elif storage is not None:
74
+ self.setup_fsm(storage=storage, key_builder=key_builder, strategy=strategy)
35
75
 
36
- Args:
37
- router (Router): A `Router` instance to include.
76
+ def setup_fsm(
77
+ self,
78
+ *,
79
+ fsm_manager: FSMManager | None = None,
80
+ storage: BaseStorage | None = None,
81
+ key_builder: KeyBuilder | None = None,
82
+ strategy: FSMStrategy | None = None,
83
+ ) -> FSMManager:
84
+ from trueconf.fsm.key_builder import DefaultKeyBuilder
85
+ from trueconf.fsm.manager import FSMManager
86
+ from trueconf.fsm.middleware import FSMMiddleware
87
+ from trueconf.fsm.storage.memory import MemoryStorage
88
+ from trueconf.fsm.strategy import FSMStrategy
89
+
90
+ if self.fsm is not None:
91
+ raise RuntimeError(
92
+ "FSM is already configured for this Dispatcher. "
93
+ "Call setup_fsm() only once, or create a new Dispatcher."
94
+ )
95
+
96
+ if fsm_manager is None:
97
+ fsm_manager = FSMManager(
98
+ storage=storage or MemoryStorage(),
99
+ key_builder=key_builder or DefaultKeyBuilder(),
100
+ strategy=strategy or FSMStrategy.USER_IN_CHAT,
101
+ )
102
+
103
+ self.fsm = fsm_manager
104
+ self._outer_middlewares.insert(0, FSMMiddleware(fsm_manager))
105
+ return fsm_manager
106
+
107
+ def include_router(self, router: "Router") -> None:
108
+ """Include a root router in the dispatcher.
109
+
110
+ The dispatcher's own middleware is applied in ``_feed_update`` before
111
+ the event reaches child routers. Therefore we do NOT set ``_parent`` —
112
+ child routers should not inherit the dispatcher's middleware through
113
+ the ancestor chain.
38
114
  """
39
115
  self.routers.append(router)
40
116
 
41
- async def _feed_update(self, event: Event):
117
+ async def _feed_update(self, event: Event, data: Dict[str, Any]) -> None:
42
118
  """
43
- Feeds an event to all routers and subrouters in order,
119
+ Feeds an event to all child routers in order,
44
120
  stopping at the first one that handles it.
45
121
 
46
- Args:
47
- event (Event): The event to be processed.
122
+ The event first passes through the dispatcher's own middleware chain
123
+ (outer middlewares from dispatcher ancestors → dispatcher), then is
124
+ fed to each child router.
48
125
 
49
- Returns:
50
- None
126
+ Args:
127
+ event (Event): The event to be processed.
128
+ data (Dict[str, Any]): Context data passed through the middleware pipeline.
51
129
  """
52
130
 
53
- async def progress_router(router, count = 0):
54
- handled = await router._feed(event)
55
- if count < 0 or count >= len(router._subrouters):
56
- return
57
- if (not handled) or (handled and router.allow_child_on_event):
58
- subrouter = router._subrouters[count]
59
- return await progress_router(subrouter, count=len(router._subrouters) - 1)
60
- return
61
-
62
- for router in self.routers:
63
- await progress_router(router)
131
+ async def _feed_children(evt: Event, ctx: Dict[str, Any]) -> None:
132
+ async def progress_router(router: Router, count: int = 0) -> None:
133
+ handled = await router._feed(evt, ctx)
134
+ if count < 0 or count >= len(router._subrouters):
135
+ return
136
+ if (not handled) or (handled and router.allow_child_on_event):
137
+ subrouter = router._subrouters[count]
138
+ await progress_router(subrouter, count=len(router._subrouters) - 1)
139
+
140
+ for router in self.routers:
141
+ await progress_router(router)
142
+
143
+ # Build outer middleware chain: dispatcher outer_mw → feed_children
144
+ chain: MiddlewareHandler = _feed_children
145
+ for mw in reversed(self._collect_middlewares("_outer_middlewares")):
146
+ nxt = chain
147
+ chain = Router._wrap_middleware(mw, nxt)
148
+
149
+ await chain(event, data)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
  import asyncio
3
3
  import logging
4
4
  import inspect
5
- from typing import Callable, Awaitable, List, Tuple, Any, Union
5
+ from typing import TYPE_CHECKING, Callable, Awaitable, Dict, List, Tuple, Any, Union
6
6
  from magic_filter import MagicFilter
7
7
  from trueconf.filters.base import Event
8
8
  from trueconf.filters.base import Filter
@@ -25,6 +25,9 @@ from trueconf.types.requests.removed_chat_participant import RemovedChatParticip
25
25
  from trueconf.types.requests.removed_message import RemovedMessage
26
26
  from trueconf.types.requests.uploading_progress import UploadingProgress
27
27
 
28
+ if TYPE_CHECKING:
29
+ from trueconf.middleware import BaseMiddleware
30
+
28
31
  logger = logging.getLogger("chat_bot")
29
32
 
30
33
  Handler = Callable[..., Awaitable[None]]
@@ -62,12 +65,19 @@ class Router:
62
65
  If you have multiple routers, use `.include_router()` to add them to a parent router.
63
66
  """
64
67
 
65
- def __init__(self, name: str | None = None, allow_child_on_event: bool = False):
66
-
68
+ def __init__(
69
+ self,
70
+ name: str | None = None,
71
+ allow_child_on_event: bool = False,
72
+ _parent: "Router | None" = None,
73
+ ):
67
74
  self.name = name or hex(id(self))
68
75
  self.allow_child_on_event = allow_child_on_event
76
+ self._parent: Router | None = _parent
69
77
  self._handlers: List[Tuple[Tuple[FilterLike, ...], Handler]] = []
70
78
  self._subrouters: List["Router"] = []
79
+ self._outer_middlewares: List["BaseMiddleware"] = []
80
+ self._inner_middlewares: List["BaseMiddleware"] = []
71
81
 
72
82
  def _iter_all(self) -> List["Router"]:
73
83
  """Return a list of this router and all nested subrouters recursively."""
@@ -76,8 +86,42 @@ class Router:
76
86
  out.extend(child._iter_all())
77
87
  return out
78
88
 
89
+ def _ancestors_with_self(self) -> List["Router"]:
90
+ """Return routers from root ancestor down to self."""
91
+ chain: list[Router] = []
92
+ current: Router | None = self
93
+ while current is not None:
94
+ chain.append(current)
95
+ current = current._parent
96
+ chain.reverse()
97
+ return chain
98
+
99
+ def _collect_middlewares(
100
+ self, attr: str
101
+ ) -> List["BaseMiddleware"]:
102
+ """Collect middlewares from ancestors → self."""
103
+ result: list[BaseMiddleware] = []
104
+ for router in self._ancestors_with_self():
105
+ result.extend(getattr(router, attr, []))
106
+ return result
107
+
108
+ def outer_middleware(self, middleware: "BaseMiddleware") -> None:
109
+ """Register outer middleware (runs before filter/handler matching)."""
110
+ self._outer_middlewares.append(middleware)
111
+
112
+ def inner_middleware(self, middleware: "BaseMiddleware") -> None:
113
+ """Register inner middleware (runs after filter match, before handler)."""
114
+ self._inner_middlewares.append(middleware)
115
+
79
116
  def _register(self, filters: Tuple[FilterLike, ...]):
80
117
  """Internal decorator for registering handlers with filters."""
118
+ # Sugar: State instances are auto-wrapped with StateFilter
119
+ from trueconf.fsm.filters import StateFilter
120
+ from trueconf.fsm.state import State
121
+ filters = tuple(
122
+ StateFilter(f) if isinstance(f, State) else f
123
+ for f in filters
124
+ )
81
125
 
82
126
  def decorator(func: Handler):
83
127
  async def async_wrapper(evt: Event, **kwargs: Any):
@@ -91,41 +135,96 @@ class Router:
91
135
 
92
136
  return decorator
93
137
 
94
- async def _feed(self, event: Event) -> bool:
95
- """Feed an incoming event to the router and invoke the first matching handler."""
96
- logger.info(f"📥 Incoming event: {event}")
97
- for flts, handler in self._handlers:
138
+ async def _feed(self, event: Event, data: Dict[str, Any]) -> bool:
139
+ """Feed an incoming event to the router and invoke the first matching handler.
98
140
 
99
- if not flts:
100
- self._spawn(handler, event, "<none>")
101
- return True
141
+ Pipeline:
142
+ outer_middleware → filter match → inner_middleware → handler
102
143
 
103
- matched = True
104
- for f in flts:
105
- try:
106
- if not await self._apply_filter(f, event):
107
- matched = False
108
- break
109
- except Exception as e:
110
- logger.exception(f"Filter {type(f).__name__} error: {e}")
111
- matched = False
112
- break
144
+ Returns:
145
+ True — event was handled or blocked by outer middleware.
146
+ False — no handler matched and outer middleware did not block;
147
+ the dispatcher may try the next router.
148
+ """
149
+ logger.info("📥 Incoming event: %s", event)
113
150
 
114
- if matched:
115
- filters_str = ", ".join(
116
- getattr(f, "__name__", type(f).__name__) if callable(f) else type(f).__name__
117
- for f in flts
118
- )
151
+ outer_passed = False
152
+ handler_found = False
119
153
 
154
+ async def _core(evt: Event, ctx: Dict[str, Any]) -> None:
155
+ nonlocal outer_passed, handler_found
156
+ outer_passed = True
157
+
158
+ # --- filter search ---
159
+ for flts, handler in self._handlers:
160
+ if not flts:
161
+ handler_found = True
162
+ self._spawn(handler, evt, "<none>")
163
+ return
164
+
165
+ matched = True
120
166
  kwargs: dict[str, Any] = {}
121
167
  for f in flts:
122
- result = await self._apply_filter(f, event)
168
+ try:
169
+ result = await self._apply_filter(f, evt, ctx)
170
+ except Exception as e:
171
+ logger.exception("Filter %s error: %s", type(f).__name__, e)
172
+ matched = False
173
+ break
174
+
175
+ if not result:
176
+ matched = False
177
+ break
178
+
123
179
  if isinstance(result, dict):
124
180
  kwargs.update(result)
125
181
 
126
- self._spawn(handler, event, filters_str, **kwargs)
127
- return True
128
- return False
182
+ if matched:
183
+ handler_found = True
184
+ filters_str = ", ".join(
185
+ getattr(f, "__name__", type(f).__name__) if callable(f) else type(f).__name__
186
+ for f in flts
187
+ )
188
+
189
+ # Merge data dict (bot, state, etc.) with filter-returned kwargs
190
+ all_kwargs: dict[str, Any] = {**ctx, **kwargs}
191
+
192
+ # --- build inner chain: inner_mw → handler ---
193
+ async def _inner_base(ievt: Event, ictx: Dict[str, Any]) -> None:
194
+ self._spawn(handler, ievt, filters_str, **all_kwargs)
195
+
196
+ inner_chain: Callable[[Event, Dict[str, Any]], Awaitable[None]] = _inner_base
197
+ for mw in reversed(self._collect_middlewares("_inner_middlewares")):
198
+ nxt = inner_chain
199
+ inner_chain = self._wrap_middleware(mw, nxt)
200
+
201
+ await inner_chain(evt, ctx)
202
+ return
203
+
204
+ # --- wrap with outer middlewares ---
205
+ chain: Callable[[Event, Dict[str, Any]], Awaitable[None]] = _core
206
+ for mw in reversed(self._collect_middlewares("_outer_middlewares")):
207
+ nxt = chain
208
+ chain = self._wrap_middleware(mw, nxt)
209
+
210
+ await chain(event, data)
211
+
212
+ if not outer_passed:
213
+ # Outer middleware blocked — event consumed
214
+ return True
215
+ if not handler_found:
216
+ # No handler matched — try next router
217
+ return False
218
+ return True
219
+
220
+ @staticmethod
221
+ def _wrap_middleware(
222
+ mw: "BaseMiddleware",
223
+ nxt: Callable[[Event, Dict[str, Any]], Awaitable[None]],
224
+ ) -> Callable[[Event, Dict[str, Any]], Awaitable[None]]:
225
+ async def wrapped(evt: Event, ctx: Dict[str, Any]) -> None:
226
+ await mw(nxt, evt, ctx)
227
+ return wrapped
129
228
 
130
229
  def _spawn(self, handler: Handler, event: Event, filters_str: str, **kwargs: dict[str, Any]):
131
230
  """Internal method to spawn a task for executing the matched handler."""
@@ -140,24 +239,42 @@ class Router:
140
239
 
141
240
  asyncio.create_task(_run())
142
241
 
143
- async def _apply_filter(self, f: Filter | Any, event: Event) -> bool:
144
- """Evaluate a filter (sync or async) against the event."""
242
+ async def _apply_filter(self, f: Filter | Any, event: Event, data: dict[str, Any] | None = None) -> bool:
243
+ """Evaluate a filter against the event, passing matching kwargs from data."""
244
+ data = data or {}
245
+
145
246
  if isinstance(f, MagicFilter):
146
247
  try:
147
248
  return bool(f.resolve(event))
148
249
  except Exception:
149
250
  return False
150
251
 
252
+ # Resolve which kwargs from data the filter accepts
253
+ kwargs: dict[str, Any] = {}
254
+ has_var_kwargs = False
151
255
  try:
152
- res = f(event)
153
- except Exception:
154
- return False
256
+ sig = inspect.signature(f)
257
+ for name, param in sig.parameters.items():
258
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
259
+ has_var_kwargs = True
260
+ continue
261
+ if name in data and param.kind in (
262
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
263
+ inspect.Parameter.KEYWORD_ONLY,
264
+ ):
265
+ kwargs[name] = data[name]
266
+ except (ValueError, TypeError):
267
+ pass
268
+
269
+ # If filter accepts **kwargs, pass all remaining data
270
+ if has_var_kwargs:
271
+ kwargs.update({k: v for k, v in data.items() if k not in kwargs})
272
+
273
+ # Regular filters: let exceptions propagate (config errors must be explicit)
274
+ res = f(event, **kwargs) if kwargs else f(event)
155
275
 
156
276
  if inspect.isawaitable(res):
157
- try:
158
- res = await res
159
- except Exception:
160
- return False
277
+ res = await res
161
278
 
162
279
  if isinstance(res, (bool, dict)):
163
280
  return res
@@ -165,6 +282,7 @@ class Router:
165
282
 
166
283
  def include_router(self, router: "Router") -> None:
167
284
  """Include a child router for hierarchical event routing."""
285
+ router._parent = self
168
286
  self._subrouters.append(router)
169
287
 
170
288
  def event(self, method: str, *filters: FilterLike):
trueconf/exceptions.py CHANGED
@@ -1,19 +1,41 @@
1
-
2
-
3
-
4
1
  class TrueConfChatBotError(Exception):
5
2
  """
6
3
  Base exception for all TrueConf ChatBot Connector errors.
4
+
5
+ All custom exceptions raised by the library inherit from this class.
6
+ You can catch this exception to handle any library-specific error in a
7
+ single place.
7
8
  """
8
9
 
9
10
  class TokenValidationError(TrueConfChatBotError):
11
+ """
12
+ Raised when the bot token fails local validation.
13
+
14
+ This error indicates that the token value has an invalid format or
15
+ cannot be used by the connector before the authorization request is sent.
16
+ """
10
17
  pass
11
18
 
12
19
  class InvalidGrantError(TrueConfChatBotError):
20
+ """
21
+ Raised when the server rejects the provided authorization grant.
22
+
23
+ This usually means that the token or credentials are invalid, expired,
24
+ revoked, or cannot be used to authorize the bot.
25
+ """
13
26
  pass
14
27
 
15
28
  class LimitExceededError(TrueConfChatBotError):
16
- """Base exception for all limit-related errors (length, size, etc.)."""
29
+ """
30
+ Base exception for all limit-related errors.
31
+
32
+ This class is used for errors related to text length, title length,
33
+ file size, and other numeric limits.
34
+
35
+ Attributes:
36
+ actual_value: Actual value that exceeded the limit.
37
+ limit: Maximum allowed value.
38
+ """
17
39
  def __init__(self, message: str, actual_value: int, limit: int):
18
40
  super().__init__(message)
19
41
  self.actual_value = actual_value
@@ -21,7 +43,17 @@ class LimitExceededError(TrueConfChatBotError):
21
43
 
22
44
 
23
45
  class TextMessageTooLongError(LimitExceededError):
24
- """Raised when the message text exceeds TrueConf's allowed length."""
46
+ """
47
+ Raised when a text message exceeds the maximum allowed length.
48
+
49
+ The default text message limit is 4096 visible characters. For long texts,
50
+ such as LLM-generated responses, use `safe_split_text` from `trueconf.utils`
51
+ and send the returned chunks one by one.
52
+
53
+ Attributes:
54
+ actual_value: Actual visible length of the message text.
55
+ limit: Maximum allowed message length.
56
+ """
25
57
  def __init__(self, actual_length: int, limit: int = 4096):
26
58
  message = (
27
59
  f"Bad Request: text message is too long. "
@@ -31,7 +63,17 @@ class TextMessageTooLongError(LimitExceededError):
31
63
  super().__init__(message, actual_length, limit)
32
64
 
33
65
  class FileCaptionTooLongError(LimitExceededError):
34
- """Raised when the caption exceeds TrueConf's allowed length."""
66
+ """
67
+ Raised when a file caption exceeds the maximum allowed length.
68
+
69
+ The default caption limit is 4096 visible characters. For long captions,
70
+ use `safe_split_text` from `trueconf.utils` and send the returned chunks
71
+ separately when appropriate.
72
+
73
+ Attributes:
74
+ actual_value: Actual visible length of the caption.
75
+ limit: Maximum allowed caption length.
76
+ """
35
77
  def __init__(self, actual_length: int, limit: int = 4096):
36
78
  message = (
37
79
  f"Bad Request: caption is too long. "
@@ -41,11 +83,22 @@ class FileCaptionTooLongError(LimitExceededError):
41
83
  super().__init__(message, actual_length, limit)
42
84
 
43
85
  class ChatTitleTooLongError(LimitExceededError):
44
- """Base error for long chat titles."""
86
+ """
87
+ Base exception for chat title length errors.
88
+
89
+ This class is inherited by more specific exceptions for group and channel
90
+ title length validation.
91
+ """
45
92
  pass
46
93
 
47
94
  class GroupTitleTooLongError(ChatTitleTooLongError):
48
- """Raised when the group title exceeds allowed length."""
95
+ """
96
+ Raised when a group title exceeds the maximum allowed length.
97
+
98
+ Attributes:
99
+ actual_value: Actual length of the group title.
100
+ limit: Maximum allowed group title length.
101
+ """
49
102
  def __init__(self, actual_length: int, limit: int = 256):
50
103
  message = (
51
104
  f"Bad Request: group title is too long. "
@@ -54,7 +107,13 @@ class GroupTitleTooLongError(ChatTitleTooLongError):
54
107
  super().__init__(message, actual_length, limit)
55
108
 
56
109
  class ChannelTitleTooLongError(ChatTitleTooLongError):
57
- """Raised when the channel title exceeds allowed length."""
110
+ """
111
+ Raised when a channel title exceeds the maximum allowed length.
112
+
113
+ Attributes:
114
+ actual_value: Actual length of the channel title.
115
+ limit: Maximum allowed channel title length.
116
+ """
58
117
  def __init__(self, actual_length: int, limit: int = 256):
59
118
  message = (
60
119
  f"Bad Request: channel title is too long. "
@@ -63,10 +122,25 @@ class ChannelTitleTooLongError(ChatTitleTooLongError):
63
122
  super().__init__(message, actual_length, limit)
64
123
 
65
124
  class FileValidationError(TrueConfChatBotError):
66
- """Общий класс для всех ошибок валидации файлов."""
125
+ """
126
+ Base exception for file validation errors.
127
+
128
+ This class is used for errors related to file size limits, extension filters,
129
+ and other file validation rules received from TrueConf Server.
130
+ """
67
131
  pass
68
132
 
69
133
  class FileSizeTooLargeError(FileValidationError, LimitExceededError):
134
+ """
135
+ Raised when a file exceeds the maximum size allowed by the server.
136
+
137
+ File size limits are configured by the TrueConf Server administrator and
138
+ are checked by the bot before sending a file.
139
+
140
+ Attributes:
141
+ actual_value: Actual file size in bytes.
142
+ limit: Maximum allowed file size in bytes.
143
+ """
70
144
  def __init__(self, actual_size: int, limit: int):
71
145
  actual_mb = round(actual_size / (1024 * 1024), 2)
72
146
  limit_mb = round(limit / (1024 * 1024), 2)
@@ -78,6 +152,18 @@ class FileSizeTooLargeError(FileValidationError, LimitExceededError):
78
152
 
79
153
 
80
154
  class InvalidFileExtensionError(FileValidationError):
155
+ """
156
+ Raised when a file extension does not satisfy the server extension filter.
157
+
158
+ File extension rules are configured by the TrueConf Server administrator.
159
+ Depending on the filter mode, the extension list can work either as an
160
+ allow list or as a block list.
161
+
162
+ Attributes:
163
+ extension: File extension that failed validation.
164
+ extensions: Set of extensions used for validation.
165
+ mode: Extension filter mode.
166
+ """
81
167
  def __init__(self, extension: str, extensions: set[str] | None = None, mode: str = "allow"):
82
168
  self.extension = extension
83
169
  self.extensions = extensions or set()
@@ -106,6 +192,17 @@ class InvalidFileExtensionError(FileValidationError):
106
192
  super().__init__(message)
107
193
 
108
194
  class ApiErrorException(TrueConfChatBotError):
195
+ """
196
+ Raised when TrueConf Server returns an API error response.
197
+
198
+ This exception stores the server error code, human-readable detail, and
199
+ optional response payload for additional context.
200
+
201
+ Attributes:
202
+ code: Error code returned by the server.
203
+ detail: Human-readable error description.
204
+ payload: Raw error payload returned by the server, or an empty dict.
205
+ """
109
206
  def __init__(self, code: int, detail: str, payload: dict | None = None):
110
207
  super().__init__(f"[{code}] {detail}")
111
208
  self.code = code
@@ -0,0 +1,20 @@
1
+ from trueconf.fsm.context import FSMContext
2
+ from trueconf.fsm.filters import StateFilter
3
+ from trueconf.fsm.key_builder import DefaultKeyBuilder, KeyBuilder, StorageKey
4
+ from trueconf.fsm.manager import FSMManager
5
+ from trueconf.fsm.state import State, StatesGroup, any_state, default_state
6
+ from trueconf.fsm.strategy import FSMStrategy
7
+
8
+ __all__ = (
9
+ "FSMContext",
10
+ "FSMManager",
11
+ "FSMStrategy",
12
+ "State",
13
+ "StateFilter",
14
+ "StatesGroup",
15
+ "StorageKey",
16
+ "KeyBuilder",
17
+ "DefaultKeyBuilder",
18
+ "any_state",
19
+ "default_state",
20
+ )