Compare commits

..

3 Commits

Author SHA1 Message Date
herel edefce9703 chore: use cache for pip 2025-10-07 08:09:42 +02:00
herel eb6da3d9cf feat: multi-stage build for slim image 2025-10-07 07:58:08 +02:00
herel bc55b95bda feat: permit to generate only multiplications 2025-10-07 07:55:18 +02:00
3 changed files with 68 additions and 42 deletions
+19 -23
View File
@@ -1,28 +1,25 @@
# Use Python 3.13 slim image as base # Use Python 3.13 slim image as base
FROM python:3.13-slim FROM python:3.13-slim AS builder
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Set environment variables # Create and set pip cache directory
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PIP_CACHE_DIR=/root/.cache/pip
PYTHONUNBUFFERED=1 \
POETRY_NO_INTERACTION=1 \
POETRY_VENV_IN_PROJECT=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements file # Copy requirements file
COPY requirements.txt . COPY requirements.txt .
# Install Python dependencies # Install Python dependencies with bind-mounted cache
RUN pip install --no-cache-dir --upgrade pip && \ RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt pip install --no-deps \
--disable-pip-version-check \
--target=/app/site-packages \
-r requirements.txt
FROM python:3.13-slim
COPY --from=builder /app/site-packages /app/site-packages
# Create a non-root user # Create a non-root user
RUN adduser --disabled-password --gecos '' appuser RUN adduser --disabled-password --gecos '' appuser
@@ -31,8 +28,11 @@ RUN adduser --disabled-password --gecos '' appuser
COPY ./app /app/app COPY ./app /app/app
COPY ./generate_math_exercises.py /app/ COPY ./generate_math_exercises.py /app/
# Change ownership of the app directory to the non-root user ENV PYTHONPATH=/app/site-packages
RUN chown -R appuser:appuser /app ENV PATH=/app/site-packages/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Set working directory
WORKDIR /app
# Switch to non-root user # Switch to non-root user
USER appuser USER appuser
@@ -40,9 +40,5 @@ USER appuser
# Expose port # Expose port
EXPOSE 8000 EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run the application # Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+39 -17
View File
@@ -167,6 +167,7 @@ class ExerciseRequest(BaseModel):
min_table: int min_table: int
max_table: int max_table: int
num_exercises: int = 15 num_exercises: int = 15
multiplication_only: bool = False
class OperationExerciseRequest(BaseModel): class OperationExerciseRequest(BaseModel):
@@ -174,7 +175,7 @@ class OperationExerciseRequest(BaseModel):
def generate_exercises( def generate_exercises(
min_table: int, max_table: int, num_exercises: int = 15 min_table: int, max_table: int, num_exercises: int = 15, multiplication_only: bool = False
) -> List[str]: ) -> List[str]:
"""Génère des exercices de multiplication et division aléatoires mélangés sans doublons""" """Génère des exercices de multiplication et division aléatoires mélangés sans doublons"""
exercises: List[str] = [] exercises: List[str] = []
@@ -192,8 +193,12 @@ def generate_exercises(
b = random.randint(min_table, max_table) b = random.randint(min_table, max_table)
result = a * b result = a * b
# Choisir aléatoirement le type d'exercice (seulement multiplication ou division) # Déterminer le type d'exercice
exercise_type = random.choice(["multiplication", "division"]) if multiplication_only:
exercise_type = "multiplication"
else:
# Choisir aléatoirement le type d'exercice (seulement multiplication ou division)
exercise_type = random.choice(["multiplication", "division"])
if exercise_type == "multiplication": if exercise_type == "multiplication":
# Exercice de multiplication # Exercice de multiplication
@@ -202,7 +207,7 @@ def generate_exercises(
exercise = f"{a} · {b} = ____" exercise = f"{a} · {b} = ____"
exercises.append(exercise) exercises.append(exercise)
used_operations.add(operation_key) used_operations.add(operation_key)
else: # division elif not multiplication_only: # division
# Exercice de division # Exercice de division
divisor = random.choice([a, b]) divisor = random.choice([a, b])
operation_key = f"div_{result}_{divisor}" # Clé unique pour cette opération operation_key = f"div_{result}_{divisor}" # Clé unique pour cette opération
@@ -221,14 +226,21 @@ def generate_exercises(
b = random.randint(min_table, max_table) b = random.randint(min_table, max_table)
result = a * b result = a * b
# Choisir aléatoirement le type d'exercice # Déterminer le type d'exercice
exercise_type = random.choice(["multiplication", "division"]) if multiplication_only:
exercise_type = "multiplication"
else:
# Choisir aléatoirement le type d'exercice
exercise_type = random.choice(["multiplication", "division"])
if exercise_type == "multiplication": if exercise_type == "multiplication":
exercise = f"{a} · {b} = ____" exercise = f"{a} · {b} = ____"
else: # division elif not multiplication_only: # division
divisor = random.choice([a, b]) divisor = random.choice([a, b])
exercise = f"{result} : {divisor} = ____" exercise = f"{result} : {divisor} = ____"
else:
# Fallback to multiplication if multiplication_only is True
exercise = f"{a} · {b} = ____"
exercises.append(exercise) exercises.append(exercise)
@@ -236,7 +248,7 @@ def generate_exercises(
def create_math_exercises_pdf( def create_math_exercises_pdf(
min_table: int, max_table: int, num_exercises: int = 15 min_table: int, max_table: int, num_exercises: int = 15, multiplication_only: bool = False
) -> str: ) -> str:
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes et l'upload sur S3""" """Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes et l'upload sur S3"""
import datetime import datetime
@@ -245,14 +257,24 @@ def create_math_exercises_pdf(
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Ajouter des informations sur la plage de tables # Ajouter des informations sur la plage de tables
if min_table == max_table: if multiplication_only:
table_info = f"Tables de multiplication et division pour {min_table}" if min_table == max_table:
pdf_filename = f"exercices_mathematiques_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf" table_info = f"Tables de multiplication seulement pour {min_table}"
pdf_filename = f"exercices_multiplication_seulement_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf"
else:
table_info = (
f"Tables de multiplication seulement de {min_table} à {max_table}"
)
pdf_filename = f"exercices_multiplication_seulement_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf"
else: else:
table_info = ( if min_table == max_table:
f"Tables de multiplication et division de {min_table} à {max_table}" table_info = f"Tables de multiplication et division pour {min_table}"
) pdf_filename = f"exercices_mathematiques_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf"
pdf_filename = f"exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf" else:
table_info = (
f"Tables de multiplication et division de {min_table} à {max_table}"
)
pdf_filename = f"exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf"
pdf = MathExercisesPDF() pdf = MathExercisesPDF()
pdf.add_page() pdf.add_page()
@@ -262,7 +284,7 @@ def create_math_exercises_pdf(
pdf.ln(5) pdf.ln(5)
# Générer les exercices # Générer les exercices
exercises = generate_exercises(min_table, max_table, num_exercises) exercises = generate_exercises(min_table, max_table, num_exercises, multiplication_only)
# Pas d'en-têtes de colonnes # Pas d'en-têtes de colonnes
@@ -354,7 +376,7 @@ async def generate_exercises_endpoint(request: ExerciseRequest):
return {"error": "Le nombre d'exercices doit être supérieur à 0"} return {"error": "Le nombre d'exercices doit être supérieur à 0"}
pdf_filename = create_math_exercises_pdf( pdf_filename = create_math_exercises_pdf(
request.min_table, request.max_table, request.num_exercises request.min_table, request.max_table, request.num_exercises, request.multiplication_only
) )
# Return redirect to automatically download the file # Return redirect to automatically download the file
+10 -2
View File
@@ -61,6 +61,11 @@
<div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div> <div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div>
</div> </div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="multiplicationOnly">
<label class="form-check-label" for="multiplicationOnly">Générer uniquement des multiplications</label>
</div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="generateBtn"> <button type="submit" class="btn btn-primary btn-lg" id="generateBtn">
<span id="buttonText">Générer le PDF (Mult/Div)</span> <span id="buttonText">Générer le PDF (Mult/Div)</span>
@@ -511,6 +516,7 @@
const minTable = parseInt(document.getElementById('minTable').value); const minTable = parseInt(document.getElementById('minTable').value);
const maxTable = parseInt(document.getElementById('maxTable').value); const maxTable = parseInt(document.getElementById('maxTable').value);
const numExercises = parseInt(document.getElementById('numExercises').value); const numExercises = parseInt(document.getElementById('numExercises').value);
const multiplicationOnly = document.getElementById('multiplicationOnly').checked;
const generateBtn = document.getElementById('generateBtn'); const generateBtn = document.getElementById('generateBtn');
const buttonText = document.getElementById('buttonText'); const buttonText = document.getElementById('buttonText');
@@ -530,7 +536,8 @@
body: JSON.stringify({ body: JSON.stringify({
min_table: minTable, min_table: minTable,
max_table: maxTable, max_table: maxTable,
num_exercises: numExercises num_exercises: numExercises,
multiplication_only: multiplicationOnly
}) })
}); });
@@ -545,13 +552,14 @@
// Show success message // Show success message
const resultContainer = document.getElementById('resultContainer'); const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage'); const resultMessage = document.getElementById('resultMessage');
const exerciseType = multiplicationOnly ? "multiplications uniquement" : "multiplications et divisions";
resultMessage.innerHTML = ` resultMessage.innerHTML = `
<div class="alert alert-success"> <div class="alert alert-success">
<h6>PDF généré avec succès!</h6> <h6>PDF généré avec succès!</h6>
<p>Le téléchargement devrait commencer automatiquement.</p> <p>Le téléchargement devrait commencer automatiquement.</p>
<ul class="mb-0"> <ul class="mb-0">
<li>Tables de ${minTable} à ${maxTable}</li> <li>Tables de ${minTable} à ${maxTable}</li>
<li>${numExercises} exercices générés</li> <li>${numExercises} exercices générés (${exerciseType})</li>
</ul> </ul>
</div> </div>
`; `;