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