feat: initial project setup — Vision Studio bot
- Python 3.12 + discord.py - Docker + docker-compose - Forgejo Actions CI (lint + test) - Cog system with /hello example - deploy.sh for auto-deployment
This commit is contained in:
commit
e44349cea9
17 changed files with 396 additions and 0 deletions
16
.dockerignore
Normal file
16
.dockerignore
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.pytest_cache/
|
||||||
|
tests/
|
||||||
|
.mypy_cache/
|
||||||
|
nohup.out
|
||||||
|
*.log
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
6
.env.example
Normal file
6
.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
DISCORD_TOKEN=your_bot_token_here
|
||||||
|
APPLICATION_ID=your_application_id_here
|
||||||
|
DEV=1
|
||||||
|
FORGEJO_URL=https://your-forgejo-instance.com
|
||||||
|
FORGEJO_OWNER=your_forgejo_owner
|
||||||
|
FORGEJO_REPO=your_repo_name
|
||||||
46
.forgejo/workflows/ci.yml
Normal file
46
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
|
- name: Ruff lint
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Ruff format check
|
||||||
|
run: ruff format --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
|
- name: Syntax check
|
||||||
|
run: python -m py_compile bot.py && python -m py_compile vision.py && python -m py_compile config.py
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest tests/ -v || true
|
||||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# --- SÉCURITÉ ---
|
||||||
|
.env
|
||||||
|
|
||||||
|
# --- PYTHON ---
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# --- DONNÉES ---
|
||||||
|
data/*
|
||||||
|
!data/.gitkeep
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# --- LOGS ---
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# --- IDE ---
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# --- DOCKER ---
|
||||||
|
docker-compose.override.yml
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.12-slim AS base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
|
CMD python -c "import discord; print('ok')" || exit 1
|
||||||
|
|
||||||
|
CMD ["python", "vision.py"]
|
||||||
37
bot.py
Normal file
37
bot.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import logging
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
logger = logging.getLogger("vision")
|
||||||
|
|
||||||
|
|
||||||
|
class VisionBot(commands.Bot):
|
||||||
|
def __init__(self):
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
command_prefix="!",
|
||||||
|
intents=intents,
|
||||||
|
application_id=config.APPLICATION_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def setup_hook(self):
|
||||||
|
logger.info("Chargement des cogs...")
|
||||||
|
await self.load_extension("commandes.hello")
|
||||||
|
logger.info("Sync des slash commands...")
|
||||||
|
await self.tree.sync()
|
||||||
|
logger.info("Slash commands synchronisées")
|
||||||
|
|
||||||
|
async def on_ready(self):
|
||||||
|
logger.info(f"Connecté en tant que {self.user} (ID: {self.user.id})")
|
||||||
|
logger.info(f"Serveurs : {len(self.guilds)}")
|
||||||
|
await self.change_presence(
|
||||||
|
activity=discord.Activity(
|
||||||
|
type=discord.ActivityType.watching,
|
||||||
|
name="Vision Studio",
|
||||||
|
)
|
||||||
|
)
|
||||||
0
commandes/__init__.py
Normal file
0
commandes/__init__.py
Normal file
22
commandes/hello.py
Normal file
22
commandes/hello.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
class Hello(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@app_commands.command(name="hello", description="Salut de la part de Vision Studio !")
|
||||||
|
async def hello(self, interaction: discord.Interaction):
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Vision Studio",
|
||||||
|
description="Salut ! Je suis **Vision Studio**, ton bot Discord.\n\nUtilise `/help` pour voir les commandes disponibles.",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
embed.set_footer(text="Vision Studio v1.0")
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot):
|
||||||
|
await bot.add_cog(Hello(bot))
|
||||||
21
config.py
Normal file
21
config.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DATA_DIR = BASE_DIR / "data"
|
||||||
|
LOGS_DIR = BASE_DIR / "logs"
|
||||||
|
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
LOGS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||||
|
APPLICATION_ID = os.getenv("APPLICATION_ID", "")
|
||||||
|
DEV = os.getenv("DEV", "").strip() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
FORGEJO_URL = os.getenv("FORGEJO_URL", "")
|
||||||
|
FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN", "")
|
||||||
|
FORGEJO_OWNER = os.getenv("FORGEJO_OWNER", "")
|
||||||
|
FORGEJO_REPO = os.getenv("FORGEJO_REPO", "")
|
||||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
services:
|
||||||
|
vision:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: vision-studio
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
- VISION_INSTANCE=prod
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
volumes:
|
||||||
|
- vision-data:/app/data
|
||||||
|
- vision-logs:/app/logs
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "1.0"
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
cpus: "0.25"
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vision-data:
|
||||||
|
vision-logs:
|
||||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[project]
|
||||||
|
name = "vision-studio"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Vision Studio — Bot Discord"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I", "B", "SIM"]
|
||||||
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/**" = ["ALL"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
3
requirements-dev.txt
Normal file
3
requirements-dev.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ruff>=0.5.0
|
||||||
|
pytest>=8.0.0
|
||||||
|
pytest-asyncio>=0.24.0
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
discord.py>=2.4.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
Pillow>=10.0.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
95
scripts/deploy.sh
Executable file
95
scripts/deploy.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Vision Studio — Script de deploiement automatique
|
||||||
|
#
|
||||||
|
# Flow :
|
||||||
|
# 1. Lock anti-concurrence
|
||||||
|
# 2. git pull origin <branch> --rebase
|
||||||
|
# 3. Si requirements.txt change -> docker compose build
|
||||||
|
# 4. Kill process Python -> Docker restart (restart: unless-stopped)
|
||||||
|
#
|
||||||
|
# Usage: deploy.sh [branch]
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BRANCH="${1:-main}"
|
||||||
|
LOCK_FILE="/tmp/vision_deploy.lock"
|
||||||
|
DEPLOY_LOG="${ROOT_DIR}/data/deploy.log"
|
||||||
|
|
||||||
|
if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
|
set -a
|
||||||
|
. "$ROOT_DIR/.env"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPOSE_DIR="${ROOT_DIR}"
|
||||||
|
|
||||||
|
log() { echo "[deploy] $(date '+%F %T') $*"; }
|
||||||
|
|
||||||
|
acquire_lock() {
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
local lock_pid
|
||||||
|
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
|
||||||
|
if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
|
||||||
|
log "ERROR: Deploy déjà en cours (PID $lock_pid)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "WARN: Lock orphelin, nettoyage"
|
||||||
|
rm -f "$LOCK_FILE"
|
||||||
|
fi
|
||||||
|
echo $$ > "$LOCK_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
release_lock() { rm -f "$LOCK_FILE"; }
|
||||||
|
trap release_lock EXIT
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
DEPLOY_START=$(date +%s)
|
||||||
|
COMMIT_BEFORE=$(git log -1 --format="%h %s" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
log "Deploy démarré — branch=$BRANCH — commit=$COMMIT_BEFORE"
|
||||||
|
|
||||||
|
log "Git pull origin $BRANCH --rebase..."
|
||||||
|
if ! git pull origin "$BRANCH" --rebase 2>&1; then
|
||||||
|
log "ERROR: git pull failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMMIT_AFTER=$(git log -1 --format="%h %s")
|
||||||
|
CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
REBUILD_NEEDED=false
|
||||||
|
if echo "$CHANGED_FILES" | grep -q "requirements\.txt"; then
|
||||||
|
REBUILD_NEEDED=true
|
||||||
|
log "requirements.txt modifié → rebuild nécessaire"
|
||||||
|
elif echo "$CHANGED_FILES" | grep -q "Dockerfile"; then
|
||||||
|
REBUILD_NEEDED=true
|
||||||
|
log "Dockerfile modifié → rebuild nécessaire"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$REBUILD_NEEDED" = true ] && command -v docker &>/dev/null; then
|
||||||
|
cd "$COMPOSE_DIR"
|
||||||
|
docker compose build --no-cache vision 2>&1 | tail -10
|
||||||
|
BUILD_EXIT=${PIPESTATUS[0]:-$?}
|
||||||
|
if [ "$BUILD_EXIT" -ne 0 ]; then
|
||||||
|
log "ERROR: Docker build échoué (exit $BUILD_EXIT)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker compose up -d vision 2>&1 | tail -5
|
||||||
|
DEPLOY_END=$(date +%s)
|
||||||
|
DURATION=$((DEPLOY_END - DEPLOY_START))
|
||||||
|
log "Deploy réussi en ${DURATION}s (deps rebuild)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_END=$(date +%s)
|
||||||
|
DURATION=$((DEPLOY_END - DEPLOY_START))
|
||||||
|
log "Redémarrage du bot..."
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$DEPLOY_LOG")"
|
||||||
|
echo "$(date '+%F %T') | $BRANCH | $COMMIT_AFTER | ${DURATION}s | auto" >> "$DEPLOY_LOG"
|
||||||
|
|
||||||
|
pkill -f "python.*vision\.py" 2>/dev/null || kill 1 2>/dev/null || true
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
41
vision.py
Normal file
41
vision.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
import config
|
||||||
|
from bot import VisionBot
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s | %(levelname)-8s | %(name)s — %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
logging.FileHandler(config.LOGS_DIR / "vision.log", encoding="utf-8"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("vision")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
if not config.DISCORD_TOKEN:
|
||||||
|
logger.critical("DISCORD_TOKEN manquant dans .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
logger.info("Lancement de Vision Studio...")
|
||||||
|
bot = VisionBot()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.start(config.DISCORD_TOKEN)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Arrêt demandé")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(f"Erreur fatale : {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue