codex-bot 0.1.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.
Files changed (64) hide show
  1. codex_bot/__init__.py +24 -0
  2. codex_bot/animation/__init__.py +10 -0
  3. codex_bot/animation/animation_service.py +298 -0
  4. codex_bot/base/__init__.py +22 -0
  5. codex_bot/base/base_orchestrator.py +123 -0
  6. codex_bot/base/context_dto.py +42 -0
  7. codex_bot/base/view_dto.py +93 -0
  8. codex_bot/cli/__init__.py +11 -0
  9. codex_bot/cli/commands.py +215 -0
  10. codex_bot/director/__init__.py +13 -0
  11. codex_bot/director/director.py +107 -0
  12. codex_bot/director/protocols.py +80 -0
  13. codex_bot/engine/__init__.py +13 -0
  14. codex_bot/engine/discovery/__init__.py +7 -0
  15. codex_bot/engine/discovery/service.py +258 -0
  16. codex_bot/engine/factory/__init__.py +7 -0
  17. codex_bot/engine/factory/bot_builder.py +111 -0
  18. codex_bot/engine/http/__init__.py +10 -0
  19. codex_bot/engine/http/api_client.py +123 -0
  20. codex_bot/engine/i18n/__init__.py +7 -0
  21. codex_bot/engine/i18n/locales_compiler.py +83 -0
  22. codex_bot/engine/middlewares/__init__.py +24 -0
  23. codex_bot/engine/middlewares/container.py +46 -0
  24. codex_bot/engine/middlewares/i18n.py +90 -0
  25. codex_bot/engine/middlewares/throttling.py +69 -0
  26. codex_bot/engine/middlewares/user_validation.py +49 -0
  27. codex_bot/engine/router_builder/__init__.py +10 -0
  28. codex_bot/engine/router_builder/router_builder.py +133 -0
  29. codex_bot/fsm/__init__.py +16 -0
  30. codex_bot/fsm/common_fsm_handlers.py +46 -0
  31. codex_bot/fsm/garbage_collector.py +141 -0
  32. codex_bot/fsm/state_helper.py +72 -0
  33. codex_bot/fsm/state_manager.py +104 -0
  34. codex_bot/helper/__init__.py +7 -0
  35. codex_bot/helper/context_helper.py +61 -0
  36. codex_bot/redis/__init__.py +15 -0
  37. codex_bot/redis/dispatcher.py +169 -0
  38. codex_bot/redis/router.py +71 -0
  39. codex_bot/redis/stream_processor.py +210 -0
  40. codex_bot/sender/__init__.py +15 -0
  41. codex_bot/sender/protocols.py +67 -0
  42. codex_bot/sender/sender_keys.py +50 -0
  43. codex_bot/sender/sender_manager.py +85 -0
  44. codex_bot/sender/view_sender.py +200 -0
  45. codex_bot/templates/feature/callbacks.py.tpl +14 -0
  46. codex_bot/templates/feature/contract.py.tpl +12 -0
  47. codex_bot/templates/feature/dto.py.tpl +7 -0
  48. codex_bot/templates/feature/feature.py.tpl +26 -0
  49. codex_bot/templates/feature/feature_redis.py.tpl +6 -0
  50. codex_bot/templates/feature/formatters.py.tpl +15 -0
  51. codex_bot/templates/feature/handlers.py.tpl +32 -0
  52. codex_bot/templates/feature/handlers_redis.py.tpl +24 -0
  53. codex_bot/templates/feature/keyboards.py.tpl +14 -0
  54. codex_bot/templates/feature/orchestrator.py.tpl +26 -0
  55. codex_bot/templates/feature/orchestrator_redis.py.tpl +17 -0
  56. codex_bot/templates/feature/texts.py.tpl +6 -0
  57. codex_bot/templates/feature/ui.py.tpl +19 -0
  58. codex_bot/url_signer/__init__.py +7 -0
  59. codex_bot/url_signer/service.py +116 -0
  60. codex_bot-0.1.0.dist-info/METADATA +116 -0
  61. codex_bot-0.1.0.dist-info/RECORD +64 -0
  62. codex_bot-0.1.0.dist-info/WHEEL +4 -0
  63. codex_bot-0.1.0.dist-info/entry_points.txt +2 -0
  64. codex_bot-0.1.0.dist-info/licenses/LICENSE +21 -0
