Compare commits
2 Commits
f94dd12216
...
75548dab2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
75548dab2b
|
|||
|
a67db405f7
|
@@ -17,3 +17,4 @@ app/static/*.pdf
|
|||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.envrc
|
||||||
|
|||||||
+118
-33
@@ -1,20 +1,96 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import random
|
import random
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.responses import HTMLResponse, FileResponse
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fpdf import FPDF
|
from fpdf import FPDF
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# Mount static files and templates
|
# S3 Configuration
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', 'math-exercises')
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
def get_s3_client():
|
||||||
|
"""Create and return an S3 client using environment variables"""
|
||||||
|
s3_access_key = os.environ.get('S3_ACCESS_KEY')
|
||||||
|
s3_secret_key = os.environ.get('S3_SECRET_KEY')
|
||||||
|
s3_host_base = os.environ.get('S3_HOST_BASE')
|
||||||
|
s3_host_bucket = os.environ.get('S3_HOST_BUCKET')
|
||||||
|
|
||||||
|
if not all([s3_access_key, s3_secret_key, s3_host_base]):
|
||||||
|
raise ValueError("S3 environment variables not properly set")
|
||||||
|
|
||||||
|
s3 = boto3.client(
|
||||||
|
's3',
|
||||||
|
aws_access_key_id=s3_access_key,
|
||||||
|
aws_secret_access_key=s3_secret_key,
|
||||||
|
endpoint_url=s3_host_base,
|
||||||
|
region_name='us-east-1' # Required but unused for Infomaniak
|
||||||
|
)
|
||||||
|
return s3
|
||||||
|
|
||||||
|
def create_bucket_if_not_exists(bucket_name):
|
||||||
|
"""Create S3 bucket if it doesn't exist"""
|
||||||
|
s3_client = get_s3_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
s3_client.head_bucket(Bucket=bucket_name)
|
||||||
|
except ClientError as e:
|
||||||
|
error_code = int(e.response['Error']['Code'])
|
||||||
|
if error_code == 404:
|
||||||
|
# Bucket doesn't exist, create it
|
||||||
|
try:
|
||||||
|
s3_client.create_bucket(Bucket=bucket_name)
|
||||||
|
print(f"Bucket {bucket_name} created successfully")
|
||||||
|
except ClientError as create_error:
|
||||||
|
print(f"Error creating bucket: {create_error}")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Some other error
|
||||||
|
print(f"Error checking bucket: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Create bucket on startup
|
||||||
|
try:
|
||||||
|
create_bucket_if_not_exists(S3_BUCKET_NAME)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not create/check S3 bucket: {e}")
|
||||||
|
|
||||||
|
def upload_to_s3(file_data, bucket_name, object_name, content_type='application/pdf'):
|
||||||
|
"""Upload file data to S3 bucket"""
|
||||||
|
s3_client = get_s3_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
s3_client.put_object(
|
||||||
|
Bucket=bucket_name,
|
||||||
|
Key=object_name,
|
||||||
|
Body=file_data,
|
||||||
|
ContentType=content_type
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error uploading to S3: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def download_from_s3(bucket_name, object_name):
|
||||||
|
"""Download file data from S3 bucket"""
|
||||||
|
s3_client = get_s3_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = s3_client.get_object(Bucket=bucket_name, Key=object_name)
|
||||||
|
return response['Body'].read()
|
||||||
|
except ClientError as e:
|
||||||
|
print(f"Error downloading from S3: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
class MathExercisesPDF(FPDF):
|
class MathExercisesPDF(FPDF):
|
||||||
def header(self):
|
def header(self):
|
||||||
self.set_font('Helvetica', 'B', 16)
|
self.set_font('Helvetica', 'B', 16)
|
||||||
@@ -91,16 +167,23 @@ def generate_exercises(min_table: int, max_table: int, num_exercises: int = 15)
|
|||||||
return exercises
|
return exercises
|
||||||
|
|
||||||
def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int = 15) -> str:
|
def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int = 15) -> str:
|
||||||
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes"""
|
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes et l'upload sur S3"""
|
||||||
pdf = MathExercisesPDF()
|
import datetime
|
||||||
pdf.add_page()
|
|
||||||
pdf.set_font('Helvetica', '', 12)
|
# Add timestamp to filename
|
||||||
|
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 min_table == max_table:
|
||||||
table_info = f'Tables de multiplication et division pour {min_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'
|
||||||
else:
|
else:
|
||||||
table_info = f'Tables de multiplication et division de {min_table} à {max_table}'
|
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.add_page()
|
||||||
|
pdf.set_font('Helvetica', '', 12)
|
||||||
|
|
||||||
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
|
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
|
||||||
pdf.ln(5)
|
pdf.ln(5)
|
||||||
@@ -146,18 +229,23 @@ def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int
|
|||||||
pdf.cell(60, 10, col2_text, 0, 0, 'L', new_x="RIGHT", new_y="TOP")
|
pdf.cell(60, 10, col2_text, 0, 0, 'L', new_x="RIGHT", new_y="TOP")
|
||||||
pdf.cell(60, 10, col3_text, 0, 1, 'L', new_x="LMARGIN", new_y="NEXT")
|
pdf.cell(60, 10, col3_text, 0, 1, 'L', new_x="LMARGIN", new_y="NEXT")
|
||||||
|
|
||||||
# Sauvegarder le PDF
|
# Sauvegarder le PDF en mémoire
|
||||||
if min_table == max_table:
|
pdf_data = pdf.output()
|
||||||
filename = f'app/static/exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf'
|
|
||||||
else:
|
# Upload to S3
|
||||||
filename = f'app/static/exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf'
|
upload_success = upload_to_s3(pdf_data, S3_BUCKET_NAME, pdf_filename)
|
||||||
pdf.output(filename)
|
|
||||||
return filename
|
if not upload_success:
|
||||||
|
raise Exception("Failed to upload PDF to S3")
|
||||||
|
|
||||||
|
return pdf_filename
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def read_root(request: Request):
|
async def read_root(request: Request):
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
@app.post("/generate")
|
@app.post("/generate")
|
||||||
async def generate_exercises_endpoint(request: ExerciseRequest):
|
async def generate_exercises_endpoint(request: ExerciseRequest):
|
||||||
try:
|
try:
|
||||||
@@ -168,35 +256,32 @@ async def generate_exercises_endpoint(request: ExerciseRequest):
|
|||||||
if request.num_exercises < 1:
|
if request.num_exercises < 1:
|
||||||
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
|
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
|
||||||
|
|
||||||
filename = create_math_exercises_pdf(
|
pdf_filename = create_math_exercises_pdf(
|
||||||
request.min_table,
|
request.min_table,
|
||||||
request.max_table,
|
request.max_table,
|
||||||
request.num_exercises
|
request.num_exercises
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract just the filename for the download link
|
# Return redirect to automatically download the file
|
||||||
pdf_filename = os.path.basename(filename)
|
return RedirectResponse(url=f"/download/{pdf_filename}", status_code=303)
|
||||||
|
|
||||||
return {
|
|
||||||
"message": "PDF généré avec succès!",
|
|
||||||
"pdf_filename": pdf_filename,
|
|
||||||
"min_table": request.min_table,
|
|
||||||
"max_table": request.max_table,
|
|
||||||
"num_exercises": request.num_exercises
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
|
return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
|
||||||
|
|
||||||
@app.get("/download/{filename}")
|
@app.get("/download/{filename}")
|
||||||
async def download_pdf(filename: str):
|
async def download_pdf(filename: str):
|
||||||
file_path = f"app/static/{filename}"
|
# Download file from S3
|
||||||
if os.path.exists(file_path):
|
file_data = download_from_s3(S3_BUCKET_NAME, filename)
|
||||||
return FileResponse(
|
|
||||||
path=file_path,
|
if file_data is None:
|
||||||
media_type='application/pdf',
|
return {"error": "File not found"}
|
||||||
filename=filename
|
|
||||||
)
|
# Return streaming response with PDF data
|
||||||
return {"error": "File not found"}
|
return StreamingResponse(
|
||||||
|
io.BytesIO(file_data),
|
||||||
|
media_type='application/pdf',
|
||||||
|
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
+24
-18
@@ -176,31 +176,37 @@
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
// Check if it's a redirect response (for automatic download)
|
||||||
|
if (response.redirected) {
|
||||||
const resultContainer = document.getElementById('resultContainer');
|
// Automatically trigger download
|
||||||
const resultMessage = document.getElementById('resultMessage');
|
window.location.href = response.url;
|
||||||
const downloadLinkContainer = document.getElementById('downloadLinkContainer');
|
|
||||||
const downloadLink = document.getElementById('downloadLink');
|
// Show success message
|
||||||
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
if (result.error) {
|
const resultMessage = document.getElementById('resultMessage');
|
||||||
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
|
||||||
downloadLinkContainer.classList.add('d-none');
|
|
||||||
} else {
|
|
||||||
resultMessage.innerHTML = `
|
resultMessage.innerHTML = `
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<h6>${result.message}</h6>
|
<h6>PDF généré avec succès!</h6>
|
||||||
|
<p>Le téléchargement devrait commencer automatiquement.</p>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>Tables de ${result.min_table} à ${result.max_table}</li>
|
<li>Tables de ${minTable} à ${maxTable}</li>
|
||||||
<li>${result.num_exercises} exercices générés</li>
|
<li>${numExercises} exercices générés</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
downloadLink.href = `/download/${result.pdf_filename}`;
|
resultContainer.classList.remove('d-none');
|
||||||
downloadLinkContainer.classList.remove('d-none');
|
} else {
|
||||||
|
// Handle JSON response (error case)
|
||||||
|
const result = await response.json();
|
||||||
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
|
const resultMessage = document.getElementById('resultMessage');
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultContainer.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
resultContainer.classList.remove('d-none');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const resultContainer = document.getElementById('resultContainer');
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
const resultMessage = document.getElementById('resultMessage');
|
const resultMessage = document.getElementById('resultMessage');
|
||||||
|
|||||||
@@ -7,8 +7,4 @@ data:
|
|||||||
PORT: "8000"
|
PORT: "8000"
|
||||||
LOG_LEVEL: "INFO"
|
LOG_LEVEL: "INFO"
|
||||||
MAX_REQUEST_SIZE: "10mb"
|
MAX_REQUEST_SIZE: "10mb"
|
||||||
|
|
||||||
# Security configuration
|
|
||||||
SECURE_COOKIES: "true"
|
|
||||||
CORS_ORIGINS: "math-exercises.local"
|
|
||||||
REQUEST_TIMEOUT: "30s"
|
REQUEST_TIMEOUT: "30s"
|
||||||
@@ -65,11 +65,3 @@ spec:
|
|||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: math-exercises-config
|
name: math-exercises-config
|
||||||
# Volume mounts for writable directories
|
|
||||||
volumeMounts:
|
|
||||||
- name: static-volume
|
|
||||||
mountPath: /app/app/static
|
|
||||||
# Volumes
|
|
||||||
volumes:
|
|
||||||
- name: static-volume
|
|
||||||
emptyDir: {}
|
|
||||||
@@ -16,6 +16,10 @@ spec:
|
|||||||
value: development
|
value: development
|
||||||
- name: DEBUG
|
- name: DEBUG
|
||||||
value: "false"
|
value: "false"
|
||||||
|
# Environment variables from S3 credentials secret
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: s3-credentials
|
||||||
# Reduce resource consumption in development
|
# Reduce resource consumption in development
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
|
|||||||
@@ -17,4 +17,9 @@ images:
|
|||||||
# Development-specific labels
|
# Development-specific labels
|
||||||
commonLabels:
|
commonLabels:
|
||||||
environment: development
|
environment: development
|
||||||
security-level: standard
|
security-level: standard
|
||||||
|
|
||||||
|
secretGenerator:
|
||||||
|
- name: s3-credentials
|
||||||
|
envs:
|
||||||
|
- s3-credentials.env
|
||||||
@@ -8,7 +8,6 @@ metadata:
|
|||||||
# Security annotations
|
# Security annotations
|
||||||
seccomp.security.alpha.kubernetes.io/pod: docker/default
|
seccomp.security.alpha.kubernetes.io/pod: docker/default
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
|||||||
@@ -22,4 +22,9 @@ images:
|
|||||||
# Production-specific labels
|
# Production-specific labels
|
||||||
commonLabels:
|
commonLabels:
|
||||||
environment: production
|
environment: production
|
||||||
security-level: high
|
security-level: high
|
||||||
|
|
||||||
|
secretGenerator:
|
||||||
|
- name: s3-credentials
|
||||||
|
envs:
|
||||||
|
- s3-credentials.env
|
||||||
|
|||||||
@@ -24,4 +24,8 @@ spec:
|
|||||||
drop:
|
drop:
|
||||||
- ALL
|
- ALL
|
||||||
add:
|
add:
|
||||||
- NET_BIND_SERVICE
|
- NET_BIND_SERVICE
|
||||||
|
# Environment variables from S3 credentials secret
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: s3-credentials
|
||||||
@@ -6,3 +6,4 @@ fastapi==0.115.0
|
|||||||
uvicorn==0.30.6
|
uvicorn==0.30.6
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
python-multipart==0.0.9
|
python-multipart==0.0.9
|
||||||
|
boto3==1.34.0
|
||||||
Reference in New Issue
Block a user