728 lines
22 KiB
Python
728 lines
22 KiB
Python
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()
|