codex_bot/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ codex_bot — Feature-based Aiogram framework library.
3
+
4
+ Reusable infrastructure for Telegram bots with stateless UI management,
5
+ Redis Stream integration, and advanced FSM capabilities.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "Codex Team"
10
+ __license__ = "MIT"
11
+
12
+ from codex_bot.base.base_orchestrator import BaseBotOrchestrator, PayloadT
13
+ from codex_bot.director.director import Director
14
+ from codex_bot.engine.factory.bot_builder import BotBuilder
15
+ from codex_bot.sender.view_sender import ViewSender
16
+
17
+ __all__ = [
18
+ "BaseBotOrchestrator",
19
+ "PayloadT",
20
+ "Director",
21
+ "BotBuilder",
22
+ "ViewSender",
23
+ "__version__",
24
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ codex_bot.animation — Waiting animation service for Telegram UI.
3
+ """
4
+
5
+ from codex_bot.animation.animation_service import AnimationType, UIAnimationService
6
+
7
+ __all__ = [
8
+ "UIAnimationService",
9
+ "AnimationType",
10
+ ]
@@ -0,0 +1,298 @@
1
+ """
2
+ UIAnimationService — Waiting animation service for Telegram UI.
3
+
4
+ Three main scenarios: delayed fetch, polling loop, timed polling.
5
+ """
6
+
7
+ import asyncio
8
+ from collections.abc import Awaitable, Callable
9
+ from enum import Enum
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from codex_bot.base.view_dto import UnifiedViewDTO, ViewResultDTO
13
+
14
+ if TYPE_CHECKING:
15
+ from codex_bot.sender.view_sender import ViewSender
16
+
17
+
18
+ class AnimationType(Enum):
19
+ """Animation type for displaying progress.
20
+
21
+ Attributes:
22
+ PROGRESS_BAR: Fills from 0% to 100%.
23
+ INFINITE: Running indicator (snake).
24
+ NONE: No animation.
25
+ """
26
+
27
+ PROGRESS_BAR = "progress_bar"
28
+ INFINITE = "infinite"
29
+ NONE = "none"
30
+
31
+
32
+ PollerFunc = Callable[[], Awaitable[tuple[UnifiedViewDTO, bool]]]
33
+ """Poller function type: returns ``(view, is_waiting)``."""
34
+
35
+
36
+ class UIAnimationService:
37
+ """Service for waiting animation (Polling).
38
+
39
+ Three main scenarios:
40
+
41
+ - `run_delayed_fetch` — animation for N seconds → one request at the end.
42
+ - `run_polling_loop` — request loop until an event occurs.
43
+ - `run_timed_polling` — immediate request → animation based on duration.
44
+
45
+ Args:
46
+ sender: ViewSender instance for sending intermediate frames.
47
+
48
+ Example:
49
+ ```python
50
+ animation = UIAnimationService(sender=container.view_sender)
51
+
52
+ async def fetch_result() -> tuple[UnifiedViewDTO, bool]:
53
+ data = await api.get_status(user_id)
54
+ return build_view(data), data.is_pending
55
+
56
+ await animation.run_polling_loop(
57
+ check_func=fetch_result,
58
+ timeout=60.0,
59
+ loading_text="⏳ <b>Waiting...</b>",
60
+ )
61
+ ```
62
+ """
63
+
64
+ def __init__(self, sender: "ViewSender") -> None:
65
+ self.sender = sender
66
+
67
+ # =========================================================================
68
+ # Public Methods
69
+ # =========================================================================
70
+
71
+ async def run_delayed_fetch(
72
+ self,
73
+ fetch_func: PollerFunc,
74
+ delay: float = 3.0,
75
+ step_interval: float = 1.0,
76
+ loading_text: str = "🔍 <b>Searching...</b>",
77
+ animation_type: AnimationType = AnimationType.PROGRESS_BAR,
78
+ ) -> None:
79
+ """Animation for N seconds → one request at the end.
80
+
81
+ Used for: Search, Scan — show animation, then make one request to the backend.
82
+
83
+ Args:
84
+ fetch_func: Function to retrieve data (called once at the end).
85
+ delay: Total animation duration in seconds.
86
+ step_interval: Interval between animation frames.
87
+ loading_text: Text to display during animation.
88
+ animation_type: Animation type (PROGRESS_BAR or INFINITE).
89
+ """
90
+ steps = max(1, int(delay / step_interval))
91
+
92
+ for i in range(steps):
93
+ anim_str = self._generate_animation(i, steps, loading_text, animation_type)
94
+ temp_view = UnifiedViewDTO(content=ViewResultDTO(text=anim_str))
95
+ await self._send(temp_view)
96
+ await asyncio.sleep(step_interval)
97
+
98
+ view_dto, _ = await self._poll_check(fetch_func)
99
+ await self._send(view_dto)
100
+
101
+ async def run_polling_loop(
102
+ self,
103
+ check_func: PollerFunc,
104
+ timeout: float = 60.0,
105
+ step_interval: float = 2.0,
106
+ loading_text: str = "⏳ <b>Waiting...</b>",
107
+ animation_type: AnimationType = AnimationType.INFINITE,
108
+ ) -> None:
109
+ """Request loop until an event occurs.
110
+
111
+ Used for: Combat polling, Arena waiting — make requests every N seconds
112
+ while ``is_waiting=True``.
113
+
114
+ Args:
115
+ check_func: Status check function, returns ``(view, is_waiting)``.
116
+ timeout: Maximum waiting time in seconds.
117
+ step_interval: Interval between checks.
118
+ loading_text: Text to display while waiting.
119
+ animation_type: Animation type (usually INFINITE).
120
+ """
121
+ steps = int(timeout / step_interval)
122
+
123
+ for i in range(steps):
124
+ view_dto, is_waiting = await self._poll_check(check_func)
125
+
126
+ if is_waiting and view_dto.content:
127
+ anim_str = self._generate_animation(i, steps, loading_text, animation_type)
128
+ view_dto = self._inject_animation(view_dto, anim_str)
129
+
130
+ await self._send(view_dto)
131
+
132
+ if not is_waiting:
133
+ return
134
+
135
+ await asyncio.sleep(step_interval)
136
+
137
+ async def run_timed_polling(
138
+ self,
139
+ check_func: PollerFunc,
140
+ duration: float = 5.0,
141
+ step_interval: float = 1.0,
142
+ loading_text: str = "🚶 <b>Moving...</b>",
143
+ animation_type: AnimationType = AnimationType.PROGRESS_BAR,
144
+ ) -> None:
145
+ """Immediate request → animation based on duration from the result.
146
+
147
+ Used for: Move — request goes to background immediately, result is stored
148
+ in Redis, show Progress Bar based on time.
149
+
150
+ Args:
151
+ check_func: Check function that reads the result from state/Redis.
152
+ duration: Expected animation duration.
153
+ step_interval: Interval between frames.
154
+ loading_text: Text to display.
155
+ animation_type: Animation type (PROGRESS_BAR for timed).
156
+ """
157
+ steps = max(1, int(duration / step_interval))
158
+
159
+ for i in range(steps):
160
+ view_dto, is_waiting = await self._poll_check(check_func)
161
+
162
+ if not is_waiting:
163
+ await self._send(view_dto)
164
+ return
165
+
166
+ anim_str = self._generate_animation(i, steps, loading_text, animation_type)
167
+ view_dto = self._inject_animation(view_dto, anim_str)
168
+
169
+ await self._send(view_dto)
170
+ await asyncio.sleep(step_interval)
171
+
172
+ # Overflow: Backend slow response → Infinite mode
173
+ infinite_step = 0
174
+ while True:
175
+ view_dto, is_waiting = await self._poll_check(check_func)
176
+
177
+ if not is_waiting:
178
+ await self._send(view_dto)
179
+ return
180
+
181
+ anim_str = self._generate_animation(infinite_step, steps, loading_text, AnimationType.INFINITE)
182
+ view_dto = self._inject_animation(view_dto, anim_str)
183
+
184
+ await self._send(view_dto)
185
+ await asyncio.sleep(step_interval)
186
+ infinite_step += 1
187
+
188
+ # =========================================================================
189
+ # Private Helpers
190
+ # =========================================================================
191
+
192
+ async def _poll_check(self, func: PollerFunc) -> tuple[UnifiedViewDTO, bool]:
193
+ """Performs status check.
194
+
195
+ Args:
196
+ func: Poller function.
197
+
198
+ Returns:
199
+ Tuple ``(view, is_waiting)``.
200
+ """
201
+ result: Any = await func()
202
+ if isinstance(result, tuple):
203
+ return result
204
+ return result, False
205
+
206
+ def _inject_animation(self, view_dto: UnifiedViewDTO, anim_str: str) -> UnifiedViewDTO:
207
+ """Injects animation string into content, returns new DTO.
208
+
209
+ Looks for ``{ANIMATION}`` in text and replaces it, otherwise adds to the end.
210
+ Uses ``model_copy`` since DTO is frozen.
211
+
212
+ Args:
213
+ view_dto: Original DTO.
214
+ anim_str: Animation string.
215
+
216
+ Returns:
217
+ New ``UnifiedViewDTO`` with updated text.
218
+ """
219
+ if not view_dto.content:
220
+ return view_dto
221
+
222
+ original_text = view_dto.content.text
223
+ if "{ANIMATION}" in original_text:
224
+ new_text = original_text.replace("{ANIMATION}", anim_str)
225
+ else:
226
+ new_text = original_text + f"\n\n{anim_str}"
227
+
228
+ new_content = view_dto.content.model_copy(update={"text": new_text})
229
+ return view_dto.model_copy(update={"content": new_content})
230
+
231
+ async def _send(self, view_dto: UnifiedViewDTO) -> None:
232
+ """Sends View via sender.
233
+
234
+ Args:
235
+ view_dto: DTO to send.
236
+ """
237
+ await self.sender.send(view_dto)
238
+
239
+ # =========================================================================
240
+ # Animation Generators
241
+ # =========================================================================
242
+
243
+ def _generate_animation(
244
+ self,
245
+ step: int,
246
+ total_steps: int,
247
+ text: str,
248
+ animation_type: AnimationType,
249
+ ) -> str:
250
+ """Generates animation string depending on the type.
251
+
252
+ Args:
253
+ step: Current step.
254
+ total_steps: Total steps.
255
+ text: Base text.
256
+ animation_type: Animation type.
257
+
258
+ Returns:
259
+ String with text and ASCII indicator.
260
+ """
261
+ if animation_type == AnimationType.PROGRESS_BAR:
262
+ return self._gen_progress_bar(step, total_steps, text)
263
+ if animation_type == AnimationType.INFINITE:
264
+ return self._gen_infinite_bar(step, text)
265
+ return text
266
+
267
+ def _gen_infinite_bar(self, step: int, text: str) -> str:
268
+ """Running indicator: ``[■□□□□] → [□■□□□] → …``
269
+
270
+ Args:
271
+ step: Current step.
272
+ text: Base text.
273
+
274
+ Returns:
275
+ String with running indicator.
276
+ """
277
+ total_chars = 10
278
+ position = step % total_chars
279
+ bar = "□" * position + "■" + "□" * (total_chars - position - 1)
280
+ return f"{text} [{bar}]"
281
+
282
+ def _gen_progress_bar(self, step: int, total_steps: int, text: str) -> str:
283
+ """Filling indicator: ``[■■□□□] 40%``
284
+
285
+ Args:
286
+ step: Current step.
287
+ total_steps: Total steps.
288
+ text: Base text.
289
+
290
+ Returns:
291
+ String with progress bar and percentage.
292
+ """
293
+ percent = 1.0 if total_steps == 0 else step / total_steps
294
+ total_chars = 10
295
+ filled = int(total_chars * percent)
296
+ empty = total_chars - filled
297
+ bar = "■" * filled + "□" * empty
298
+ return f"{text} [{bar}] {int(percent * 100)}%"
@@ -0,0 +1,22 @@
1
+ """
2
+ codex_bot.base — Base DTOs and abstract orchestrator.
3
+
4
+ Exports all public classes of the module.
5
+ """
6
+
7
+ from codex_bot.base.base_orchestrator import BaseBotOrchestrator, PayloadT
8
+ from codex_bot.base.context_dto import BaseBotContext
9
+ from codex_bot.base.view_dto import (
10
+ MessageCoordsDTO,
11
+ UnifiedViewDTO,
12
+ ViewResultDTO,
13
+ )
14
+
15
+ __all__ = [
16
+ "BaseBotOrchestrator",
17
+ "PayloadT",
18
+ "BaseBotContext",
19
+ "MessageCoordsDTO",
20
+ "UnifiedViewDTO",
21
+ "ViewResultDTO",
22
+ ]
@@ -0,0 +1,123 @@
1
+ """
2
+ BaseBotOrchestrator — Abstract STATELESS feature orchestrator.
3
+
4
+ The Orchestrator is the heart of each feature. It knows how to transform
5
+ an incoming payload into a UnifiedViewDTO for sending to the user.
6
+
7
+ IMPORTANT: The class must be Stateless — it does not store user state in self.
8
+ Orchestrators are singletons shared between all concurrent requests.
9
+ All context is passed through method arguments (director, payload).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import TYPE_CHECKING, Generic, TypeVar
16
+
17
+ from codex_bot.base.view_dto import UnifiedViewDTO, ViewResultDTO
18
+
19
+ if TYPE_CHECKING:
20
+ from codex_bot.director.director import Director
21
+
22
+ PayloadT = TypeVar("PayloadT")
23
+ """TypeVar for the orchestrator payload. A specific subclass specifies the type explicitly."""
24
+
25
+
26
+ class BaseBotOrchestrator(ABC, Generic[PayloadT]): # noqa: UP046
27
+ """Abstract STATELESS feature orchestrator.
28
+
29
+ Defines the contract for all orchestrators in the system.
30
+ The Director uses this interface for cross-feature transitions.
31
+
32
+ The class is a singleton — one instance handles requests from all users
33
+ concurrently. No mutable user state in ``self``.
34
+ All context (user_id, chat_id, FSM) is passed through the ``director``.
35
+
36
+ Subclasses must implement:
37
+ - ``render_content(payload, director)`` — main rendering logic.
38
+
39
+ Subclasses may override:
40
+ - ``handle_entry(director, payload)`` — entry point into the feature.
41
+
42
+ Args:
43
+ expected_state: FSM state string set when entering the feature.
44
+ None — state does not change.
45
+
46
+ Example:
47
+ ```python
48
+ class BookingOrchestrator(BaseBotOrchestrator[BookingPayload]):
49
+ def __init__(self):
50
+ super().__init__(expected_state="BookingStates:main")
51
+
52
+ async def render_content(
53
+ self, payload: BookingPayload, director: Director
54
+ ) -> ViewResultDTO:
55
+ slots = await self.api.get_slots(director.user_id)
56
+ return ViewResultDTO(text=format_slots(slots), kb=build_kb(slots))
57
+ ```
58
+ """
59
+
60
+ def __init__(self, expected_state: str | None = None) -> None:
61
+ self.expected_state = expected_state
62
+
63
+ @abstractmethod
64
+ async def render_content(
65
+ self,
66
+ payload: PayloadT,
67
+ director: Director,
68
+ ) -> ViewResultDTO:
69
+ """Main logic for rendering feature content.
70
+
71
+ Must be implemented in each specific orchestrator.
72
+
73
+ Args:
74
+ payload: Data for rendering (DTO from backend, dict, etc.).
75
+ director: Context of the current request (user_id, chat_id, state).
76
+
77
+ Returns:
78
+ ViewResultDTO with text and keyboard.
79
+ """
80
+ ...
81
+
82
+ async def handle_entry(
83
+ self,
84
+ director: Director,
85
+ payload: PayloadT | None = None,
86
+ ) -> UnifiedViewDTO:
87
+ """Entry point into the feature. Called by the Director during set_scene().
88
+
89
+ The default implementation simply calls render(payload, director).
90
+ Override for complex feature initialization logic.
91
+
92
+ Args:
93
+ director: Context of the current request.
94
+ payload: Initial data for rendering.
95
+
96
+ Returns:
97
+ UnifiedViewDTO for sending to the user.
98
+ """
99
+ return await self.render(payload, director)
100
+
101
+ async def render(
102
+ self,
103
+ payload: PayloadT | None,
104
+ director: Director,
105
+ ) -> UnifiedViewDTO:
106
+ """Assembles UnifiedViewDTO from render_content().
107
+
108
+ Enriches the result with data from the director (chat_id, session_key).
109
+
110
+ Args:
111
+ payload: Data for rendering.
112
+ director: Context of the current request.
113
+
114
+ Returns:
115
+ UnifiedViewDTO ready to be sent via ViewSender.
116
+ """
117
+ content_view = await self.render_content(payload, director) # type: ignore[arg-type]
118
+ return UnifiedViewDTO(content=content_view, menu=None).model_copy(
119
+ update={
120
+ "chat_id": director.chat_id,
121
+ "session_key": director.user_id,
122
+ }
123
+ )
@@ -0,0 +1,42 @@
1
+ """
2
+ Context DTOs — Immutable context of a Telegram event.
3
+
4
+ BaseBotContext contains the minimum necessary set of data
5
+ for any orchestrator to work: user_id, chat_id, message_id.
6
+ """
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+
11
+ class BaseBotContext(BaseModel):
12
+ """
13
+ Base immutable context for a Telegram event.
14
+
15
+ Extracted from Message or CallbackQuery via ContextHelper.
16
+ Contains only identifiers — no business logic.
17
+
18
+ Attributes:
19
+ user_id: Telegram ID of the user. Used as a session key.
20
+ chat_id: ID of the chat (private, group, channel).
21
+ message_id: ID of the message that triggered the event.
22
+ message_thread_id: ID of the topic in a supergroup (if applicable).
23
+ session_key: Key for state storage (defaults to user_id).
24
+
25
+ Example:
26
+ ```python
27
+ ctx = BaseBotContext(user_id=123, chat_id=123, message_id=42)
28
+ print(ctx.session_key) # 123
29
+ ```
30
+ """
31
+
32
+ user_id: int
33
+ chat_id: int
34
+ message_id: int | None = None
35
+ message_thread_id: int | None = None
36
+
37
+ model_config = ConfigDict(frozen=True)
38
+
39
+ @property
40
+ def session_key(self) -> int:
41
+ """Key for storing state in Redis (defaults to user_id)."""
42
+ return self.user_id
@@ -0,0 +1,93 @@
1
+ """
2
+ View DTOs — Immutable response objects from the Orchestrator.
3
+
4
+ All DTOs are frozen (frozen=True) for safe transfer
5
+ between asynchronous services without the risk of race conditions.
6
+ If you need to change a field — use model_copy(update={...}).
7
+ """
8
+
9
+ from typing import Literal
10
+
11
+ from aiogram.types import InlineKeyboardMarkup
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+
15
+ class ViewResultDTO(BaseModel):
16
+ """DTO for representing a single message (text + keyboard).
17
+
18
+ Attributes:
19
+ text: HTML-text of the message.
20
+ kb: Inline keyboard. None — without a keyboard.
21
+
22
+ Example:
23
+ ```python
24
+ view = ViewResultDTO(text="Hello!", kb=my_keyboard)
25
+ ```
26
+ """
27
+
28
+ text: str
29
+ kb: InlineKeyboardMarkup | None = None
30
+
31
+ model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
32
+
33
+
34
+ class MessageCoordsDTO(BaseModel):
35
+ """Telegram message coordinates (chat_id + message_id).
36
+
37
+ Attributes:
38
+ chat_id: Chat ID.
39
+ message_id: Message ID in the chat.
40
+
41
+ Example:
42
+ ```python
43
+ coords = MessageCoordsDTO(chat_id=123456, message_id=42)
44
+ ```
45
+ """
46
+
47
+ chat_id: int
48
+ message_id: int
49
+
50
+ model_config = ConfigDict(frozen=True)
51
+
52
+
53
+ class UnifiedViewDTO(BaseModel):
54
+ """Unified immutable response DTO from the Orchestrator.
55
+
56
+ Contains optional Menu and Content blocks, as well as metadata
57
+ for routing and UI management (deletion, history clearing).
58
+
59
+ Attributes:
60
+ content: Main content block (text + buttons).
61
+ menu: Navigation menu block. None — menu is not updated.
62
+ clean_history: If True — ViewSender will delete previous UI messages.
63
+ alert_text: Text for a popup alert (for CallbackQuery).
64
+ trigger_message_id: ID of the trigger message (e.g., /start) for deletion.
65
+ chat_id: Target chat ID. Filled by the Director.
66
+ session_key: Session key (user_id or channel session). Filled by the Director.
67
+ mode: Sending mode — strictly ``"channel"``, ``"topic"``, or ``"user"``.
68
+ message_thread_id: Topic ID in a supergroup.
69
+
70
+ Example:
71
+ ```python
72
+ view = UnifiedViewDTO(
73
+ content=ViewResultDTO(text="Content"),
74
+ menu=ViewResultDTO(text="Menu"),
75
+ )
76
+ # To change chat_id after creation — use model_copy:
77
+ view = view.model_copy(update={"chat_id": 123456})
78
+ ```
79
+ """
80
+
81
+ content: ViewResultDTO | None = None
82
+ menu: ViewResultDTO | None = None
83
+ clean_history: bool = False
84
+ alert_text: str | None = None
85
+ trigger_message_id: int | None = None
86
+
87
+ # --- Routing & Session (filled by Director) ---
88
+ chat_id: int | str | None = None
89
+ session_key: int | str | None = None
90
+ mode: Literal["channel", "topic", "user"] | None = None
91
+ message_thread_id: int | None = None
92
+
93
+ model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
@@ -0,0 +1,11 @@
1
+ """
2
+ codex_bot.cli — Project management commands.
3
+
4
+ Usage:
5
+ codex-bot create-feature my_feature
6
+ codex-bot create-feature my_feature --type redis
7
+ """
8
+
9
+ from codex_bot.cli.commands import main
10
+
11
+ __all__ = ["main"]