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.
- codex_bot/__init__.py +24 -0
- codex_bot/animation/__init__.py +10 -0
- codex_bot/animation/animation_service.py +298 -0
- codex_bot/base/__init__.py +22 -0
- codex_bot/base/base_orchestrator.py +123 -0
- codex_bot/base/context_dto.py +42 -0
- codex_bot/base/view_dto.py +93 -0
- codex_bot/cli/__init__.py +11 -0
- codex_bot/cli/commands.py +215 -0
- codex_bot/director/__init__.py +13 -0
- codex_bot/director/director.py +107 -0
- codex_bot/director/protocols.py +80 -0
- codex_bot/engine/__init__.py +13 -0
- codex_bot/engine/discovery/__init__.py +7 -0
- codex_bot/engine/discovery/service.py +258 -0
- codex_bot/engine/factory/__init__.py +7 -0
- codex_bot/engine/factory/bot_builder.py +111 -0
- codex_bot/engine/http/__init__.py +10 -0
- codex_bot/engine/http/api_client.py +123 -0
- codex_bot/engine/i18n/__init__.py +7 -0
- codex_bot/engine/i18n/locales_compiler.py +83 -0
- codex_bot/engine/middlewares/__init__.py +24 -0
- codex_bot/engine/middlewares/container.py +46 -0
- codex_bot/engine/middlewares/i18n.py +90 -0
- codex_bot/engine/middlewares/throttling.py +69 -0
- codex_bot/engine/middlewares/user_validation.py +49 -0
- codex_bot/engine/router_builder/__init__.py +10 -0
- codex_bot/engine/router_builder/router_builder.py +133 -0
- codex_bot/fsm/__init__.py +16 -0
- codex_bot/fsm/common_fsm_handlers.py +46 -0
- codex_bot/fsm/garbage_collector.py +141 -0
- codex_bot/fsm/state_helper.py +72 -0
- codex_bot/fsm/state_manager.py +104 -0
- codex_bot/helper/__init__.py +7 -0
- codex_bot/helper/context_helper.py +61 -0
- codex_bot/redis/__init__.py +15 -0
- codex_bot/redis/dispatcher.py +169 -0
- codex_bot/redis/router.py +71 -0
- codex_bot/redis/stream_processor.py +210 -0
- codex_bot/sender/__init__.py +15 -0
- codex_bot/sender/protocols.py +67 -0
- codex_bot/sender/sender_keys.py +50 -0
- codex_bot/sender/sender_manager.py +85 -0
- codex_bot/sender/view_sender.py +200 -0
- codex_bot/templates/feature/callbacks.py.tpl +14 -0
- codex_bot/templates/feature/contract.py.tpl +12 -0
- codex_bot/templates/feature/dto.py.tpl +7 -0
- codex_bot/templates/feature/feature.py.tpl +26 -0
- codex_bot/templates/feature/feature_redis.py.tpl +6 -0
- codex_bot/templates/feature/formatters.py.tpl +15 -0
- codex_bot/templates/feature/handlers.py.tpl +32 -0
- codex_bot/templates/feature/handlers_redis.py.tpl +24 -0
- codex_bot/templates/feature/keyboards.py.tpl +14 -0
- codex_bot/templates/feature/orchestrator.py.tpl +26 -0
- codex_bot/templates/feature/orchestrator_redis.py.tpl +17 -0
- codex_bot/templates/feature/texts.py.tpl +6 -0
- codex_bot/templates/feature/ui.py.tpl +19 -0
- codex_bot/url_signer/__init__.py +7 -0
- codex_bot/url_signer/service.py +116 -0
- codex_bot-0.1.0.dist-info/METADATA +116 -0
- codex_bot-0.1.0.dist-info/RECORD +64 -0
- codex_bot-0.1.0.dist-info/WHEEL +4 -0
- codex_bot-0.1.0.dist-info/entry_points.txt +2 -0
- codex_bot-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for codex-bot feature scaffolding.
|
|
3
|
+
|
|
4
|
+
Used as the ``codex-bot`` entry point (see pyproject.toml).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Path to the library's built-in templates
|
|
14
|
+
_TEMPLATES_DIR = Path(__file__).parent.parent / "templates" / "feature"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_template(name: str, templates_dir: Path) -> str:
|
|
18
|
+
"""Loads a template from a ``.py.tpl`` file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
name: Template name without extension.
|
|
22
|
+
templates_dir: Directory containing templates.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Content of the template file.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
FileNotFoundError: If the template is not found.
|
|
29
|
+
"""
|
|
30
|
+
path = templates_dir / f"{name}.py.tpl"
|
|
31
|
+
if not path.exists():
|
|
32
|
+
raise FileNotFoundError(f"Template not found: {path}")
|
|
33
|
+
return path.read_text(encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_template(base_name: str, suffix: str, templates_dir: Path) -> str:
|
|
37
|
+
"""Loads a specific template, otherwise takes the base one.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_name: Base template name.
|
|
41
|
+
suffix: Suffix for a specific template (e.g., ``"_redis"``).
|
|
42
|
+
templates_dir: Directory containing templates.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Template content.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return _load_template(f"{base_name}{suffix}", templates_dir)
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
return _load_template(base_name, templates_dir)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_feature(
|
|
54
|
+
name: str,
|
|
55
|
+
feature_type: str,
|
|
56
|
+
base_dir: Path | None = None,
|
|
57
|
+
templates_dir: Path | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Creates the structure of a new feature based on templates.
|
|
60
|
+
|
|
61
|
+
Generates a ``features/{feature_type}/{name}/`` directory with files:
|
|
62
|
+
``feature_setting.py``, ``handlers/handlers.py``, ``logic/orchestrator.py``,
|
|
63
|
+
``ui/ui.py``, ``contracts/contract.py``, ``resources/``, and ``tests/``.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Feature name in ``snake_case``.
|
|
67
|
+
feature_type: Feature type — ``"telegram"`` or ``"redis"``.
|
|
68
|
+
base_dir: Project root directory. ``None`` → current directory.
|
|
69
|
+
templates_dir: Templates directory. ``None`` → built-in templates.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
SystemExit: If the feature already exists or a template is not found.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```bash
|
|
76
|
+
codex-bot create-feature booking
|
|
77
|
+
codex-bot create-feature payment_notify --type redis
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
base_dir = base_dir or Path.cwd()
|
|
81
|
+
templates_dir = templates_dir or _TEMPLATES_DIR
|
|
82
|
+
|
|
83
|
+
feature_base = base_dir / "features" / feature_type / name
|
|
84
|
+
|
|
85
|
+
if feature_base.exists():
|
|
86
|
+
print(f"❌ Error: Feature '{name}' already exists in features/{feature_type}/")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Name conversions
|
|
90
|
+
class_name = "".join(word.capitalize() for word in name.split("_"))
|
|
91
|
+
feature_key = name.lower()
|
|
92
|
+
container_key = f"redis_{feature_key}" if feature_type == "redis" else feature_key
|
|
93
|
+
|
|
94
|
+
# Check for paired feature
|
|
95
|
+
other_type = "redis" if feature_type == "telegram" else "telegram"
|
|
96
|
+
has_pair = (base_dir / "features" / other_type / name).exists()
|
|
97
|
+
|
|
98
|
+
# Create directories
|
|
99
|
+
dirs = [
|
|
100
|
+
feature_base,
|
|
101
|
+
feature_base / "handlers",
|
|
102
|
+
feature_base / "logic",
|
|
103
|
+
feature_base / "ui",
|
|
104
|
+
feature_base / "resources",
|
|
105
|
+
feature_base / "contracts",
|
|
106
|
+
feature_base / "tests",
|
|
107
|
+
]
|
|
108
|
+
for d in dirs:
|
|
109
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
(d / "__init__.py").write_text("", encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
suffix = "_redis" if feature_type == "redis" else ""
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
templates: dict[str, str] = {
|
|
116
|
+
"feature.py": _get_template("feature", suffix, templates_dir),
|
|
117
|
+
"handlers.py": _get_template("handlers", suffix, templates_dir),
|
|
118
|
+
"orchestrator.py": _get_template("orchestrator", suffix, templates_dir),
|
|
119
|
+
"ui.py": _get_template("ui", "", templates_dir),
|
|
120
|
+
"contract.py": _get_template("contract", "", templates_dir),
|
|
121
|
+
"texts.py": _get_template("texts", "", templates_dir),
|
|
122
|
+
"keyboards.py": _get_template("keyboards", "", templates_dir),
|
|
123
|
+
"callbacks.py": _get_template("callbacks", "", templates_dir),
|
|
124
|
+
"formatters.py": _get_template("formatters", "", templates_dir),
|
|
125
|
+
"dto.py": _get_template("dto", "", templates_dir),
|
|
126
|
+
}
|
|
127
|
+
except FileNotFoundError as e:
|
|
128
|
+
print(f"❌ Error: Template file not found. {e}")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
format_vars = {
|
|
132
|
+
"class_name": class_name,
|
|
133
|
+
"feature_key": feature_key,
|
|
134
|
+
"container_key": container_key,
|
|
135
|
+
"feature_type": feature_type,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
files: dict[Path, str] = {
|
|
139
|
+
feature_base / "feature_setting.py": templates["feature.py"],
|
|
140
|
+
feature_base / "handlers" / "handlers.py": templates["handlers.py"],
|
|
141
|
+
feature_base / "logic" / "orchestrator.py": templates["orchestrator.py"],
|
|
142
|
+
feature_base / "ui" / "ui.py": templates["ui.py"],
|
|
143
|
+
feature_base / "contracts" / "contract.py": templates["contract.py"],
|
|
144
|
+
feature_base / "resources" / "texts.py": templates["texts.py"],
|
|
145
|
+
feature_base / "resources" / "keyboards.py": templates["keyboards.py"],
|
|
146
|
+
feature_base / "resources" / "callbacks.py": templates["callbacks.py"],
|
|
147
|
+
feature_base / "resources" / "formatters.py": templates["formatters.py"],
|
|
148
|
+
feature_base / "resources" / "dto.py": templates["dto.py"],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for file_path, template in files.items():
|
|
152
|
+
content = template.format(**format_vars)
|
|
153
|
+
file_path.write_text(content, encoding="utf-8")
|
|
154
|
+
|
|
155
|
+
# Export router from handlers/__init__.py
|
|
156
|
+
router_attr = "redis_router" if feature_type == "redis" else "router"
|
|
157
|
+
init_content = f"from .handlers import {router_attr}\n"
|
|
158
|
+
(feature_base / "handlers" / "__init__.py").write_text(init_content, encoding="utf-8")
|
|
159
|
+
|
|
160
|
+
print(f"\n✅ Feature '{name}' ({feature_type}) successfully created!")
|
|
161
|
+
|
|
162
|
+
if has_pair:
|
|
163
|
+
print(f"🔗 Paired feature found in '{other_type}'!")
|
|
164
|
+
if feature_type == "telegram":
|
|
165
|
+
print("💡 Tip: Configure inheritance in resources/callbacks.py to handle buttons from Redis notifications.")
|
|
166
|
+
|
|
167
|
+
setting_list = "INSTALLED_FEATURES" if feature_type == "telegram" else "INSTALLED_REDIS_FEATURES"
|
|
168
|
+
print(f"👉 Add 'features.{feature_type}.{name}' to {setting_list}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main() -> None:
|
|
172
|
+
"""CLI entry point for ``codex-bot``.
|
|
173
|
+
|
|
174
|
+
Subcommands:
|
|
175
|
+
create-feature: Create a new feature.
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
```bash
|
|
179
|
+
codex-bot create-feature booking
|
|
180
|
+
codex-bot create-feature payment_notify --type redis
|
|
181
|
+
```
|
|
182
|
+
"""
|
|
183
|
+
parser = argparse.ArgumentParser(
|
|
184
|
+
description="codex-bot: Aiogram Bot Management Framework",
|
|
185
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
186
|
+
)
|
|
187
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
188
|
+
|
|
189
|
+
create_parser = subparsers.add_parser("create-feature", help="Create a new feature")
|
|
190
|
+
create_parser.add_argument("name", help="Feature name in snake_case")
|
|
191
|
+
create_parser.add_argument(
|
|
192
|
+
"--type",
|
|
193
|
+
choices=["telegram", "redis"],
|
|
194
|
+
default="telegram",
|
|
195
|
+
help="Feature type (default: telegram)",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if len(sys.argv) == 1:
|
|
199
|
+
parser.print_help()
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
args = parser.parse_args()
|
|
203
|
+
|
|
204
|
+
if args.command == "create-feature":
|
|
205
|
+
if not args.name:
|
|
206
|
+
create_parser.print_help()
|
|
207
|
+
else:
|
|
208
|
+
create_feature(args.name, args.type)
|
|
209
|
+
else:
|
|
210
|
+
parser.print_help()
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_bot.director — Cross-feature transition coordinator.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from codex_bot.director.director import Director
|
|
6
|
+
from codex_bot.director.protocols import ContainerProtocol, OrchestratorProtocol, SceneConfig
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Director",
|
|
10
|
+
"OrchestratorProtocol",
|
|
11
|
+
"ContainerProtocol",
|
|
12
|
+
"SceneConfig",
|
|
13
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Director — Coordinator of cross-feature transitions.
|
|
3
|
+
|
|
4
|
+
The Director knows how to transition from one feature to another: change the FSM state,
|
|
5
|
+
get the required orchestrator from the container, and call its handle_entry().
|
|
6
|
+
Orchestrators are stateless — the Director passes itself as context with each call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from aiogram.fsm.context import FSMContext
|
|
15
|
+
|
|
16
|
+
from ..base.view_dto import UnifiedViewDTO
|
|
17
|
+
from .protocols import ContainerProtocol, OrchestratorProtocol
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Director:
|
|
23
|
+
"""Coordinator of transitions between features (scenes).
|
|
24
|
+
|
|
25
|
+
Instantiated in the handler for each incoming request.
|
|
26
|
+
Stores the request context (user_id, chat_id, state) and passes itself
|
|
27
|
+
to orchestrators as an argument — no mutable state in the orchestrator.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
container: Project's DI container with a ``features`` attribute.
|
|
31
|
+
state: FSM context of the current user.
|
|
32
|
+
user_id: Telegram ID of the user.
|
|
33
|
+
chat_id: Target chat ID.
|
|
34
|
+
trigger_id: ID of the trigger message (e.g., /start) for subsequent deletion.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
director = Director(
|
|
39
|
+
container=container,
|
|
40
|
+
state=state,
|
|
41
|
+
user_id=callback.from_user.id,
|
|
42
|
+
chat_id=callback.message.chat.id,
|
|
43
|
+
)
|
|
44
|
+
view = await director.set_scene(feature="booking", payload=None)
|
|
45
|
+
await sender.send(view)
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
container: ContainerProtocol,
|
|
52
|
+
state: FSMContext | None = None,
|
|
53
|
+
user_id: int | None = None,
|
|
54
|
+
chat_id: int | None = None,
|
|
55
|
+
trigger_id: int | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.container = container
|
|
58
|
+
self.state = state
|
|
59
|
+
self.user_id = user_id
|
|
60
|
+
self.chat_id = chat_id
|
|
61
|
+
self.trigger_id = trigger_id
|
|
62
|
+
|
|
63
|
+
async def set_scene(self, feature: str, payload: Any = None) -> Any:
|
|
64
|
+
"""Cross-feature transition: changes FSM state and calls the feature orchestrator.
|
|
65
|
+
|
|
66
|
+
Algorithm:
|
|
67
|
+
1. Retrieves the orchestrator by key from ``container.features``.
|
|
68
|
+
2. Sets the FSM state if the orchestrator has declared it.
|
|
69
|
+
3. Passes itself to ``handle_entry(director=self, payload=payload)``.
|
|
70
|
+
4. Enriches the result with ``chat_id`` and ``session_key`` as a fallback.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
feature: Orchestrator key in ``container.features`` (e.g., ``"booking"``).
|
|
74
|
+
payload: Data to pass to ``handle_entry()``.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
UnifiedViewDTO or any orchestrator result. None if the feature is not found.
|
|
78
|
+
"""
|
|
79
|
+
orchestrator = self.container.features.get(feature)
|
|
80
|
+
|
|
81
|
+
if orchestrator is None:
|
|
82
|
+
log.error(f"Director | unknown_feature='{feature}' user_id={self.user_id}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# 1. FSM state change
|
|
86
|
+
if self.state and hasattr(orchestrator, "expected_state") and orchestrator.expected_state:
|
|
87
|
+
await self.state.set_state(orchestrator.expected_state)
|
|
88
|
+
|
|
89
|
+
# 2. Call handle_entry (pass self as context) or render
|
|
90
|
+
if isinstance(orchestrator, OrchestratorProtocol):
|
|
91
|
+
view = await orchestrator.handle_entry(director=self, payload=payload)
|
|
92
|
+
elif hasattr(orchestrator, "render"):
|
|
93
|
+
view = await orchestrator.render(payload, self)
|
|
94
|
+
else:
|
|
95
|
+
log.warning(f"Director | orchestrator='{feature}' has no handle_entry or render")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# 3. Fallback enrichment of UnifiedViewDTO with session data
|
|
99
|
+
if isinstance(view, UnifiedViewDTO):
|
|
100
|
+
view = view.model_copy(
|
|
101
|
+
update={
|
|
102
|
+
"chat_id": view.chat_id or self.chat_id,
|
|
103
|
+
"session_key": view.session_key or self.user_id,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return view
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocols for Director — Dependency inversion without concrete classes.
|
|
3
|
+
|
|
4
|
+
OrchestratorProtocol describes the minimum contract of a stateless orchestrator,
|
|
5
|
+
ContainerProtocol — the minimum contract of the project's DI container.
|
|
6
|
+
The library does not know about specific implementations — only about interfaces.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, NamedTuple, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from aiogram.fsm.state import State
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SceneConfig(NamedTuple):
|
|
15
|
+
"""Scene configuration: FSM state + entry-point service key.
|
|
16
|
+
|
|
17
|
+
Used in the project's SCENE_ROUTES to describe cross-feature transitions.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
fsm_state: Aiogram State set when entering the scene.
|
|
21
|
+
entry_service: Orchestrator key in the container registry (e.g., ``"booking"``).
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```python
|
|
25
|
+
SCENE_ROUTES = {
|
|
26
|
+
"booking": SceneConfig(
|
|
27
|
+
fsm_state=BookingStates.main,
|
|
28
|
+
entry_service="booking",
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
fsm_state: State
|
|
35
|
+
entry_service: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class OrchestratorProtocol(Protocol):
|
|
40
|
+
"""Minimum contract for a stateless feature orchestrator.
|
|
41
|
+
|
|
42
|
+
The Director works through this protocol without knowing about specific classes.
|
|
43
|
+
BaseBotOrchestrator implements it automatically.
|
|
44
|
+
|
|
45
|
+
The orchestrator must be stateless — it does not store user state.
|
|
46
|
+
Context is passed via the ``director`` argument on each call.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
async def render(self, payload: Any, director: Any) -> Any:
|
|
50
|
+
"""Renders content for the passed payload."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def handle_entry(
|
|
54
|
+
self,
|
|
55
|
+
director: Any,
|
|
56
|
+
payload: Any = None,
|
|
57
|
+
) -> Any:
|
|
58
|
+
"""Entry point into the feature."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@runtime_checkable
|
|
63
|
+
class ContainerProtocol(Protocol):
|
|
64
|
+
"""Minimum contract for the project's DI container.
|
|
65
|
+
|
|
66
|
+
The Director only requires the ``features`` attribute — a dictionary of orchestrators.
|
|
67
|
+
The specific BotContainer of the project must provide this attribute.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
features: Dictionary of ``{feature_key: orchestrator}``.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```python
|
|
74
|
+
class BotContainer:
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self.features: dict[str, Any] = {}
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
features: dict[str, OrchestratorProtocol]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codex_bot.engine — инфраструктурный движок (подкапотная магия).
|
|
3
|
+
|
|
4
|
+
Содержит модули, которые нужны для сборки бота, но не используются
|
|
5
|
+
разработчиком фичи в повседневной работе.
|
|
6
|
+
|
|
7
|
+
Подмодули:
|
|
8
|
+
engine.middlewares — системные middleware (Throttling, Security, Container, …)
|
|
9
|
+
engine.discovery — FeatureDiscoveryService (авто-обнаружение фич)
|
|
10
|
+
engine.factory — BotBuilder, compile_locales
|
|
11
|
+
engine.router_builder — collect_feature_routers, build_main_router
|
|
12
|
+
engine.http — BaseApiClient, ApiClientError
|
|
13
|
+
"""
|