Skip to content

Commit 1929494

Browse files
committed
clean pybotx
1 parent 6397f9e commit 1929494

479 files changed

Lines changed: 22542 additions & 5496 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/python-app.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343

4444
- name: Run linters
4545
run: |
46+
poetry run python scripts/generate_botx_facets.py --check
4647
poetry run ./scripts/lint
4748
4849
docs-lint:

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,9 @@ collector = HandlerCollector()
232232
@collector.sync_smartapp_event
233233
async def handle_sync_smartapp_event(
234234
event: SmartAppEvent, bot: Bot,
235-
) -> BotAPISyncSmartAppEventResultResponse:
235+
) -> SyncSmartAppEventResult:
236236
print(f"Got sync smartapp event: {event}")
237-
return BotAPISyncSmartAppEventResultResponse.from_domain(
238-
data={},
239-
files=[],
240-
)
237+
return SyncSmartAppEventResult(data={}, files=[])
241238
```
242239

243240

docs/adapters.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Веб-адаптеры
2+
3+
## FastAPI
4+
5+
```python
6+
from fastapi import FastAPI
7+
from pybotx import Bot, wrap_asgi_app
8+
from pybotx.presentation.fastapi import FastAPIAdapter
9+
10+
adapter = FastAPIAdapter(bot, verify_requests=True)
11+
app = FastAPI(title="Todo Bot")
12+
app.include_router(adapter.router)
13+
app = wrap_asgi_app(app, bot)
14+
```
15+
16+
Если не используете `wrap_asgi_app`, подключите lifecycle вручную:
17+
18+
```python
19+
app.add_event_handler("startup", bot.startup)
20+
app.add_event_handler("shutdown", bot.shutdown)
21+
```
22+
23+
## aiohttp
24+
25+
```python
26+
from aiohttp import web
27+
from pybotx.presentation.aiohttp import AiohttpAdapter
28+
29+
adapter = AiohttpAdapter(bot, verify_requests=True)
30+
app = web.Application()
31+
app.add_routes(adapter.routes)
32+
```
33+
34+
## Django Ninja (ASGI)
35+
36+
```python
37+
from ninja import NinjaAPI
38+
from pybotx.presentation.django_ninja import DjangoNinjaAdapter
39+
40+
api = NinjaAPI()
41+
adapter = DjangoNinjaAdapter(bot, verify_requests=True)
42+
api.add_router("/", adapter.router)
43+
```
44+
45+
Для ASGI убедитесь, что `bot.startup()` и `bot.shutdown()` вызываются в lifecycle.
46+

docs/architecture.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Архитектура и слои
2+
3+
## Модель слоёв
4+
5+
Мы придерживаемся 4 слоёв и строгого направления зависимостей:
6+
7+
1. Domain
8+
2. Application
9+
3. Infrastructure
10+
4. Presentation
11+
12+
Зависимости направлены только сверху вниз. Domain не зависит ни от чего. Application зависит от Domain. Infrastructure и Presentation зависят от Application и Domain. Контракты разделены на inbound и outbound, чтобы убрать боковые зависимости.
13+
14+
## Что где живёт
15+
16+
### Domain
17+
18+
- Чистые бизнес-модели и value objects
19+
- Доменные порты и ошибки
20+
- Модели и builders сообщений
21+
- Доменная логика виджетов
22+
- Утилиты для текста и упоминаний
23+
24+
### Application
25+
26+
- Фасад бота
27+
- Handler collectors
28+
- WidgetFactory и WidgetSession
29+
- Оркестрация use-cases
30+
31+
### Infrastructure
32+
33+
- HTTP клиенты и retry policy
34+
- Реализация BotX API
35+
- JWT encoder и verifier
36+
- Хранилища и adapters
37+
- AttachmentFactory implementation
38+
39+
### Presentation
40+
41+
- Web adapters
42+
- DTO для входящих payloads
43+
- Framework-specific роутинг и lifecycle
44+
45+
## Контракты
46+
47+
Контракты разделены на inbound и outbound, чтобы убрать зависимость domain от транспортных DTO.
48+
49+
- Inbound: presentation payloads
50+
- Outbound: infrastructure API payloads
51+
52+
## Порты и адаптеры
53+
54+
Все внешние зависимости accessed через порты. Infrastructure реализует адаптеры этих портов.
55+
56+
Примеры:
57+
58+
- `WidgetStateStorePort` -> `InMemoryWidgetStateStore` / `RedisWidgetStateStore`
59+
- `JwtEncoderPort` -> `PyJwtEncoder`
60+
- `JwtVerifierPort` -> `PyJwtVerifier`
61+
- `HttpClientPort` -> `HttpxClientAdapter` / `AioHttpClientAdapter`
62+
63+
## DI границы
64+
65+
DI делается только в контейнерах. Все runtime настройки прокидываются в `container.config`.
66+

docs/attachments.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# AttachmentFactory
2+
3+
`AttachmentFactory` изолирует I/O и даёт удобные методы для создания `OutgoingAttachment`.
4+
5+
## From bytes
6+
7+
```python
8+
attachment = await attachment_factory.from_bytes(
9+
b"hello",
10+
filename="hello.txt",
11+
)
12+
```
13+
14+
## From file path
15+
16+
```python
17+
attachment = await attachment_factory.from_path("/path/to/file.txt")
18+
```
19+
20+
## From file object
21+
22+
```python
23+
from io import BytesIO
24+
25+
buffer = BytesIO(b"content")
26+
attachment = await attachment_factory.from_file(buffer, filename="demo.txt")
27+
```
28+
29+
## Использование с builder
30+
31+
```python
32+
message = (
33+
OutgoingMessageBuilder(bot_id=bot_id, chat_id=chat_id, body="file")
34+
.with_file(attachment)
35+
.build()
36+
)
37+
await bot.send(message=message)
38+
```
39+

docs/bot.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Bot API
2+
3+
Основные методы `Bot` и ключевые изменения.
4+
5+
## Reply и edit sugar
6+
7+
Без contextvars и без ручного проброса ID.
8+
9+
```python
10+
await bot.reply_to(
11+
message,
12+
body="Thanks!",
13+
)
14+
15+
await bot.edit_from(
16+
message,
17+
body="Updated text",
18+
)
19+
```
20+
21+
## Bulk операции
22+
23+
### Bulk send
24+
25+
```python
26+
messages = [
27+
OutgoingMessageBuilder(...).build(),
28+
OutgoingMessageBuilder(...).build(),
29+
]
30+
result = await bot.bulk_send(messages=messages, max_concurrency=5)
31+
```
32+
33+
### Bulk reply
34+
35+
```python
36+
messages = [
37+
ReplyMessageBuilder.for_incoming_message(message, body="one").build(),
38+
ReplyMessageBuilder.for_incoming_message(message, body="two").build(),
39+
]
40+
result = await bot.bulk_reply(messages=messages, max_concurrency=3)
41+
```
42+
43+
### Bulk с общими MessageOptions
44+
45+
Bulk операции принимают `options`. Значения в отдельных сообщениях имеют приоритет над общими.
46+
47+
```python
48+
options = MessageOptions(silent_response=True, send_push=False)
49+
result = await bot.bulk_send(messages=messages, options=options)
50+
```
51+
52+
## Request verification
53+
54+
`RequestVerifier` доступен как публичный интерфейс и может быть заменён через DI.
55+
56+
## Логирование
57+
58+
Логгер можно инжектить через контейнер. Порт логгера поддерживает `enable()` и `disable()`.
59+

docs/bot_command_link.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Bot Command Link
2+
3+
`build_bot_command_link` генерирует ссылку, которая открывает чат с ботом и
4+
отправляет ему команду. Это удобно для внешних UI, SmartApp или админ‑панелей.
5+
6+
## Пример
7+
8+
```python
9+
from pybotx import build_bot_command_link
10+
11+
link = build_bot_command_link(
12+
huid=bot_huid,
13+
command="/start",
14+
body="/start",
15+
)
16+
```
17+
18+
## Параметры
19+
20+
- `huid`: UUID бота (его HUID)
21+
- `command`: команда, например `/start` или `/help`
22+
- `body`: текст сообщения (если указан `command`, `body` обязателен)
23+
- `host`: по умолчанию `xlnk.ms`
24+
- `scheme`: по умолчанию `https`
25+
26+
## Рекомендации
27+
28+
- Храните ссылку в UI и показывайте пользователю только при необходимости.
29+
- Команда должна быть валидной BotX командой.

docs/botx_api.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# BotX API
2+
3+
Высокоуровневый интерфейс представлен через `Bot`. Низкоуровневый API реализован в `HttpBotXApi` и строится через `BotXApiMethodFactory`.
4+
5+
Рекомендуемый путь работы:
6+
7+
- Использовать методы `Bot`
8+
- Использовать builders и bulk операции
9+
- Использовать `MessageOptions` для единообразного API
10+
11+
Если нужны transport DTO:
12+
13+
- Inbound contracts для входящих payloads
14+
- Outbound contracts для исходящих API payloads
15+

docs/builders.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Message Builders и Options
2+
3+
## MessageOptions
4+
5+
`MessageOptions` упаковывает delivery-опции в один объект.
6+
7+
```python
8+
from pybotx import MessageOptions
9+
10+
options = MessageOptions(
11+
silent_response=True,
12+
markup_auto_adjust=True,
13+
stealth_mode=True,
14+
send_push=False,
15+
ignore_mute=True,
16+
)
17+
```
18+
19+
## OutgoingMessageBuilder
20+
21+
```python
22+
from pybotx import OutgoingMessageBuilder
23+
24+
message = (
25+
OutgoingMessageBuilder(bot_id=bot_id, chat_id=chat_id, body="Hi!")
26+
.silent()
27+
.auto_adjust_buttons()
28+
.stealth()
29+
.no_push()
30+
.force_notification()
31+
.build()
32+
)
33+
34+
await bot.send(message=message)
35+
```
36+
37+
`with_options` применяет `MessageOptions` к builder.
38+
39+
```python
40+
message = OutgoingMessageBuilder(bot_id=bot_id, chat_id=chat_id, body="Hi")
41+
message = message.with_options(options).build()
42+
```
43+
44+
## ReplyMessageBuilder
45+
46+
### From incoming message
47+
48+
```python
49+
builder = ReplyMessageBuilder.for_incoming_message(message, body="Reply text")
50+
reply = builder.with_options(options).build()
51+
await bot.reply(message=reply)
52+
```
53+
54+
### Explicit sync_id
55+
56+
```python
57+
builder = ReplyMessageBuilder.for_incoming(message, body="Reply", sync_id=sync_id)
58+
reply = builder.build()
59+
```
60+
61+
## EditMessageBuilder
62+
63+
### From source sync id
64+
65+
```python
66+
builder = EditMessageBuilder.for_incoming_source_id(message)
67+
edit = builder.with_body("Updated text").build()
68+
await bot.edit(message=edit)
69+
```
70+
71+
### Clear helpers
72+
73+
```python
74+
edit = (
75+
EditMessageBuilder.for_incoming_source_id(message)
76+
.clear_body()
77+
.clear_metadata()
78+
.clear_bubbles()
79+
.clear_keyboard()
80+
.clear_file()
81+
.build()
82+
)
83+
```
84+

0 commit comments

Comments
 (0)