bootstrap v2 stable start

This commit is contained in:
2026-04-13 20:47:04 +03:00
commit 7565aa485e
51 changed files with 1190 additions and 0 deletions

14
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,4 @@
{
"python.defaultInterpreterPath": "app/.venv/bin/python",
"python-envs.defaultEnvManager": "ms-python.python:system"
}

33
README.md Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
# app
Здесь находятся исходный код приложения, env-файлы и зависимости.

2
app/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
aiogram==3.13.1
python-dotenv==1.0.1

1
app/src/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Package marker."""

View File

@@ -0,0 +1 @@
"""Package marker."""

View 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

View 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
View File

@@ -0,0 +1 @@
"""Package marker."""

35
app/src/core/config.py Normal file
View 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",
)

View File

@@ -0,0 +1,2 @@
APP_NAME = "Dzentra Bot"
APP_VERSION = "2.0.0"

View File

@@ -0,0 +1,2 @@
class AppError(Exception):
"""Base application exception."""

View File

@@ -0,0 +1 @@
Здесь будут внешние интеграции, например биржа.

View File

@@ -0,0 +1 @@
"""Package marker."""

12
app/src/main.py Normal file
View 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
View File

@@ -0,0 +1 @@
Здесь будут общие утилиты.

View File

@@ -0,0 +1 @@
"""Package marker."""

View File

@@ -0,0 +1 @@
Здесь будет слой доступа к данным.

View File

@@ -0,0 +1 @@
"""Package marker."""

View File

@@ -0,0 +1 @@
"""Package marker."""

View File

@@ -0,0 +1 @@
"""Package marker."""

View File

@@ -0,0 +1 @@
"""Package marker."""

View 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)

View 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)

View 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)

View 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)

View 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)

View 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())

View 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)

View 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)

View File

@@ -0,0 +1 @@
"""Package marker."""

View 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
View 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Раздел пока в разработке."

View 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)

View File

@@ -0,0 +1 @@
Здесь будет торговая бизнес-логика.

View File

@@ -0,0 +1 @@
"""Package marker."""

1
app/tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Package marker."""

2
app/tests/test_smoke.py Normal file
View File

@@ -0,0 +1,2 @@
def test_smoke() -> None:
assert True

727
bootstrap_project.py Normal file
View 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()

View File

@@ -0,0 +1,11 @@
# Architecture Overview
Проект строится как modular monolith с разделением по слоям:
- `telegram` — меню, handlers, routers
- `bootstrap` — сборка приложения
- `core` — конфигурация и базовые сущности
- `trading` — бизнес-логика торговли
- `storage` — доступ к данным
- `integrations` — внешние API
- `shared` — общие утилиты

View File

@@ -0,0 +1,15 @@
# Project Structure
## Корневые папки
- `app/` — код приложения
- `docs/` — документация
- `infra/` — Docker и compose
## Внутри `app/src`
- `bootstrap/`
- `core/`
- `telegram/`
- `trading/`
- `storage/`
- `integrations/`
- `shared/`

View File

@@ -0,0 +1,13 @@
# Telegram Menu
Верхнее меню bootstrap v2:
1. `🏠 Главная` `📈 Рынок` `💼 Портфель`
2. `⚡ Торговля` `🤖 Авто` `📒 Журнал`
3. `⚙️ Система`
На старте реально работают:
- `/start`
- `/menu`
- `/help`
- все кнопки верхнего меню

10
docs/changelog.md Normal file
View File

@@ -0,0 +1,10 @@
# Changelog
## v2.0.0
- создан стабильный bootstrap v2
- добавлена чистая структура проекта
- добавлено компактное верхнее меню 3 / 3 / 1
- добавлены базовые handlers
- help перенесен в раздел `Система`
- добавлены Docker-файлы
- добавлена стартовая документация

View File

@@ -0,0 +1,5 @@
# 0001 — Project Structure
Решение:
использовать modular monolith с разбиением на `core`, `bootstrap`, `telegram`,
`trading`, `storage`, `integrations`, `shared`.

View File

@@ -0,0 +1,9 @@
# 0002 — Menu Design
Решение:
использовать компактное верхнее меню 3 / 3 / 1:
- Главная / Рынок / Портфель
- Торговля / Авто / Журнал
- Система
Справка перенесена в раздел `Система`.

View File

@@ -0,0 +1,7 @@
# 0003 — Python Workflow
Решение:
- использовать Python 3.12
- создавать `.venv` внутри `app/`
- не использовать conda/base для этого проекта
- в VS Code выбирать `app/.venv/bin/python`

View File

@@ -0,0 +1,14 @@
# Stage 01 — Bootstrap
## Цель
Получить стабильный стартовый каркас проекта без архитектурной каши.
## Что есть
- структура каталогов
- env
- зависимости
- базовый aiogram-бот
- reply-меню
- handlers
- docs
- Docker

View 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
View 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"]