diff --git a/backend/api/Projetcs-service/app.py b/backend/api/Projetcs-service/app.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/Projetcs-service/config.py b/backend/api/Projetcs-service/config.py new file mode 100644 index 0000000..4399415 --- /dev/null +++ b/backend/api/Projetcs-service/config.py @@ -0,0 +1 @@ +DB_USE = "JSONDB" # Change this to "PostgreSQL" or "MongoDB" as needed diff --git a/backend/api/Projetcs-service/projects_routes.py b/backend/api/Projetcs-service/projects_routes.py new file mode 100644 index 0000000..9a772d9 --- /dev/null +++ b/backend/api/Projetcs-service/projects_routes.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, HTTPException +from config import DB_USE +from src import ProjectCreate, ProjectOut +from src import get_repo + +ProjectRouter = APIRouter() + +db = get_repo(DB_USE) + + +@ProjectRouter.post("/projects/", response_model=ProjectOut) +def create_project(project: ProjectCreate): + """Create a new project.""" + return db.create_project(project) + + +@ProjectRouter.get("/projects/", response_model=list[ProjectOut]) +def get_projects(): + """Get all projects.""" + return db.get_projects() + + +@ProjectRouter.get("/projects/{project_id}", response_model=ProjectOut) +def get_project(project_id: str): + """Get a project by ID.""" + project = db.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return project + + +@ProjectRouter.delete("/projects/{project_id}") +def delete_project(project_id: str): + """Delete a project by ID.""" + db.delete_project(project_id) + return {"detail": "Project deleted"} + + +@ProjectRouter.put("/projects/{project_id}", response_model=ProjectOut) +def update_project(project_id: str, project: ProjectCreate): + """Update a project by ID.""" + updated_project = db.update_project(project_id, project) + if not updated_project: + raise HTTPException(status_code=404, detail="Project not found") + return updated_project diff --git a/backend/api/Projetcs-service/src/__init__.py b/backend/api/Projetcs-service/src/__init__.py new file mode 100644 index 0000000..debfc05 --- /dev/null +++ b/backend/api/Projetcs-service/src/__init__.py @@ -0,0 +1,11 @@ +from database.DBSelect import get_repo +from models.projects import Project +from schemas.projects_schema import ProjectCreate, ProjectOut + + +__all__ = [ + "get_repo", + "Project", + "ProjectCreate", + "ProjectOut", +] diff --git a/backend/api/Projetcs-service/src/database/AbstradDB.py b/backend/api/Projetcs-service/src/database/AbstradDB.py new file mode 100644 index 0000000..31dddd9 --- /dev/null +++ b/backend/api/Projetcs-service/src/database/AbstradDB.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + + +class AbstractDB(ABC): + @abstractmethod + def create_project(self, project): + """Create a new project in the database.""" + pass + + @abstractmethod + def get_projects(self): + """Retrieve all projects from the database.""" + pass + + @abstractmethod + def get_project(self, project_id): + """Retrieve a specific project by its ID.""" + pass + + @abstractmethod + def delete_project(self, project_id): + """Delete a project from the database.""" + pass + + @abstractmethod + def update_project(self, project_id, project_data): + """Update an existing project.""" + pass diff --git a/backend/api/Projetcs-service/src/database/DBSelect.py b/backend/api/Projetcs-service/src/database/DBSelect.py new file mode 100644 index 0000000..fc9ead8 --- /dev/null +++ b/backend/api/Projetcs-service/src/database/DBSelect.py @@ -0,0 +1,21 @@ +from database.JSONDB import JSONDB +from database.PostgreSQLDB import PostgreSQLDB +from sqlalchemy import SessionLocal +from pymongo import MongoClient +from database.MongoDB import MongoDB + + +def get_repo(db_type: str): + """Get the appropriate database repository based on the type.""" + if db_type == "JSONDB": + return JSONDB("projects.json") + + elif db_type == "PostgreSQL": + return PostgreSQLDB(SessionLocal()) + + elif db_type == "MongoDB": + return MongoDB(MongoClient("mongodb://localhost:27017/"), + "projects_db") + + else: + raise ValueError("Unknown DB type") diff --git a/backend/api/Projetcs-service/src/database/JSONDB.py b/backend/api/Projetcs-service/src/database/JSONDB.py new file mode 100644 index 0000000..5b5ff48 --- /dev/null +++ b/backend/api/Projetcs-service/src/database/JSONDB.py @@ -0,0 +1,67 @@ +from database.AbstradDB import AbstractDB +from models.projects import Project +from schemas.projects_schema import ProjectCreate +import json + +file_path = "projectsDB.json" + +# JSONDB is a simple file-based database for storing project data in JSON. +# It implements the AbstractDB interface and provides methods for creating, + + +class JSONDB(AbstractDB): + def __init__(self, file_path: str): + """Initialize the JSONDB with a file path.""" + self.file_path = file_path + self.projects = [] + self.load_data() + + def load_data(self): + """Load data from the JSON file.""" + try: + with open(self.file_path, "r") as file: + self.projects = json.load(file) + except FileNotFoundError: + self.projects = [] + + def save_data(self): + """Save data to the JSON file.""" + with open(self.file_path, "w") as file: + json.dump(self.projects, file, indent=4) + + def create_project(self, project: ProjectCreate) -> Project: + """Create a new project in the database.""" + new_project = Project(**project.dict()) + self.projects.append(new_project.dict()) + self.save_data() + return new_project + + def get_projects(self) -> list[Project]: + """Retrieve all projects from the database.""" + return [Project(**project) for project in self.projects] + + def get_project(self, project_id: int) -> Project: + """Retrieve a specific project by its ID.""" + for project in self.projects: + if project["id"] == project_id: + return Project(**project) + return None + + def delete_project(self, project_id: int) -> None: + """Delete a project from the database.""" + self.projects = [ + project for project in self.projects if project["id"] != project_id + ] + self.save_data() + + def update_project( + self, project_id: int, project_data: ProjectCreate + ) -> Project: + """Update an existing project.""" + for project in self.projects: + if project["id"] == project_id: + for key, value in project_data.dict().items(): + project[key] = value + self.save_data() + return Project(**project) + return None diff --git a/backend/api/Projetcs-service/src/database/MongoDB.py b/backend/api/Projetcs-service/src/database/MongoDB.py new file mode 100644 index 0000000..7c26d75 --- /dev/null +++ b/backend/api/Projetcs-service/src/database/MongoDB.py @@ -0,0 +1,35 @@ +from pymongo import MongoClient +from database.AbstradDB import AbstractDB +from models.projects import Project + + +class MondoDB(AbstractDB): + def __init__(self, uri: str, db_name: str): + """Initialize the MongoDB client and database.""" + self.uri = uri + self.client = MongoClient(uri) + self.db = self.client[db_name] + self.collection = self.db["projects"] + + def create_project(self, project: Project) -> Project: + """Create a new project in the database.""" + project_dict = project.dict() + result = self.collection.insert_one(project_dict) + project.id = str(result.inserted_id) + return project + + def get_projects(self) -> list[Project]: + """Retrieve all projects from the database.""" + projects = self.collection.find() + return [Project(**project) for project in projects] + + def get_project(self, project_id: str) -> Project: + """Retrieve a specific project by its ID.""" + project = self.collection.find_one({"_id": project_id}) + if project: + return Project(**project) + return None + + def delete_project(self, project_id: str) -> None: + """Delete a project from the database.""" + self.collection.delete_one({"_id": project_id}) diff --git a/backend/api/Projetcs-service/src/database/PostgreSQLDB.py b/backend/api/Projetcs-service/src/database/PostgreSQLDB.py new file mode 100644 index 0000000..58a1a05 --- /dev/null +++ b/backend/api/Projetcs-service/src/database/PostgreSQLDB.py @@ -0,0 +1,45 @@ +from sqlalchemy.orm import Session +from models.projects import Project +from schemas.projects_schema import ProjectCreate +from database.AbstradDB import AbstractDB + + +class PostgreSQLDB(AbstractDB): + def __init__(self, db: Session): + self.db = db + + def create_project(self, project: ProjectCreate) -> Project: + """Create a new project in the database.""" + db_project = Project(**project.dict()) + self.db.add(db_project) + self.db.commit() + self.db.refresh(db_project) + return db_project + + def get_projects(self) -> list[Project]: + """Retrieve all projects from the database.""" + return self.db.query(Project).all() + + def get_project(self, project_id: int) -> Project: + """Retrieve a specific project by its ID.""" + return self.db.query(Project).filter(Project.id == project_id).first() + + def delete_project(self, project_id: int) -> None: + """Delete a project from the database.""" + project = self.get_project(project_id) + if project: + self.db.delete(project) + self.db.commit() + + def update_project( + self, project_id: int, project_data: ProjectCreate + ) -> Project: + """Update an existing project.""" + project = self.get_project(project_id) + if project: + for key, value in project_data.dict().items(): + setattr(project, key, value) + self.db.commit() + self.db.refresh(project) + return project + return None diff --git a/backend/api/Projetcs-service/src/models/projects.py b/backend/api/Projetcs-service/src/models/projects.py new file mode 100644 index 0000000..cb66878 --- /dev/null +++ b/backend/api/Projetcs-service/src/models/projects.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String, Text +from database import Base + + +class Project(Base): + __tablename__ = "projects" + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + description = Column(Text) + owner_id = Column(Integer, nullable=False) diff --git a/backend/api/Projetcs-service/src/schemas/projects_schema.py b/backend/api/Projetcs-service/src/schemas/projects_schema.py new file mode 100644 index 0000000..1fedfd2 --- /dev/null +++ b/backend/api/Projetcs-service/src/schemas/projects_schema.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + + +class ProjectBase(BaseModel): + name: str + description: str | None = None + owner_id: int + + +class ProjectCreate(ProjectBase): + name: str + description: str + + +class ProjectOut(ProjectBase): + id: int + owner_id: int + + class Config: + orm_mode = True diff --git a/backend/api/Projetcs-service/tests/project_test.py b/backend/api/Projetcs-service/tests/project_test.py new file mode 100644 index 0000000..406e18f --- /dev/null +++ b/backend/api/Projetcs-service/tests/project_test.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock +import pytest + + +class TestCodeUnderTest: + + # create_project successfully adds a new project to the database + def test_create_project_success(self): + # Arrange + from sqlalchemy.orm import Session + from src import Project + from src import ProjectCreate + from src import create_project + # Mock session + mock_db = MagicMock(spec=Session) + # Create project data + project_data = { + "name": "Test Project", + "description": "Test Description", + "owner_id": 1 + } + project_create = ProjectCreate(**project_data) + # Act + result = create_project(mock_db, project_create) + # Assert + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + mock_db.refresh.assert_called_once() + assert isinstance(result, Project) + assert result.name == project_data["name"] + assert result.description == project_data["description"] + assert result.owner_id == project_data["owner_id"] + + # create_project with missing required fields (name, owner_id) + def test_create_project_missing_required_fields(self): + # Arrange + from sqlalchemy.orm import Session + from src import ProjectCreate + from src import create_project + from sqlalchemy.exc import IntegrityError + # Mock session + mock_db = MagicMock(spec=Session) + # Set up the mock to raise IntegrityError when commit is called + mock_db.commit.side_effect = IntegrityError( + "(sqlite3.IntegrityError) NOT NULL constraint failed", None, None + ) + # Create project with missing required fields + project_data = { + "description": "Test Description" + # Missing name and owner_id + } + project_create = ProjectCreate(**project_data) + # Act & Assert + with pytest.raises(IntegrityError): + create_project(mock_db, project_create) + # Verify the session interactions + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + mock_db.refresh.assert_not_called() diff --git a/backend/docs/ProjectsService.md b/backend/docs/ProjectsService.md new file mode 100644 index 0000000..f533476 --- /dev/null +++ b/backend/docs/ProjectsService.md @@ -0,0 +1,117 @@ +# Microservicio de Gestión de Proyectos + +## Diagrama de Arquitectura + +```mermaid +graph TD + A[Clientes] --> B[API REST - FastAPI] + B --> C[ProjectRouter] + C --> D[Servicio de Proyectos] + D --> E[AbstractDB] + E --> F[(JSONDB)] + E --> G[(PostgreSQL)] + E --> H[(MongoDB)] + + style A fill:#4a90e2,stroke:#333 + style B fill:#50e3c2,stroke:#333 + style C fill:#f5a623,stroke:#333 + style D fill:#7ed321,stroke:#333 + style E fill:#bd10e0,stroke:#333 + style F fill:#ff7675,stroke:#333 + style G fill:#ff7675,stroke:#333 + style H fill:#ff7675,stroke:#333 +``` + +## Estructura de Carpetas + +📁 Projects-service +├── 📁 src +│ ├── 📁 database +│ │ ├── 📄 AbstradDB.py +│ │ ├── 📄 DBSelect.py +│ │ ├── 📄 JSONDB.py +│ │ ├── 📄 MongoDB.py +│ │ └── 📄 PostgreSQLDB.py +│ ├── 📁 models +│ │ └── 📄 projects.py +│ ├── 📁 schemas +│ │ └── 📄 projects_schema.py +│ ├── 📄 `__init__`.py +├── 📁 tests +│ └── 📄 project_test.py +├── 📄 config.py +├── 📄 projects_routes.py +└── 📄 requirements.txt + +__Descripción de Directorios:__ + +- `src/`: Código fuente principal + - `api/`: Endpoints y routers FastAPI + - `services/`: Lógica de negocio + - `database/`: Conexiones y abstracciones DB + - `schemas/`: Modelos Pydantic +- `docs/`: Documentación técnica +- `config/`: Configuraciones y variables de entorno +- `tests/`: Pruebas unitarias e integración + +## Estructura Técnica + +### 1. Capa API + +- __Router__: `projects_routes.py` + - Endpoints REST para operaciones CRUD + - Validación automática con modelos Pydantic + - Manejo de errores HTTP + +```python +@ProjectRouter.post("/projects/", response_model=ProjectOut) +def create_project(project: ProjectCreate): + """Create a new project.""" + return db.create_project(project) +``` + +### 2. Capa de Servicio + +- __AbstractDB__: `AbstradDB.py` + - Interfaz abstracta para operaciones de base de datos + - Patrón Repository para desacoplamiento + - Implementaciones concretas: + - `JSONDB`: Almacenamiento en archivo JSON + - `PostgreSQLDB`: Base de datos relacional + - `MongoDB`: Base de datos NoSQL + +### 3. Capa de Datos + +- __Esquemas__: `projects_schema.py` + - Modelos Pydantic para: + - Validación de entrada/salida + - Documentación automática de API + - Configuración ORM para integración con DB + +### 4. Configuración + +- __DB_USE__: `config.py` + - Selección dinámica de base de datos + - Estrategia de inyección de dependencias + +```python +def get_repo(db_type: str): + """Get the appropriate database repository based on type.""" + if db_type == "JSONDB": + return JSONDB("projects.json") +``` + +## Flujo de Datos + +1. Cliente realiza petición HTTP +2. Router valida entrada con esquemas Pydantic +3. Servicio ejecuta lógica de negocio +4. Repositorio interactúa con la base de datos +5. Respuesta se serializa con modelo ProjectOut + +## Consideraciones de Diseño + +- Desacople total entre capas +- Fácil intercambio de proveedores de base de datos +- Documentación automática mediante OpenAPI +- Tipado fuerte con validación en tiempo de ejecución