diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..033963b --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +DB_HOST= +DB_PORT=3306 + +DB_NAME= + +DB_USER= +DB_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..991c3c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd + +.env + +.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb67acd --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..0913d01 --- /dev/null +++ b/app/database.py @@ -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() \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..57d7fcb --- /dev/null +++ b/app/main.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI + +from app.routers import projects, experiences, skills + +app = FastAPI() + +app.include_router(projects.router, prefix="/api") +app.include_router(experiences.router, prefix="/api") +app.include_router(skills.router, prefix="/api") diff --git a/app/models/experience.py b/app/models/experience.py new file mode 100644 index 0000000..215b150 --- /dev/null +++ b/app/models/experience.py @@ -0,0 +1,47 @@ +from app.models.experience_task import ExperienceTask + +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" + ) \ No newline at end of file diff --git a/app/models/experience_task.py b/app/models/experience_task.py new file mode 100644 index 0000000..ee854db --- /dev/null +++ b/app/models/experience_task.py @@ -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" + ) \ No newline at end of file diff --git a/app/models/project.py b/app/models/project.py new file mode 100644 index 0000000..43a7937 --- /dev/null +++ b/app/models/project.py @@ -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" + ) \ No newline at end of file diff --git a/app/models/project_skill.py b/app/models/project_skill.py new file mode 100644 index 0000000..c048a15 --- /dev/null +++ b/app/models/project_skill.py @@ -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 + ) +) \ No newline at end of file diff --git a/app/models/skill.py b/app/models/skill.py new file mode 100644 index 0000000..5bd83c7 --- /dev/null +++ b/app/models/skill.py @@ -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" + ) \ No newline at end of file diff --git a/app/routers/experiences.py b/app/routers/experiences.py new file mode 100644 index 0000000..98984c9 --- /dev/null +++ b/app/routers/experiences.py @@ -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() + ) diff --git a/app/routers/projects.py b/app/routers/projects.py new file mode 100644 index 0000000..424a7c5 --- /dev/null +++ b/app/routers/projects.py @@ -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 \ No newline at end of file diff --git a/app/routers/skills.py b/app/routers/skills.py new file mode 100644 index 0000000..4f3393d --- /dev/null +++ b/app/routers/skills.py @@ -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() \ No newline at end of file diff --git a/app/schemas/experience.py b/app/schemas/experience.py new file mode 100644 index 0000000..8b19059 --- /dev/null +++ b/app/schemas/experience.py @@ -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 = True + diff --git a/app/schemas/project.py b/app/schemas/project.py new file mode 100644 index 0000000..cd56f1d --- /dev/null +++ b/app/schemas/project.py @@ -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 \ No newline at end of file diff --git a/app/schemas/skill.py b/app/schemas/skill.py new file mode 100644 index 0000000..501959f --- /dev/null +++ b/app/schemas/skill.py @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fc8fbec --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7823962 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +sqlalchemy +pymysql +python-dotenv \ No newline at end of file