Stage 01 - bootstrap v2 stable start
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.DS_Store
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
app/.env
|
||||||
|
app/.venv/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
logs/
|
||||||
|
data/
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "app/.venv/bin/python",
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system"
|
||||||
|
}
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# dzentra_bot
|
||||||
|
|
||||||
|
Telegram-бот для автоторговли криптовалютой.
|
||||||
|
|
||||||
|
## Bootstrap v2
|
||||||
|
Это стабильный стартовый каркас проекта с:
|
||||||
|
- чистой структурой каталогов
|
||||||
|
- компактным верхним меню
|
||||||
|
- базовыми handlers
|
||||||
|
- документацией
|
||||||
|
- Docker-файлами
|
||||||
|
- опциональным созданием `.venv`, установкой зависимостей и первым git commit
|
||||||
|
|
||||||
|
## Верхнее меню
|
||||||
|
- 🏠 Главная
|
||||||
|
- 📈 Рынок
|
||||||
|
- 💼 Портфель
|
||||||
|
- ⚡ Торговля
|
||||||
|
- 🤖 Авто
|
||||||
|
- 📒 Журнал
|
||||||
|
- ⚙️ Система
|
||||||
|
|
||||||
|
## Рекомендуемый запуск
|
||||||
|
```bash
|
||||||
|
/opt/homebrew/bin/python3.12 bootstrap_project.py --with-venv --with-install
|
||||||
|
```
|
||||||
|
|
||||||
|
## После bootstrap
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m src.main
|
||||||
|
```
|
||||||
5
app/.env.example
Normal file
5
app/.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
BOT_TOKEN=PUT_YOUR_TELEGRAM_BOT_TOKEN_HERE
|
||||||
|
BOT_PARSE_MODE=HTML
|
||||||
|
APP_ENV=dev
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
TZ=Europe/Madrid
|
||||||
3
app/README.md
Normal file
3
app/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# app
|
||||||
|
|
||||||
|
Здесь находятся исходный код приложения, env-файлы и зависимости.
|
||||||
2
app/requirements.txt
Normal file
2
app/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
aiogram==3.13.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
1
app/src/__init__.py
Normal file
1
app/src/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/src/bootstrap/__init__.py
Normal file
1
app/src/bootstrap/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
22
app/src/bootstrap/app_factory.py
Normal file
22
app/src/bootstrap/app_factory.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
|
from src.bootstrap.logging import setup_logging
|
||||||
|
from src.core.config import load_settings
|
||||||
|
from src.telegram.routers import setup_routers
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> tuple[Bot, Dispatcher]:
|
||||||
|
settings = load_settings()
|
||||||
|
setup_logging(settings.log_level)
|
||||||
|
|
||||||
|
bot = Bot(
|
||||||
|
token=settings.bot_token,
|
||||||
|
default=DefaultBotProperties(parse_mode=settings.bot_parse_mode),
|
||||||
|
)
|
||||||
|
dispatcher = Dispatcher()
|
||||||
|
setup_routers(dispatcher)
|
||||||
|
|
||||||
|
return bot, dispatcher
|
||||||
10
app/src/bootstrap/logging.py
Normal file
10
app/src/bootstrap/logging.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(log_level: str) -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, log_level.upper(), logging.INFO),
|
||||||
|
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||||
|
)
|
||||||
1
app/src/core/__init__.py
Normal file
1
app/src/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
35
app/src/core/config.py
Normal file
35
app/src/core/config.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
ENV_FILE = BASE_DIR / ".env"
|
||||||
|
load_dotenv(ENV_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Settings:
|
||||||
|
bot_token: str
|
||||||
|
bot_parse_mode: str
|
||||||
|
app_env: str
|
||||||
|
log_level: str
|
||||||
|
tz: str
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings() -> Settings:
|
||||||
|
bot_token = os.getenv("BOT_TOKEN", "").strip()
|
||||||
|
if not bot_token:
|
||||||
|
raise RuntimeError("BOT_TOKEN is not set in app/.env")
|
||||||
|
|
||||||
|
return Settings(
|
||||||
|
bot_token=bot_token,
|
||||||
|
bot_parse_mode=os.getenv("BOT_PARSE_MODE", "HTML").strip() or "HTML",
|
||||||
|
app_env=os.getenv("APP_ENV", "dev").strip() or "dev",
|
||||||
|
log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO",
|
||||||
|
tz=os.getenv("TZ", "Europe/Madrid").strip() or "Europe/Madrid",
|
||||||
|
)
|
||||||
2
app/src/core/constants.py
Normal file
2
app/src/core/constants.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
APP_NAME = "Dzentra Bot"
|
||||||
|
APP_VERSION = "2.0.0"
|
||||||
2
app/src/core/exceptions.py
Normal file
2
app/src/core/exceptions.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class AppError(Exception):
|
||||||
|
"""Base application exception."""
|
||||||
1
app/src/integrations/README.md
Normal file
1
app/src/integrations/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Здесь будут внешние интеграции, например биржа.
|
||||||
1
app/src/integrations/__init__.py
Normal file
1
app/src/integrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
12
app/src/main.py
Normal file
12
app/src/main.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.bootstrap.app_factory import create_app
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
bot, dispatcher = create_app()
|
||||||
|
await dispatcher.start_polling(bot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
1
app/src/shared/README.md
Normal file
1
app/src/shared/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Здесь будут общие утилиты.
|
||||||
1
app/src/shared/__init__.py
Normal file
1
app/src/shared/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/src/storage/README.md
Normal file
1
app/src/storage/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Здесь будет слой доступа к данным.
|
||||||
1
app/src/storage/__init__.py
Normal file
1
app/src/storage/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/src/telegram/__init__.py
Normal file
1
app/src/telegram/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/src/telegram/callbacks/__init__.py
Normal file
1
app/src/telegram/callbacks/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/src/telegram/handlers/__init__.py
Normal file
1
app/src/telegram/handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
12
app/src/telegram/handlers/auto.py
Normal file
12
app/src/telegram/handlers/auto.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import AUTO_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="auto")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "🤖 Авто")
|
||||||
|
async def open_auto(message: Message) -> None:
|
||||||
|
await message.answer(AUTO_TEXT)
|
||||||
12
app/src/telegram/handlers/home.py
Normal file
12
app/src/telegram/handlers/home.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import HOME_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="home")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "🏠 Главная")
|
||||||
|
async def open_home(message: Message) -> None:
|
||||||
|
await message.answer(HOME_TEXT)
|
||||||
12
app/src/telegram/handlers/journal.py
Normal file
12
app/src/telegram/handlers/journal.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import JOURNAL_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="journal")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📒 Журнал")
|
||||||
|
async def open_journal(message: Message) -> None:
|
||||||
|
await message.answer(JOURNAL_TEXT)
|
||||||
12
app/src/telegram/handlers/market.py
Normal file
12
app/src/telegram/handlers/market.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import MARKET_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="market")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📈 Рынок")
|
||||||
|
async def open_market(message: Message) -> None:
|
||||||
|
await message.answer(MARKET_TEXT)
|
||||||
12
app/src/telegram/handlers/portfolio.py
Normal file
12
app/src/telegram/handlers/portfolio.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import PORTFOLIO_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="portfolio")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "💼 Портфель")
|
||||||
|
async def open_portfolio(message: Message) -> None:
|
||||||
|
await message.answer(PORTFOLIO_TEXT)
|
||||||
29
app/src/telegram/handlers/start.py
Normal file
29
app/src/telegram/handlers/start.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.keyboards.reply import build_main_menu_keyboard
|
||||||
|
from src.telegram.menus import MAIN_MENU_TEXT, SYSTEM_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="start")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("start"))
|
||||||
|
async def cmd_start(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("menu"))
|
||||||
|
async def cmd_menu(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("help"))
|
||||||
|
async def cmd_help(message: Message) -> None:
|
||||||
|
await message.answer(SYSTEM_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "Меню")
|
||||||
|
async def menu_shortcut(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
21
app/src/telegram/handlers/system.py
Normal file
21
app/src/telegram/handlers/system.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import platform
|
||||||
|
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.core.constants import APP_NAME, APP_VERSION
|
||||||
|
from src.telegram.menus import SYSTEM_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="system")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.in_({"⚙️ Система", "⚙ Система"}))
|
||||||
|
async def open_system(message: Message) -> None:
|
||||||
|
runtime_info = (
|
||||||
|
"\n\n<b>Runtime</b>\n"
|
||||||
|
f"- app: {APP_NAME} {APP_VERSION}\n"
|
||||||
|
f"- python: {platform.python_version()}\n"
|
||||||
|
f"- os: {platform.system()} {platform.release()}"
|
||||||
|
)
|
||||||
|
await message.answer(SYSTEM_TEXT + runtime_info)
|
||||||
12
app/src/telegram/handlers/trade.py
Normal file
12
app/src/telegram/handlers/trade.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import TRADE_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="trade")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "⚡ Торговля")
|
||||||
|
async def open_trade(message: Message) -> None:
|
||||||
|
await message.answer(TRADE_TEXT)
|
||||||
1
app/src/telegram/keyboards/__init__.py
Normal file
1
app/src/telegram/keyboards/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
23
app/src/telegram/keyboards/reply.py
Normal file
23
app/src/telegram/keyboards/reply.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
|
||||||
|
|
||||||
|
|
||||||
|
def build_main_menu_keyboard() -> ReplyKeyboardMarkup:
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton(text="🏠 Главная"),
|
||||||
|
KeyboardButton(text="📈 Рынок"),
|
||||||
|
KeyboardButton(text="💼 Портфель"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="⚡ Торговля"),
|
||||||
|
KeyboardButton(text="🤖 Авто"),
|
||||||
|
KeyboardButton(text="📒 Журнал"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="⚙️ Система"),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
resize_keyboard=True,
|
||||||
|
input_field_placeholder="Выбери раздел...",
|
||||||
|
)
|
||||||
30
app/src/telegram/menus.py
Normal file
30
app/src/telegram/menus.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
MAIN_MENU_TEXT = (
|
||||||
|
"<b>Dzentra Bot</b>\n\n"
|
||||||
|
"Новый каркас проекта успешно создан.\n\n"
|
||||||
|
"Выбери раздел через меню ниже."
|
||||||
|
)
|
||||||
|
|
||||||
|
HOME_TEXT = (
|
||||||
|
"<b>🏠 Главная</b>\n\n"
|
||||||
|
"Это главный экран бота.\n\n"
|
||||||
|
"Сейчас здесь отображается базовый статус:\n"
|
||||||
|
"- бот запущен\n"
|
||||||
|
"- меню подключено\n"
|
||||||
|
"- handlers работают\n"
|
||||||
|
"- проект на этапе Bootstrap v2\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
SYSTEM_TEXT = (
|
||||||
|
"<b>⚙️ Система</b>\n\n"
|
||||||
|
"Системный экран.\n\n"
|
||||||
|
"<b>Справка</b>\n"
|
||||||
|
"/start — запуск\n"
|
||||||
|
"/menu — показать меню\n"
|
||||||
|
"/help — краткая справка\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
MARKET_TEXT = "<b>📈 Рынок</b>\n\nРаздел пока в разработке."
|
||||||
|
PORTFOLIO_TEXT = "<b>💼 Портфель</b>\n\nРаздел пока в разработке."
|
||||||
|
TRADE_TEXT = "<b>⚡ Торговля</b>\n\nРаздел пока в разработке."
|
||||||
|
AUTO_TEXT = "<b>🤖 Авто</b>\n\nРаздел пока в разработке."
|
||||||
|
JOURNAL_TEXT = "<b>📒 Журнал</b>\n\nРаздел пока в разработке."
|
||||||
21
app/src/telegram/routers.py
Normal file
21
app/src/telegram/routers.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from aiogram import Dispatcher
|
||||||
|
|
||||||
|
from src.telegram.handlers.auto import router as auto_router
|
||||||
|
from src.telegram.handlers.home import router as home_router
|
||||||
|
from src.telegram.handlers.journal import router as journal_router
|
||||||
|
from src.telegram.handlers.market import router as market_router
|
||||||
|
from src.telegram.handlers.portfolio import router as portfolio_router
|
||||||
|
from src.telegram.handlers.start import router as start_router
|
||||||
|
from src.telegram.handlers.system import router as system_router
|
||||||
|
from src.telegram.handlers.trade import router as trade_router
|
||||||
|
|
||||||
|
|
||||||
|
def setup_routers(dispatcher: Dispatcher) -> None:
|
||||||
|
dispatcher.include_router(start_router)
|
||||||
|
dispatcher.include_router(home_router)
|
||||||
|
dispatcher.include_router(market_router)
|
||||||
|
dispatcher.include_router(portfolio_router)
|
||||||
|
dispatcher.include_router(trade_router)
|
||||||
|
dispatcher.include_router(auto_router)
|
||||||
|
dispatcher.include_router(journal_router)
|
||||||
|
dispatcher.include_router(system_router)
|
||||||
1
app/src/trading/README.md
Normal file
1
app/src/trading/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Здесь будет торговая бизнес-логика.
|
||||||
1
app/src/trading/__init__.py
Normal file
1
app/src/trading/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
1
app/tests/__init__.py
Normal file
1
app/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Package marker."""
|
||||||
2
app/tests/test_smoke.py
Normal file
2
app/tests/test_smoke.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def test_smoke() -> None:
|
||||||
|
assert True
|
||||||
727
bootstrap_project.py
Normal file
727
bootstrap_project.py
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path.cwd()
|
||||||
|
MIN_PYTHON = (3, 12)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(text: str) -> str:
|
||||||
|
return textwrap.dedent(text).lstrip("\n").rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def write(path: Path, content: str, overwrite: bool = False) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if path.exists() and not overwrite:
|
||||||
|
print(f"[SKIP] {path}")
|
||||||
|
return
|
||||||
|
path.write_text(norm(content), encoding="utf-8")
|
||||||
|
print(f"[WRITE] {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def touch(path: Path) -> None:
|
||||||
|
write(path, '"""Package marker."""\n', overwrite=False)
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: list[str], cwd: Path | None = None, allow_fail: bool = False) -> int:
|
||||||
|
print("[RUN ]", " ".join(cmd))
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(cmd, cwd=cwd, check=not allow_fail)
|
||||||
|
return completed.returncode
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
print(f"[FAIL] {' '.join(cmd)} -> {exc.returncode}")
|
||||||
|
if not allow_fail:
|
||||||
|
raise
|
||||||
|
return exc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_python() -> None:
|
||||||
|
current = sys.version_info[:3]
|
||||||
|
if current < MIN_PYTHON:
|
||||||
|
req = ".".join(map(str, MIN_PYTHON))
|
||||||
|
got = ".".join(map(str, current))
|
||||||
|
raise SystemExit(
|
||||||
|
f"Python {req}+ is required. Current interpreter: {got}. "
|
||||||
|
f"Run with /opt/homebrew/bin/python3.12 bootstrap_project.py --with-venv --with-install"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_dirs() -> None:
|
||||||
|
for rel in [
|
||||||
|
"app/src/bootstrap",
|
||||||
|
"app/src/core",
|
||||||
|
"app/src/telegram/handlers",
|
||||||
|
"app/src/telegram/keyboards",
|
||||||
|
"app/src/telegram/callbacks",
|
||||||
|
"app/src/trading",
|
||||||
|
"app/src/storage",
|
||||||
|
"app/src/integrations",
|
||||||
|
"app/src/shared",
|
||||||
|
"app/tests",
|
||||||
|
"docs/architecture",
|
||||||
|
"docs/stages",
|
||||||
|
"docs/decisions",
|
||||||
|
"infra/docker",
|
||||||
|
"infra/compose",
|
||||||
|
".vscode",
|
||||||
|
]:
|
||||||
|
path = ROOT / rel
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
print(f"[DIR ] {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_init_files() -> None:
|
||||||
|
for rel in [
|
||||||
|
"app/src/__init__.py",
|
||||||
|
"app/src/bootstrap/__init__.py",
|
||||||
|
"app/src/core/__init__.py",
|
||||||
|
"app/src/telegram/__init__.py",
|
||||||
|
"app/src/telegram/handlers/__init__.py",
|
||||||
|
"app/src/telegram/keyboards/__init__.py",
|
||||||
|
"app/src/telegram/callbacks/__init__.py",
|
||||||
|
"app/src/trading/__init__.py",
|
||||||
|
"app/src/storage/__init__.py",
|
||||||
|
"app/src/integrations/__init__.py",
|
||||||
|
"app/src/shared/__init__.py",
|
||||||
|
"app/tests/__init__.py",
|
||||||
|
]:
|
||||||
|
touch(ROOT / rel)
|
||||||
|
|
||||||
|
|
||||||
|
def create_root_files() -> None:
|
||||||
|
write(ROOT / ".gitignore", """
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.DS_Store
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
app/.env
|
||||||
|
app/.venv/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
logs/
|
||||||
|
data/
|
||||||
|
""")
|
||||||
|
write(ROOT / ".vscode" / "settings.json", """
|
||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "app/.venv/bin/python"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
write(ROOT / "README.md", """
|
||||||
|
# dzentra_bot
|
||||||
|
|
||||||
|
Telegram-бот для автоторговли криптовалютой.
|
||||||
|
|
||||||
|
## Bootstrap v2
|
||||||
|
Это стабильный стартовый каркас проекта с:
|
||||||
|
- чистой структурой каталогов
|
||||||
|
- компактным верхним меню
|
||||||
|
- базовыми handlers
|
||||||
|
- документацией
|
||||||
|
- Docker-файлами
|
||||||
|
- опциональным созданием `.venv`, установкой зависимостей и первым git commit
|
||||||
|
|
||||||
|
## Верхнее меню
|
||||||
|
- 🏠 Главная
|
||||||
|
- 📈 Рынок
|
||||||
|
- 💼 Портфель
|
||||||
|
- ⚡ Торговля
|
||||||
|
- 🤖 Авто
|
||||||
|
- 📒 Журнал
|
||||||
|
- ⚙️ Система
|
||||||
|
|
||||||
|
## Рекомендуемый запуск
|
||||||
|
```bash
|
||||||
|
/opt/homebrew/bin/python3.12 bootstrap_project.py --with-venv --with-install
|
||||||
|
```
|
||||||
|
|
||||||
|
## После bootstrap
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m src.main
|
||||||
|
```
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_app_files() -> None:
|
||||||
|
write(ROOT / "app" / ".env.example", """
|
||||||
|
BOT_TOKEN=PUT_YOUR_TELEGRAM_BOT_TOKEN_HERE
|
||||||
|
BOT_PARSE_MODE=HTML
|
||||||
|
APP_ENV=dev
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
TZ=Europe/Madrid
|
||||||
|
""")
|
||||||
|
write(ROOT / "app" / "requirements.txt", """
|
||||||
|
aiogram==3.13.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
""")
|
||||||
|
write(ROOT / "app" / "README.md", """
|
||||||
|
# app
|
||||||
|
|
||||||
|
Здесь находятся исходный код приложения, env-файлы и зависимости.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_core_files() -> None:
|
||||||
|
write(ROOT / "app/src/core/config.py", """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
ENV_FILE = BASE_DIR / ".env"
|
||||||
|
load_dotenv(ENV_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Settings:
|
||||||
|
bot_token: str
|
||||||
|
bot_parse_mode: str
|
||||||
|
app_env: str
|
||||||
|
log_level: str
|
||||||
|
tz: str
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings() -> Settings:
|
||||||
|
bot_token = os.getenv("BOT_TOKEN", "").strip()
|
||||||
|
if not bot_token:
|
||||||
|
raise RuntimeError("BOT_TOKEN is not set in app/.env")
|
||||||
|
|
||||||
|
return Settings(
|
||||||
|
bot_token=bot_token,
|
||||||
|
bot_parse_mode=os.getenv("BOT_PARSE_MODE", "HTML").strip() or "HTML",
|
||||||
|
app_env=os.getenv("APP_ENV", "dev").strip() or "dev",
|
||||||
|
log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO",
|
||||||
|
tz=os.getenv("TZ", "Europe/Madrid").strip() or "Europe/Madrid",
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/core/constants.py", """
|
||||||
|
APP_NAME = "Dzentra Bot"
|
||||||
|
APP_VERSION = "2.0.0"
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/core/exceptions.py", """
|
||||||
|
class AppError(Exception):
|
||||||
|
\"\"\"Base application exception.\"\"\"
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_bootstrap_files() -> None:
|
||||||
|
write(ROOT / "app/src/bootstrap/logging.py", """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(log_level: str) -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, log_level.upper(), logging.INFO),
|
||||||
|
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/bootstrap/app_factory.py", """
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
|
from src.bootstrap.logging import setup_logging
|
||||||
|
from src.core.config import load_settings
|
||||||
|
from src.telegram.routers import setup_routers
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> tuple[Bot, Dispatcher]:
|
||||||
|
settings = load_settings()
|
||||||
|
setup_logging(settings.log_level)
|
||||||
|
|
||||||
|
bot = Bot(
|
||||||
|
token=settings.bot_token,
|
||||||
|
default=DefaultBotProperties(parse_mode=settings.bot_parse_mode),
|
||||||
|
)
|
||||||
|
dispatcher = Dispatcher()
|
||||||
|
setup_routers(dispatcher)
|
||||||
|
|
||||||
|
return bot, dispatcher
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_telegram_files() -> None:
|
||||||
|
write(ROOT / "app/src/telegram/menus.py", """
|
||||||
|
MAIN_MENU_TEXT = (
|
||||||
|
"<b>Dzentra Bot</b>\\n\\n"
|
||||||
|
"Новый каркас проекта успешно создан.\\n\\n"
|
||||||
|
"Выбери раздел через меню ниже."
|
||||||
|
)
|
||||||
|
|
||||||
|
HOME_TEXT = (
|
||||||
|
"<b>🏠 Главная</b>\\n\\n"
|
||||||
|
"Это главный экран бота.\\n\\n"
|
||||||
|
"Сейчас здесь отображается базовый статус:\\n"
|
||||||
|
"- бот запущен\\n"
|
||||||
|
"- меню подключено\\n"
|
||||||
|
"- handlers работают\\n"
|
||||||
|
"- проект на этапе Bootstrap v2\\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
SYSTEM_TEXT = (
|
||||||
|
"<b>⚙️ Система</b>\\n\\n"
|
||||||
|
"Системный экран.\\n\\n"
|
||||||
|
"<b>Справка</b>\\n"
|
||||||
|
"/start — запуск\\n"
|
||||||
|
"/menu — показать меню\\n"
|
||||||
|
"/help — краткая справка\\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
MARKET_TEXT = "<b>📈 Рынок</b>\\n\\nРаздел пока в разработке."
|
||||||
|
PORTFOLIO_TEXT = "<b>💼 Портфель</b>\\n\\nРаздел пока в разработке."
|
||||||
|
TRADE_TEXT = "<b>⚡ Торговля</b>\\n\\nРаздел пока в разработке."
|
||||||
|
AUTO_TEXT = "<b>🤖 Авто</b>\\n\\nРаздел пока в разработке."
|
||||||
|
JOURNAL_TEXT = "<b>📒 Журнал</b>\\n\\nРаздел пока в разработке."
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/keyboards/reply.py", """
|
||||||
|
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
|
||||||
|
|
||||||
|
|
||||||
|
def build_main_menu_keyboard() -> ReplyKeyboardMarkup:
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton(text="🏠 Главная"),
|
||||||
|
KeyboardButton(text="📈 Рынок"),
|
||||||
|
KeyboardButton(text="💼 Портфель"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="⚡ Торговля"),
|
||||||
|
KeyboardButton(text="🤖 Авто"),
|
||||||
|
KeyboardButton(text="📒 Журнал"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="⚙️ Система"),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
resize_keyboard=True,
|
||||||
|
input_field_placeholder="Выбери раздел...",
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/routers.py", """
|
||||||
|
from aiogram import Dispatcher
|
||||||
|
|
||||||
|
from src.telegram.handlers.auto import router as auto_router
|
||||||
|
from src.telegram.handlers.home import router as home_router
|
||||||
|
from src.telegram.handlers.journal import router as journal_router
|
||||||
|
from src.telegram.handlers.market import router as market_router
|
||||||
|
from src.telegram.handlers.portfolio import router as portfolio_router
|
||||||
|
from src.telegram.handlers.start import router as start_router
|
||||||
|
from src.telegram.handlers.system import router as system_router
|
||||||
|
from src.telegram.handlers.trade import router as trade_router
|
||||||
|
|
||||||
|
|
||||||
|
def setup_routers(dispatcher: Dispatcher) -> None:
|
||||||
|
dispatcher.include_router(start_router)
|
||||||
|
dispatcher.include_router(home_router)
|
||||||
|
dispatcher.include_router(market_router)
|
||||||
|
dispatcher.include_router(portfolio_router)
|
||||||
|
dispatcher.include_router(trade_router)
|
||||||
|
dispatcher.include_router(auto_router)
|
||||||
|
dispatcher.include_router(journal_router)
|
||||||
|
dispatcher.include_router(system_router)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/start.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.keyboards.reply import build_main_menu_keyboard
|
||||||
|
from src.telegram.menus import MAIN_MENU_TEXT, SYSTEM_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="start")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("start"))
|
||||||
|
async def cmd_start(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("menu"))
|
||||||
|
async def cmd_menu(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("help"))
|
||||||
|
async def cmd_help(message: Message) -> None:
|
||||||
|
await message.answer(SYSTEM_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "Меню")
|
||||||
|
async def menu_shortcut(message: Message) -> None:
|
||||||
|
await message.answer(MAIN_MENU_TEXT, reply_markup=build_main_menu_keyboard())
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/home.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import HOME_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="home")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "🏠 Главная")
|
||||||
|
async def open_home(message: Message) -> None:
|
||||||
|
await message.answer(HOME_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/market.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import MARKET_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="market")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📈 Рынок")
|
||||||
|
async def open_market(message: Message) -> None:
|
||||||
|
await message.answer(MARKET_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/portfolio.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import PORTFOLIO_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="portfolio")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "💼 Портфель")
|
||||||
|
async def open_portfolio(message: Message) -> None:
|
||||||
|
await message.answer(PORTFOLIO_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/trade.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import TRADE_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="trade")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "⚡ Торговля")
|
||||||
|
async def open_trade(message: Message) -> None:
|
||||||
|
await message.answer(TRADE_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/auto.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import AUTO_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="auto")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "🤖 Авто")
|
||||||
|
async def open_auto(message: Message) -> None:
|
||||||
|
await message.answer(AUTO_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/journal.py", """
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.menus import JOURNAL_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="journal")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📒 Журнал")
|
||||||
|
async def open_journal(message: Message) -> None:
|
||||||
|
await message.answer(JOURNAL_TEXT)
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/telegram/handlers/system.py", """
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.core.constants import APP_NAME, APP_VERSION
|
||||||
|
from src.telegram.menus import SYSTEM_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
router = Router(name="system")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text.in_({"⚙️ Система", "⚙ Система"}))
|
||||||
|
async def open_system(message: Message) -> None:
|
||||||
|
runtime_info = (
|
||||||
|
"\\n\\n<b>Runtime</b>\\n"
|
||||||
|
f"- app: {APP_NAME} {APP_VERSION}\\n"
|
||||||
|
f"- python: {platform.python_version()}\\n"
|
||||||
|
f"- os: {platform.system()} {platform.release()}"
|
||||||
|
)
|
||||||
|
await message.answer(SYSTEM_TEXT + runtime_info)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_misc_files() -> None:
|
||||||
|
write(ROOT / "app/src/trading/README.md", """
|
||||||
|
Здесь будет торговая бизнес-логика.
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/storage/README.md", """
|
||||||
|
Здесь будет слой доступа к данным.
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/integrations/README.md", """
|
||||||
|
Здесь будут внешние интеграции, например биржа.
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/shared/README.md", """
|
||||||
|
Здесь будут общие утилиты.
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/tests/test_smoke.py", """
|
||||||
|
def test_smoke() -> None:
|
||||||
|
assert True
|
||||||
|
""")
|
||||||
|
write(ROOT / "app/src/main.py", """
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.bootstrap.app_factory import create_app
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
bot, dispatcher = create_app()
|
||||||
|
await dispatcher.start_polling(bot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
""")
|
||||||
|
write(ROOT / "infra/docker/Dockerfile", """
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /project
|
||||||
|
|
||||||
|
COPY app /project/app
|
||||||
|
RUN pip install --no-cache-dir -r /project/app/requirements.txt
|
||||||
|
|
||||||
|
WORKDIR /project/app
|
||||||
|
CMD ["python", "-m", "src.main"]
|
||||||
|
""")
|
||||||
|
write(ROOT / "infra/compose/docker-compose.yml", """
|
||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: infra/docker/Dockerfile
|
||||||
|
container_name: dzentra_bot
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ../../app/.env
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def create_docs() -> None:
|
||||||
|
write(ROOT / "docs/changelog.md", """
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.0
|
||||||
|
- создан стабильный bootstrap v2
|
||||||
|
- добавлена чистая структура проекта
|
||||||
|
- добавлено компактное верхнее меню 3 / 3 / 1
|
||||||
|
- добавлены базовые handlers
|
||||||
|
- help перенесен в раздел `Система`
|
||||||
|
- добавлены Docker-файлы
|
||||||
|
- добавлена стартовая документация
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/architecture/overview.md", """
|
||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
Проект строится как modular monolith с разделением по слоям:
|
||||||
|
|
||||||
|
- `telegram` — меню, handlers, routers
|
||||||
|
- `bootstrap` — сборка приложения
|
||||||
|
- `core` — конфигурация и базовые сущности
|
||||||
|
- `trading` — бизнес-логика торговли
|
||||||
|
- `storage` — доступ к данным
|
||||||
|
- `integrations` — внешние API
|
||||||
|
- `shared` — общие утилиты
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/architecture/project_structure.md", """
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
## Корневые папки
|
||||||
|
- `app/` — код приложения
|
||||||
|
- `docs/` — документация
|
||||||
|
- `infra/` — Docker и compose
|
||||||
|
|
||||||
|
## Внутри `app/src`
|
||||||
|
- `bootstrap/`
|
||||||
|
- `core/`
|
||||||
|
- `telegram/`
|
||||||
|
- `trading/`
|
||||||
|
- `storage/`
|
||||||
|
- `integrations/`
|
||||||
|
- `shared/`
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/architecture/telegram_menu.md", """
|
||||||
|
# Telegram Menu
|
||||||
|
|
||||||
|
Верхнее меню bootstrap v2:
|
||||||
|
|
||||||
|
1. `🏠 Главная` `📈 Рынок` `💼 Портфель`
|
||||||
|
2. `⚡ Торговля` `🤖 Авто` `📒 Журнал`
|
||||||
|
3. `⚙️ Система`
|
||||||
|
|
||||||
|
На старте реально работают:
|
||||||
|
- `/start`
|
||||||
|
- `/menu`
|
||||||
|
- `/help`
|
||||||
|
- все кнопки верхнего меню
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/stages/stage-01-bootstrap.md", """
|
||||||
|
# Stage 01 — Bootstrap
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Получить стабильный стартовый каркас проекта без архитектурной каши.
|
||||||
|
|
||||||
|
## Что есть
|
||||||
|
- структура каталогов
|
||||||
|
- env
|
||||||
|
- зависимости
|
||||||
|
- базовый aiogram-бот
|
||||||
|
- reply-меню
|
||||||
|
- handlers
|
||||||
|
- docs
|
||||||
|
- Docker
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/decisions/0001-project-structure.md", """
|
||||||
|
# 0001 — Project Structure
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
использовать modular monolith с разбиением на `core`, `bootstrap`, `telegram`,
|
||||||
|
`trading`, `storage`, `integrations`, `shared`.
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/decisions/0002-menu-design.md", """
|
||||||
|
# 0002 — Menu Design
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
использовать компактное верхнее меню 3 / 3 / 1:
|
||||||
|
- Главная / Рынок / Портфель
|
||||||
|
- Торговля / Авто / Журнал
|
||||||
|
- Система
|
||||||
|
|
||||||
|
Справка перенесена в раздел `Система`.
|
||||||
|
""")
|
||||||
|
write(ROOT / "docs/decisions/0003-python-workflow.md", """
|
||||||
|
# 0003 — Python Workflow
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
- использовать Python 3.12
|
||||||
|
- создавать `.venv` внутри `app/`
|
||||||
|
- не использовать conda/base для этого проекта
|
||||||
|
- в VS Code выбирать `app/.venv/bin/python`
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_create_env() -> None:
|
||||||
|
env = ROOT / "app/.env"
|
||||||
|
example = ROOT / "app/.env.example"
|
||||||
|
if not env.exists():
|
||||||
|
env.write_text(example.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
print("[WRITE] app/.env created from .env.example")
|
||||||
|
else:
|
||||||
|
print("[SKIP] app/.env already exists")
|
||||||
|
|
||||||
|
|
||||||
|
def create_venv_and_install(with_venv: bool, with_install: bool) -> None:
|
||||||
|
app_dir = ROOT / "app"
|
||||||
|
venv_dir = app_dir / ".venv"
|
||||||
|
|
||||||
|
if with_venv and not venv_dir.exists():
|
||||||
|
run([sys.executable, "-m", "venv", str(venv_dir)])
|
||||||
|
|
||||||
|
if with_install:
|
||||||
|
if not venv_dir.exists():
|
||||||
|
raise RuntimeError("app/.venv does not exist. Use --with-venv together with --with-install.")
|
||||||
|
pip_path = venv_dir / "bin" / "pip"
|
||||||
|
run([str(pip_path), "install", "--upgrade", "pip"], cwd=app_dir)
|
||||||
|
run([str(pip_path), "install", "-r", "requirements.txt"], cwd=app_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_git(with_git: bool) -> None:
|
||||||
|
if not with_git:
|
||||||
|
return
|
||||||
|
if not (ROOT / ".git").exists():
|
||||||
|
run(["git", "init"], cwd=ROOT)
|
||||||
|
run(["git", "add", "."], cwd=ROOT)
|
||||||
|
status = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=ROOT, check=False)
|
||||||
|
if status.returncode != 0:
|
||||||
|
run(["git", "commit", "-m", "bootstrap v2"], cwd=ROOT, allow_fail=True)
|
||||||
|
else:
|
||||||
|
print("[INFO] Nothing new to commit.")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Stable bootstrap v2 for dzentra_bot.")
|
||||||
|
parser.add_argument("--with-venv", action="store_true", help="Create app/.venv")
|
||||||
|
parser.add_argument("--with-install", action="store_true", help="Install requirements into app/.venv")
|
||||||
|
parser.add_argument("--with-git", action="store_true", help="Initialize git and create first commit")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ensure_python()
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.with_install and not args.with_venv:
|
||||||
|
raise SystemExit("--with-install requires --with-venv")
|
||||||
|
|
||||||
|
print("\n=== dzentra_bot bootstrap v2 ===\n")
|
||||||
|
print(f"[INFO] Interpreter: {sys.executable}")
|
||||||
|
print(f"[INFO] Python: {sys.version.split()[0]}")
|
||||||
|
|
||||||
|
create_dirs()
|
||||||
|
create_init_files()
|
||||||
|
create_root_files()
|
||||||
|
create_app_files()
|
||||||
|
create_core_files()
|
||||||
|
create_bootstrap_files()
|
||||||
|
create_telegram_files()
|
||||||
|
create_misc_files()
|
||||||
|
create_docs()
|
||||||
|
maybe_create_env()
|
||||||
|
create_venv_and_install(args.with_venv, args.with_install)
|
||||||
|
setup_git(args.with_git)
|
||||||
|
|
||||||
|
print("\nBootstrap v2 completed.\n")
|
||||||
|
print("Next steps:")
|
||||||
|
print("1. Open app/.env and fill BOT_TOKEN")
|
||||||
|
print("2. cd app")
|
||||||
|
if args.with_venv:
|
||||||
|
print("3. source .venv/bin/activate")
|
||||||
|
else:
|
||||||
|
print("3. Create venv later with --with-venv --with-install")
|
||||||
|
print("4. python -m src.main")
|
||||||
|
print("5. In VS Code use interpreter: app/.venv/bin/python")
|
||||||
|
print("6. For Synology: sudo docker compose -f infra/compose/docker-compose.yml up --build -d")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
11
docs/architecture/overview.md
Normal file
11
docs/architecture/overview.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
Проект строится как modular monolith с разделением по слоям:
|
||||||
|
|
||||||
|
- `telegram` — меню, handlers, routers
|
||||||
|
- `bootstrap` — сборка приложения
|
||||||
|
- `core` — конфигурация и базовые сущности
|
||||||
|
- `trading` — бизнес-логика торговли
|
||||||
|
- `storage` — доступ к данным
|
||||||
|
- `integrations` — внешние API
|
||||||
|
- `shared` — общие утилиты
|
||||||
15
docs/architecture/project_structure.md
Normal file
15
docs/architecture/project_structure.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Project Structure
|
||||||
|
|
||||||
|
## Корневые папки
|
||||||
|
- `app/` — код приложения
|
||||||
|
- `docs/` — документация
|
||||||
|
- `infra/` — Docker и compose
|
||||||
|
|
||||||
|
## Внутри `app/src`
|
||||||
|
- `bootstrap/`
|
||||||
|
- `core/`
|
||||||
|
- `telegram/`
|
||||||
|
- `trading/`
|
||||||
|
- `storage/`
|
||||||
|
- `integrations/`
|
||||||
|
- `shared/`
|
||||||
13
docs/architecture/telegram_menu.md
Normal file
13
docs/architecture/telegram_menu.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Telegram Menu
|
||||||
|
|
||||||
|
Верхнее меню bootstrap v2:
|
||||||
|
|
||||||
|
1. `🏠 Главная` `📈 Рынок` `💼 Портфель`
|
||||||
|
2. `⚡ Торговля` `🤖 Авто` `📒 Журнал`
|
||||||
|
3. `⚙️ Система`
|
||||||
|
|
||||||
|
На старте реально работают:
|
||||||
|
- `/start`
|
||||||
|
- `/menu`
|
||||||
|
- `/help`
|
||||||
|
- все кнопки верхнего меню
|
||||||
10
docs/changelog.md
Normal file
10
docs/changelog.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.0
|
||||||
|
- создан стабильный bootstrap v2
|
||||||
|
- добавлена чистая структура проекта
|
||||||
|
- добавлено компактное верхнее меню 3 / 3 / 1
|
||||||
|
- добавлены базовые handlers
|
||||||
|
- help перенесен в раздел `Система`
|
||||||
|
- добавлены Docker-файлы
|
||||||
|
- добавлена стартовая документация
|
||||||
5
docs/decisions/0001-project-structure.md
Normal file
5
docs/decisions/0001-project-structure.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 0001 — Project Structure
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
использовать modular monolith с разбиением на `core`, `bootstrap`, `telegram`,
|
||||||
|
`trading`, `storage`, `integrations`, `shared`.
|
||||||
9
docs/decisions/0002-menu-design.md
Normal file
9
docs/decisions/0002-menu-design.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 0002 — Menu Design
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
использовать компактное верхнее меню 3 / 3 / 1:
|
||||||
|
- Главная / Рынок / Портфель
|
||||||
|
- Торговля / Авто / Журнал
|
||||||
|
- Система
|
||||||
|
|
||||||
|
Справка перенесена в раздел `Система`.
|
||||||
7
docs/decisions/0003-python-workflow.md
Normal file
7
docs/decisions/0003-python-workflow.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 0003 — Python Workflow
|
||||||
|
|
||||||
|
Решение:
|
||||||
|
- использовать Python 3.12
|
||||||
|
- создавать `.venv` внутри `app/`
|
||||||
|
- не использовать conda/base для этого проекта
|
||||||
|
- в VS Code выбирать `app/.venv/bin/python`
|
||||||
14
docs/stages/stage-01-bootstrap.md
Normal file
14
docs/stages/stage-01-bootstrap.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Stage 01 — Bootstrap
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Получить стабильный стартовый каркас проекта без архитектурной каши.
|
||||||
|
|
||||||
|
## Что есть
|
||||||
|
- структура каталогов
|
||||||
|
- env
|
||||||
|
- зависимости
|
||||||
|
- базовый aiogram-бот
|
||||||
|
- reply-меню
|
||||||
|
- handlers
|
||||||
|
- docs
|
||||||
|
- Docker
|
||||||
9
infra/compose/docker-compose.yml
Normal file
9
infra/compose/docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: infra/docker/Dockerfile
|
||||||
|
container_name: dzentra_bot
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ../../app/.env
|
||||||
12
infra/docker/Dockerfile
Normal file
12
infra/docker/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /project
|
||||||
|
|
||||||
|
COPY app /project/app
|
||||||
|
RUN pip install --no-cache-dir -r /project/app/requirements.txt
|
||||||
|
|
||||||
|
WORKDIR /project/app
|
||||||
|
CMD ["python", "-m", "src.main"]
|
||||||
Reference in New Issue
Block a user