initialize backend with FastAPI and Docker setup

This commit is contained in:
Giovanni committed 2026-06-03 11:35:09 +02:00
1 parent 5d724e37ab
commit 5ff02c0ff5
24 files changed
+420 -243

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
DB_HOST=
DB_PORT=3306
DB_NAME=
DB_USER=
DB_PASSWORD=
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
+38
View File
@@ -0,0 +1,38 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import declarative_base
from dotenv import load_dotenv
import os
load_dotenv()
DATABASE_URL = (
f"mysql+pymysql://"
f"{os.getenv('DB_USER')}:"
f"{os.getenv('DB_PASSWORD')}@"
f"{os.getenv('DB_HOST')}:"
f"{os.getenv('DB_PORT')}/"
f"{os.getenv('DB_NAME')}"
)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True
)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
+9
View File
@@ -0,0 +1,9 @@
from fastapi import FastAPI
from app.routers import projects, experiences, skills
app = FastAPI()
app.include_router(projects.router)
app.include_router(experiences.router)
app.include_router(skills.router)
+45
View File
@@ -0,0 +1,45 @@
from sqlalchemy import (
Column,
Integer,
String
)
from sqlalchemy.orm import relationship
from app.database import Base
class Experience(Base):
__tablename__ = "experiences"
id = Column(
Integer,
primary_key=True,
index=True
)
company = Column(
String(255),
nullable=False
)
location = Column(
String(255),
nullable=False
)
duration = Column(
String(255),
nullable=False
)
role = Column(
String(255),
nullable=False
)
tasks = relationship(
"ExperienceTask",
back_populates="experience",
cascade="all, delete-orphan"
)
+39
View File
@@ -0,0 +1,39 @@
from sqlalchemy import (
Column,
Integer,
String,
ForeignKey
)
from sqlalchemy.orm import relationship
from app.database import Base
class ExperienceTask(Base):
__tablename__ = "experience_tasks"
id = Column(
Integer,
primary_key=True,
index=True
)
description = Column(
String(255),
nullable=False
)
experience_id = Column(
Integer,
ForeignKey(
"experiences.id",
ondelete="CASCADE"
),
nullable=False
)
experience = relationship(
"Experience",
back_populates="tasks"
)
+70
View File
@@ -0,0 +1,70 @@
from sqlalchemy import (
Column,
Integer,
String,
Text,
Boolean
)
from sqlalchemy.orm import relationship
from app.database import Base
from app.models.project_skill import project_skills
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
title = Column(
String(255),
nullable=False
)
nb_image = Column(
Integer,
default=1
)
short_description = Column(
Text,
nullable=False
)
image_name = Column(
String(255),
nullable=False,
default="empty"
)
school = Column(
Boolean,
nullable=False
)
long_description = Column(
Text,
nullable=False
)
repo_link = Column(
String(255),
nullable=True
)
beginning_year = Column(
Integer,
nullable=False
)
end_year = Column(
Integer,
nullable=True
)
skills = relationship(
"Skill",
secondary=project_skills,
back_populates="projects"
)
+22
View File
@@ -0,0 +1,22 @@
from sqlalchemy import Table, Column, Integer, ForeignKey
from app.database import Base
project_skills = Table(
"project_skills",
Base.metadata,
Column(
"project_id",
Integer,
ForeignKey("projects.id", ondelete="CASCADE"),
primary_key=True
),
Column(
"skill_id",
Integer,
ForeignKey("skills.id", ondelete="CASCADE"),
primary_key=True
)
)
+28
View File
@@ -0,0 +1,28 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from app.database import Base
from app.models.project_skill import project_skills
class Skill(Base):
__tablename__ = "skills"
id = Column(Integer, primary_key=True, index=True)
name = Column(
String(255),
nullable=False,
unique=True
)
type = Column(
String(255),
nullable=False
)
projects = relationship(
"Project",
secondary=project_skills,
back_populates="skills"
)
+17
View File
@@ -0,0 +1,17 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from app.database import get_db
from app.models.experience import Experience
from app.schemas.experience import ExperienceResponse
router = APIRouter(prefix="/experiences", tags=["Experiences"])
@router.get("/", response_model=list[ExperienceResponse])
def get_experiences(db: Session = Depends(get_db)):
return (
db.query(Experience)
.options(joinedload(Experience.tasks))
.all()
)
+32
View File
@@ -0,0 +1,32 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from app.database import get_db
from app.models.project import Project
from app.schemas.project import ProjectCard, ProjectPage
router = APIRouter(prefix="/projects", tags=["Projects"])
@router.get("/", response_model=list[ProjectCard])
def get_projects(db: Session = Depends(get_db)):
return (
db.query(Project)
.options(joinedload(Project.skills))
.all()
)
@router.get("/{project_id}", response_model=ProjectPage)
def get_project(project_id: int, db: Session = Depends(get_db)):
project = (
db.query(Project)
.options(joinedload(Project.skills))
.filter(Project.id == project_id)
.first()
)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
+13
View File
@@ -0,0 +1,13 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.skill import Skill
from app.schemas.skill import SkillResponse
router = APIRouter(prefix="/skills", tags=["Skills"])
@router.get("/", response_model=list[SkillResponse])
def get_skills(db: Session = Depends(get_db)):
return db.query(Skill).all()
+20
View File
@@ -0,0 +1,20 @@
from pydantic import BaseModel
class ExperienceTaskResponse(BaseModel):
id: int
description: str
class Config:
from_attributes = True
class ExperienceResponse(BaseModel):
id: int
company: str
location: str
duration: str
role: str
tasks: list[ExperienceTaskResponse]
class Config:
from_attributes = Trues
+33
View File
@@ -0,0 +1,33 @@
from pydantic import BaseModel
from app.schemas.skill import ProjectSkill
class ProjectCard(BaseModel):
id: int
title: str
short_description: str
image_name: str
school: bool
beginning_year: int
end_year: int | None
skills: list[ProjectSkill]
class Config:
from_attributes = True
class ProjectPage(BaseModel):
id: int
title: str
short_description: str
long_description: str
repo_link: str | None
nb_image: int
image_name: str
school: bool
beginning_year: int
end_year: int | None
skills: list[ProjectSkill]
class Config:
from_attributes = True
+17
View File
@@ -0,0 +1,17 @@
from pydantic import BaseModel
class SkillResponse(BaseModel):
id: int
name: str
type: str
class Config:
from_attributes = True
class ProjectSkill(BaseModel):
id: int
name: str
class Config:
from_attributes = True
-20
View File
@@ -1,20 +0,0 @@
# database.py (sans générateur)
import os
import MySQLdb
from dotenv import load_dotenv
load_dotenv()
def get_db_connection():
try:
conn = MySQLdb.connect(
user=os.getenv("DB_USER"),
passwd=os.getenv("DB_PASS"),
host=os.getenv("DB_HOST"),
db=os.getenv("DB_NAME"),
charset='utf8mb4'
)
return conn
except Exception as e:
print(f"ERREUR CRITIQUE DE CONNEXION DB : {e}")
return None
+13
View File
@@ -0,0 +1,13 @@
version: "3.8"
services:
backend:
build: .
container_name: portfolio-backend
restart: unless-stopped
env_file:
- .env
ports:
- "8000:8000"
volumes:
- .:/app
-48
View File
@@ -1,48 +0,0 @@
import os
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from routers import skills, experiences, experienceTasks, shortProjects, longProjects
app = FastAPI()
origins = [
"http://localhost",
"http://localhost:5173",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def read_root():
return {"status": "API Portfolio fonctionnelle !"}
app.include_router(
skills.router,
prefix="/api/skills",
tags=["Skills"],
)
app.include_router(
experiences.router,
prefix="/api/experiences",
tags=["Experiences"]
)
app.include_router(
experienceTasks.router,
prefix="/api/experienceTasks",
tags=["ExperienceTasks"]
)
app.include_router(
shortProjects.router,
prefix="/api/shortProjects",
tags=["ShortProjects"]
)
app.include_router(
longProjects.router,
prefix="/api/longProjects",
tags=["LongProjects"]
)
+4 -3
View File
@@ -1,4 +1,5 @@
fastapi
uvicorn
mysqlclient
fastapi
uvicorn[standard]
sqlalchemy
pymysql
python-dotenv
-24
View File
@@ -1,24 +0,0 @@
# routers/skills.py (sans générateur)
from fastapi import APIRouter, HTTPException
import MySQLdb
from database import get_db_connection
router = APIRouter()
@router.get("/")
def read_experienceTasks():
conn = get_db_connection()
if not conn:
raise HTTPException(status_code=503, detail="Service de base de données indisponible.")
try:
with conn.cursor(MySQLdb.cursors.DictCursor) as cursor:
sql = "SELECT * FROM experience_tasks;"
cursor.execute(sql)
result = cursor.fetchall()
return {"data": result}
except MySQLdb.Error as e:
raise HTTPException(status_code=500, detail=f"Erreur lors de la requête: {e}")
finally:
conn.close()
-24
View File
@@ -1,24 +0,0 @@
# routers/skills.py (sans générateur)
from fastapi import APIRouter, HTTPException
import MySQLdb
from database import get_db_connection
router = APIRouter()
@router.get("/")
def read_experiences():
conn = get_db_connection()
if not conn:
raise HTTPException(status_code=503, detail="Service de base de données indisponible.")
try:
with conn.cursor(MySQLdb.cursors.DictCursor) as cursor:
sql = "SELECT * FROM experiences;"
cursor.execute(sql)
result = cursor.fetchall()
return {"data": result}
except MySQLdb.Error as e:
raise HTTPException(status_code=500, detail=f"Erreur lors de la requête: {e}")
finally:
conn.close()
-53
View File
@@ -1,53 +0,0 @@
from fastapi import APIRouter, HTTPException
import MySQLdb
from database import get_db_connection
router = APIRouter()
@router.get("/{project_id}")
def read_long_project(project_id: int):
conn = get_db_connection()
if not conn:
raise HTTPException(status_code=503, detail="Service de base de données indisponible.")
try:
with conn.cursor(MySQLdb.cursors.DictCursor) as cursor:
sql = """
SELECT
p.id,
p.title,
p.long_description,
p.short_description,
p.school,
p.repo_link,
p.nb_image,
p.image_name,
p.beginning_year,
p.end_year,
GROUP_CONCAT(DISTINCT s.name ORDER BY s.name SEPARATOR ', ') AS skills
FROM
projects p
LEFT JOIN
project_skills ps ON p.id = ps.project_id
LEFT JOIN
skills s ON ps.skill_id = s.id
WHERE p.id = %s
GROUP BY p.id;
"""
cursor.execute(sql, (project_id,))
result = cursor.fetchone()
if not result:
raise HTTPException(status_code=404, detail="Projet non trouvé.")
if result['skills']:
result['skills'] = result['skills'].split(', ')
else:
result['skills'] = []
return result
except MySQLdb.Error as e:
raise HTTPException(status_code=500, detail=f"Erreur lors de la requête: {e}")
finally:
conn.close()
-48
View File
@@ -1,48 +0,0 @@
from fastapi import APIRouter, HTTPException
import MySQLdb
from database import get_db_connection
router = APIRouter()
@router.get("/")
def read_shortProjects():
conn = get_db_connection()
if not conn:
raise HTTPException(status_code=503, detail="Service de base de données indisponible.")
try:
with conn.cursor(MySQLdb.cursors.DictCursor) as cursor:
sql = """
SELECT
p.id,
p.title,
p.short_description,
p.school,
p.image_name,
p.beginning_year,
p.end_year,
GROUP_CONCAT(DISTINCT s.name ORDER BY s.name SEPARATOR ', ') AS skills
FROM
projects p
LEFT JOIN
project_skills ps ON p.id = ps.project_id
LEFT JOIN
skills s ON ps.skill_id = s.id
GROUP BY
p.id;
"""
cursor.execute(sql)
result = cursor.fetchall()
for project in result:
if project['skills']:
project['skills'] = project['skills'].split(', ')
else:
project['skills'] = []
return {"data": result}
except MySQLdb.Error as e:
raise HTTPException(status_code=500, detail=f"Erreur lors de la requête: {e}")
finally:
conn.close()
-23
View File
@@ -1,23 +0,0 @@
# routers/skills.py (sans générateur)
from fastapi import APIRouter, HTTPException
import MySQLdb
from database import get_db_connection
router = APIRouter()
@router.get("/")
def read_skills():
conn = get_db_connection()
if not conn:
raise HTTPException(status_code=503, detail="Service de base de données indisponible.")
try:
with conn.cursor(MySQLdb.cursors.DictCursor) as cursor:
sql = "SELECT * FROM skills;"
cursor.execute(sql)
result = cursor.fetchall()
return {"data": result}
except MySQLdb.Error as e:
raise HTTPException(status_code=500, detail=f"Erreur lors de la requête: {e}")
finally:
conn.close()