mira, tengo este rpoyecto:

La idea es desarrollar un sistema en el cual los usuarios puedan registrar, editar, eliminar y visualizar tareas (por ejemplo, una aplicación de lista de tareas o “To-Do List”). Aquí están los requerimientos detallados:

Requerimientos Funcionales


Registro e inicio de sesión de usuarios:
•    Los usuarios deben poder registrarse con un correo electrónico y una contraseña.
•    Debe implementarse una funcionalidad para el inicio de sesión con autenticación JWT (JSON Web Tokens).
•    Se deben manejar roles: “Usuario normal” y “Administrador”.
Gestión de tareas:
•    Crear tareas: Cada usuario debe poder crear nuevas tareas que contengan al menos: título, descripción, fecha de vencimiento y estado (por hacer, en progreso, completado).
•    Editar tareas: Los usuarios pueden modificar la información de una tarea existente.
•    Eliminar tareas: Los usuarios pueden eliminar tareas específicas.
•    Listar tareas: Debe existir una vista para que el usuario vea todas sus tareas, con la opción de filtrarlas por estado y fecha de vencimiento.
Control de acceso basado en roles (RBAC):
•    Los usuarios normales solo pueden ver y gestionar sus propias tareas.
•    Los administradores tienen la capacidad de ver y gestionar las tareas de todos los usuarios.
Gestión de usuarios (solo para administradores):
•    Los administradores deben poder listar todos los usuarios registrados, así como editar y eliminar usuarios.
Funcionalidades adicionales:
•    Historial de cambios: Mantener un registro de los cambios realizados a las tareas (creación, edición, cambio de estado).
•    Marcar tareas como “Favoritas”: Los usuarios pueden marcar algunas tareas como favoritas para resaltarlas en la lista.

y como tal tengo esto:

Folder Structure:

To-do-List/
├─ .gitignore
├─ docker-compose.yml
├─ FastAPI/
│   FastAPI/
│   ├─ Dockerfile
│   ├─ requirements.txt
│   └─ app/
│       app/
│       ├─ database.py
│       ├─ config/
│       │   config/
│       │   ├─ database.py
│       │   ├─ __init__.py
│       │   └─ settings.py
│       ├─ services/
│       │   services/
│       │   └─ __init__.py
│       ├─ utils/
│       │   utils/
│       │   └─ __init__.py
│       ├─ models/
│       │   models/
│       │   ├─ status.py
│       │   ├─ __init__.py
│       │   ├─ task.py
│       │   ├─ role.py
│       │   ├─ user.py
│       │   └─ change.py
│       ├─ routes/
│       │   routes/
│       │   └─ __init__.py
│       └─ main.py
├─ MySQL/
│   MySQL/
│   └─ Dockerfile
└─ .github/
    .github/
    └─ workflows/
        workflows/
        ├─ deploy.yml
        └─ pylint.yml


File Contents:

File: To-do-List/.gitignore
===========================
Contenido binario o no legible.


File: To-do-List/docker-compose.yml
===================================
services:
  db:
    build: 
      context: ./MySQL
      dockerfile: Dockerfile
    container_name: database
    restart: always
    ports:
      - 3306:3306
    volumes:
      - ./MySQL/volumes:/var/lib/mysql
    hostname: to-do-list-database
    networks:
      - net_to-do-list
    healthcheck:
        test: ["CMD","mysql", "-u", "user", "-proot"]
        interval: 30s
        timeout: 5s
        retries: 5
# --------------------------------------------------------------------
  # - The adminer is a tool for managing content in databases.
  adminer:
    image: adminer
    container_name: adminer-to-do-list
    restart: always
    ports:
      - 8080:8080
    networks:
      - net_to-do-list
# --------------------------------------------------------------------
  # - fastapi refers to the container where the application backend will be stored.
  fastapi:
    build: 
      context: ./FastAPI
      dockerfile: Dockerfile
    container_name: backend
    restart: always
    volumes:
      - ./FastAPI/app:/app 
    ports:
      - "8000:8000"
    command: uvicorn main:app --reload --host 0.0.0.0 --port 80  # Recarga automática con --reload
    depends_on:
      db:
        condition: service_healthy
    networks:
      - net_to-do-list
# --------------------------------------------------------------------

networks:
  net_to-do-list:
      driver: bridge
      name: net_to-do-list



File: To-do-List/FastAPI/Dockerfile
===================================
FROM python:3.12

COPY ./app /app
COPY ./requirements.txt /app

WORKDIR /app

RUN apt-get update && apt-get upgrade -y && apt-get install -y mariadb-client && pip install mysqlclient

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80" ]


File: To-do-List/FastAPI/requirements.txt
=========================================
alembic==1.13.3
annotated-types==0.7.0
anyio==4.6.0
astroid==3.3.5
black==24.10.0
click==8.1.7
dill==0.3.9
fastapi==0.115.0
greenlet==3.1.1
h11==0.14.0
idna==3.10
isort==5.13.2
Mako==1.3.5
MarkupSafe==3.0.1
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==24.1
pathspec==0.12.1
peewee==3.17.6
platformdirs==4.3.6
pydantic==2.9.2
pydantic_core==2.23.4
pylint==3.3.1
PyMySQL==1.1.1
python-dotenv==1.0.1
sniffio==1.3.1
SQLAlchemy==2.0.35
starlette==0.38.6
tomlkit==0.13.2
typing_extensions==4.12.2
uvicorn==0.31.0

passlib==1.7.4
python-jose==3.3.0


File: To-do-List/FastAPI/app/database.py
========================================
"""
Module for database configuration and models.
"""

import os
from dotenv import load_dotenv
from peewee import MySQLDatabase


# Load environment variables from the .env file
load_dotenv()

database = MySQLDatabase(
    os.getenv("MYSQL_DATABASE"),
    user=os.getenv("MYSQL_USER"),
    passwd=os.getenv("MYSQL_PASSWORD"),
    host=os.getenv("MYSQL_HOST"),
    port=int(os.getenv("MYSQL_PORT")),
)



File: To-do-List/FastAPI/app/config/database.py
===============================================
"""
Necessary imports to define data models using Peewee ORM.
- Model: Base class for all models.
- CharField, TextField, DateField, etc.: Database field types.
- ForeignKeyField: Defines relationships between tables.
- Check: Defines column-level constraints.
- AutoField: Auto-incrementing field used as a primary key.
- fn: Provides SQL functions like CURRENT_TIMESTAMP.
"""
from datetime import date
from dotenv import load_dotenv
from settings import DATABASE #pylint: disable=import-error
from peewee import (
    MySQLDatabase, Model, CharField, TextField, DateField,
    ForeignKeyField, BooleanField, TimestampField, Check, AutoField, fn, SQL
)

# Load environment variables from the .env file
load_dotenv()

# Create a MySQL database instance using environment variables
database = MySQLDatabase(
    DATABASE["name"],
    user=DATABASE["user"],
    passwd=DATABASE["password"],
    host=DATABASE["host"],
    port=DATABASE["port"],
)

class RoleModel(Model):
    """Role table."""
    id = AutoField(primary_key=True)
    name = CharField(null=False)

    class Meta:# pylint: disable=too-few-public-methods
        """Meta information for the Role model."""
        table_name = "role"
        doc = "Table that stores user roles."


class StatusModel(Model):
    """Status table with constraints on the name field."""
    id = AutoField(primary_key=True)
    name = CharField(
        null=False,
        constraints=[Check(SQL('"name" IN (\'TO_DO\', \'IN_PROGRESS\', \'COMPLETED\')'))]
    )

    class Meta:# pylint: disable=too-few-public-methods
        """Meta information for the Status model."""
        table_name = "status"
        doc = "Table that defines the different states for tasks."


class UserModel(Model):
    """User table with a relation to Role."""
    id = AutoField(primary_key=True)
    email = CharField(unique=True, null=False)
    password = CharField(null=False)
    role = ForeignKeyField(RoleModel, backref='users', null=True, on_delete='SET NULL')

    class Meta:# pylint: disable=too-few-public-methods
        """Meta information for the User model."""
        table_name = "user"
        doc = "Table that stores user information."


class TaskModel(Model):
    """Task table with relationships to User and Status."""
    id = AutoField(primary_key=True)
    title = CharField(null=False)
    description = TextField(null=True)
    date_of_creation = DateField(default=date.today, null=False)
    expiration_date = DateField(null=True)
    status = ForeignKeyField(StatusModel, backref='tasks', on_delete='CASCADE')
    user = ForeignKeyField(UserModel, backref='tasks', on_delete='CASCADE')
    is_favorite = BooleanField(default=False)

    class Meta:# pylint: disable=too-few-public-methods
        """Meta information for the Task model."""
        table_name = "task"
        doc = "Table that stores tasks and their associated information."
        constraints = [
            Check('expiration_date IS NULL OR expiration_date >= date_of_creation')
        ]


class ChangeModel(Model):
    """Change table to log modifications in Task."""
    id = AutoField(primary_key=True)
    task = ForeignKeyField(TaskModel, backref='changes', on_delete='CASCADE')
    timestamp = TimestampField(default=fn.CURRENT_TIMESTAMP)
    field_changed = CharField(null=False)
    old_value = TextField(null=True)
    new_value = TextField(null=True)

    class Meta:# pylint: disable=too-few-public-methods
        """Meta information for the Change model."""
        table_name = "change"
        doc = "Table that logs changes made to tasks."



File: To-do-List/FastAPI/app/config/__init__.py
===============================================



File: To-do-List/FastAPI/app/config/settings.py
===============================================
"""
This module loads the environment configuration for the application.

Uses dotenv to load environment variables from a .env file.  
Defines the database configuration based on the application's environment.

Variables:
    ENV (str): Specifies the application environment. Defaults to 'dev' if not set.
    DATABASE (dict): Dictionary containing the database configuration.
        - name: Name of the database.
        - engine: Database engine used.
        - user: Username for the database connection.
        - password: Password for the database connection.
        - host: Host address of the database.
        - port: Port used for the database connection.
"""

import os
from dotenv import load_dotenv

# Load environment variables from the .env file.
load_dotenv()

# Set environment to 'dev' by default if 'ENV' is not provided.
ENV = os.getenv("ENV", "dev")

if ENV == "production":
    DATABASE = {
        "name": os.getenv("MYSQL_DATABASE"),
        "engine": "peewee.MySQLDatabase",  # MySQL as the database engine
        "user": os.getenv("MYSQL_USER"),
        "password": os.getenv("MYSQL_PASSWORD"),
        "host": os.getenv("MYSQL_HOST"),
        "port": int(os.getenv("MYSQL_PORT")),
    }
else:
    DATABASE = {
        "name": os.getenv("MYSQL_DATABASE"),
        "engine": "peewee.MySQLDatabase",  # MySQL as the database engine
        "user": os.getenv("MYSQL_USER"),
        "password": os.getenv("MYSQL_PASSWORD"),
        "host": os.getenv("MYSQL_HOST"),
        "port": int(os.getenv("MYSQL_PORT")),
    }



File: To-do-List/FastAPI/app/services/__init__.py
=================================================



File: To-do-List/FastAPI/app/utils/__init__.py
==============================================



File: To-do-List/FastAPI/app/models/status.py
=============================================
"""
This module defines the Status class, which represents a status model for managing 
different statuses that can be assigned to tasks or other entities within an application.

The Status class includes attributes such as the unique identifier and the name of the status.

It is built using Pydantic's `BaseModel` to ensure data validation and type checking.
"""

from pydantic import BaseModel


class Status(BaseModel):
    """
    Represents a status with relevant details.

    Attributes:
    -----------
    id : int
        Unique identifier for the status.
    name : str
        The name of the status (e.g., 'pending', 'completed', 'in progress'),
        describing the current state of an entity.
    """

    id: int
    name: str



File: To-do-List/FastAPI/app/models/__init__.py
===============================================



File: To-do-List/FastAPI/app/models/task.py
===========================================
"""
This module defines the Task class, which represents a task model for handling 
task-related data within an application.

The Task class includes attributes such as title, description, creation date, 
expiration date, and flags for tracking the status and importance of the task.

It is based on the Pydantic `BaseModel` to ensure data validation and type checking.
"""

from datetime import date
from pydantic import BaseModel


class Task(BaseModel):
    """
    Represents a task with relevant details.

    Attributes:
    -----------
    id : int
        Unique identifier for the task.
    title : str
        Title of the task.
    description : str
        Detailed description of the task.
    date_of_creation : date
        The date when the task was created.
    expiration_date : date
        The deadline or expiration date for the task.
    status_id : int
        The status identifier, representing the current state
        of the task (e.g., completed, pending).
    user_id : int
        The identifier of the user associated with the task.
    is_favorite : bool
        Flag indicating whether the task is marked as a favorite.
    """

    id: int
    title: str
    description: str
    date_of_creation: date
    expiration_date: date
    status_id: int
    user_id: int
    is_favorite: bool



File: To-do-List/FastAPI/app/models/role.py
===========================================
"""
This module defines the Role class, which represents a role model used to manage 
different roles or permissions within an application.

The Role class includes attributes such as the role's unique identifier and name.

It is built using Pydantic's `BaseModel` to ensure data validation and type checking.
"""

from pydantic import BaseModel


class Role(BaseModel):
    """
    Represents a role with relevant details.

    Attributes:
    -----------
    id : int
        Unique identifier for the role.
    name : str
        The name of the role (e.g., 'admin', 'user', 'moderator'),
        representing the permissions associated with it.
    """

    id: int
    name: str



File: To-do-List/FastAPI/app/models/user.py
===========================================
"""
This module defines the User class, which represents a user model for managing 
user-related data and authentication within an application.

The User class includes attributes for user credentials, an associated role 
(which is a reference to the Role model), and other relevant user information.

It is built using Pydantic's `BaseModel` to enable data validation and type checking.
"""

from pydantic import BaseModel
from .role import Role  # pylint: disable=relative-beyond-top-level


class User(BaseModel):
    """
    Represents a user with their credentials and associated role.

    Attributes:
    -----------
    id : int
        Unique identifier for the user.
    email : str
        The user's email address, used as their login.
    password : str
        The user's hashed password for authentication.
    role_id : int
        Identifier for the role associated with the user.
    role : Role
        The role object, representing the user's permissions and access levels.
    """

    id: int
    email: str
    password: str
    role_id: int
    role: Role



File: To-do-List/FastAPI/app/models/change.py
=============================================
"""
This module defines the Change class, which represents a model for tracking changes 
made to tasks or other entities within an application.

The Change class captures details about what field was changed, the old value, 
the new value, and when the change occurred.

It is built using Pydantic's `BaseModel` to ensure data validation and type checking.
"""

from datetime import datetime
from pydantic import BaseModel


class Change(BaseModel):
    """
    Represents a record of a change made to a task.

    Attributes:
    -----------
    id : int
        Unique identifier for the change record.
    task_id : int
        Identifier of the task associated with this change.
    timestamp : datetime
        The date and time when the change was made.
    field_changed : str
        The name of the field that was changed (e.g., 'status', 'description').
    old_value : str
        The previous value of the field before the change.
    new_value : str
        The new value of the field after the change.
    """

    id: int
    task_id: int
    timestamp: datetime
    field_changed: str
    old_value: str
    new_value: str



File: To-do-List/FastAPI/app/routes/__init__.py
===============================================



File: To-do-List/FastAPI/app/main.py
====================================
"""
Main module for the FastAPI application.
"""

from contextlib import asynccontextmanager
from fastapi import FastAPI
from starlette.responses import RedirectResponse

# Routers


from database import database as connection


@asynccontextmanager
async def lifespan(api: FastAPI):  # pylint: disable=unused-argument
    """
    Manage the lifespan of the FastAPI application.

    Connects to the database at startup and closes the connection at shutdown.
    """

    # Connect to the database if the connection is closed
    if connection.is_closed():
        connection.connect()
    try:
        yield  # Application code runs here
    finally:
        #  close connection when the aplication is stop
        if not connection.is_closed:
            connection.close()


app = FastAPI(lifespan=lifespan)

# On Startup
# On Shutdown


# Documentation
@app.get("/")
async def docs():
    """
    Redirect to the documentation.
    """
    return RedirectResponse(url="/docs")


# Include routers for events and tickets



File: To-do-List/MySQL/Dockerfile
=================================
FROM mysql:8.0

ENV MYSQL_ROOT_PASSWORD root
ENV MYSQL_DATABASE to-do-list-database
ENV MYSQL_USER user
ENV MYSQL_PASSWORD root

EXPOSE 3306



File: To-do-List/.github/workflows/deploy.yml
=============================================
name: Deploy

on:
  push:
    branches: [ main ]

jobs:
  Deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Build & Deploy
      env:
        PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        HOST_NAME: ${{ secrets.SSH_HOST }}
        USER_NAME: ${{ secrets.USER_NAME }}

      run: |
        # Guardamos la llave privada y le asignamos los permisos correctos
        echo "$PRIVATE_KEY" > private_key.pem && chmod 400 private_key.pem

        # Nos conectamos a la instancia EC2 usando SSH
        ssh -o StrictHostKeyChecking=no -i private_key.pem ${USER_NAME}@${HOST_NAME} '

        # Verificamos si el directorio es un repositorio Git
        if [ ! -d "/home/ubuntu/prod-to-do-list/.git" ]; then
          # Si no es un repositorio Git, clonamos el repositorio
          git clone https://github.com/STXVXN06/To-do-List.git /home/ubuntu/prod-to-do-list;
        fi

        # Comandos para desplegar el contenedor de Docker
        cd /home/ubuntu/prod-to-do-list &&

        # Actualizamos el repositorio
        git checkout main &&
        git fetch --all &&
        git reset --hard origin/main &&
        git pull origin main &&

        # Construimos y ejecutamos el contenedor de Docker
        docker compose down &&
        docker compose build &&
        docker compose up -d
        '


File: To-do-List/.github/workflows/pylint.yml
=============================================
name: Pylint Check

on:
  push:
    branches:
      - "**" # Se ejecuta en todas las ramas
  pull_request:
    branches:
      - "**" # Se ejecuta en pull requests desde todas las ramas

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.12"

      - name: Install dependencies from requirements.txt
        run: |
          python -m pip install --upgrade pip
          pip install -r ${{ github.workspace }}/FastAPI/requirements.txt

      - name: Run Pylint on the whole project
        run: |
          pylint ./**/*.py

      - name: Analysing the code with pylint
        run: |
          if [ ${{ steps.rate.outputs.rate }} -gt 9 ]; then
            pylint $(git ls-files '*.py')
          else
            echo "Rate is not greater than 9. Skipping pylint analysis."
          fi

como tal ya tengo un proyecto finalizado, que los servicios y las rutas las tengo asi:

# app/services/user_service.py

"""
Service layer for User operations.

This module contains the business logic for managing users.
It interacts with the `UserModel` from the database and uses
the `User` Pydantic model for data validation.
"""

from typing import Optional
from peewee import DoesNotExist
from config.database import UserModel, RoleModel
from models.user import User


class UserService:
    """Service layer for User operations"""

    @staticmethod
    def create_user(name: str, email: str, password: str, role_id: int) -> User:
        """
        Create a new user.

        Args:
            name (str): The name of the user.
            email (str): The email of the user.
            password (str): The password of the user.
            role_id (int): The ID of the assigned role.

        Returns:
            User: The created user instance.

        Raises:
            ValueError: If the role with the given ID does not exist.
        """
        try:
            role_instance = RoleModel.get_by_id(role_id)
            user_instance = UserModel.create(
                name=name, email=email, password=password, role=role_instance
            )
            # Using from_orm to convert Peewee model to Pydantic model
            return User.from_orm(user_instance)
        except DoesNotExist as exc:
            raise ValueError(f"Role with id {role_id} not found") from exc

    @staticmethod
    def get_user_by_id(user_id: int) -> Optional[User]:
        """
        Retrieve a user by ID.

        Args:
            user_id (int): The ID of the user to retrieve.

        Returns:
            Optional[User]: The user instance if found, else None.
        """
        try:
            user_instance = UserModel.get_by_id(user_id)
            # Using from_orm to convert Peewee model to Pydantic model
            return User.from_orm(user_instance)
        except DoesNotExist:
            return None

    @staticmethod
    def update_user(
        user_id: int,
        name: Optional[str] = None,
        email: Optional[str] = None,
        password: Optional[str] = None,
        role_id: Optional[int] = None,
    ) -> Optional[User]:
        """
        Update an existing user by ID.

        Args:
            user_id (int): The ID of the user to update.
            name (Optional[str]): The new name of the user.
            email (Optional[str]): The new email of the user.
            password (Optional[str]): The new password of the user.
            role_id (Optional[int]): The new role's ID.

        Returns:
            Optional[User]: The updated user instance if successful, else None.

        Raises:
            ValueError: If the role with the given ID does not exist.
        """
        try:
            user_instance = UserModel.get_by_id(user_id)

            # Update fields only if new values are provided
            if name:
                user_instance.name = name
            if email:
                user_instance.email = email
            if password:
                user_instance.password = password
            if role_id:
                try:
                    role_instance = RoleModel.get_by_id(role_id)
                    user_instance.role = role_instance
                except DoesNotExist as exc:
                    raise ValueError(f"Role with id {role_id} not found") from exc

            user_instance.save()
            return User.from_orm(user_instance)

        except DoesNotExist:
            return None

    @staticmethod
    def delete_user(user_id: int) -> bool:
        """
        Delete a user by ID.

        Args:
            user_id (int): The ID of the user to delete.

        Returns:
            bool: True if the user was deleted, else False.
        """
        try:
            # Check if the user exists before attempting to delete
            user_instance = UserModel.get_by_id(user_id)
            user_instance.delete_instance()
            return True
        except DoesNotExist:
            return False


y esta es la ruta de ese proyecto:

# app/user_routes.py

"""
Module that defines the routes for managing users.

This module uses FastAPI to define the routes that allow
creating, reading, updating, and deleting users through a REST API.

Available routes:

- POST /users/: Creates a new user.
- GET /users/{user_id}: Retrieves user information by ID.
- PUT /users/{user_id}: Updates user information.
- DELETE /users/{user_id}: Deletes a user.

Each route uses the `UserService` to interact with the
business logic related to users.
"""

from services.user_service import UserService
from models.user import User
from fastapi import APIRouter, HTTPException


router = APIRouter(
    prefix="/users",
    tags=["users"],
)


@router.post("/", response_model=User)
def create_user(name: str, email: str, password: str, role_id: int) -> User:
    """
    Create a new user.

    Args:
        name (str): The name of the user.
        email (str): The email of the user.
        password (str): The password of the user.
        role_id (int): The ID of the assigned role.

    Returns:
        User: The created user instance.
    """
    user = UserService.create_user(name, email, password, role_id)
    return user


@router.get("/{user_id}", response_model=User)
def get_user(user_id: int) -> User:
    """
    Retrieve user information by ID.

    Args:
        user_id (int): The ID of the user to retrieve.

    Returns:
        User: The user with the specified ID.

    Raises:
        HTTPException: If the user is not found.
    """
    user = UserService.get_user_by_id(user_id)
    if user:
        return user
    raise HTTPException(status_code=404, detail="User not found")


@router.put("/{user_id}", response_model=User)
def update_user(
    user_id: int, name: str = None, email: str = None, password: str = None, role_id: int = None
) -> User:
    """
    Update user information.

    Args:
        user_id (int): The ID of the user to update.
        name (str, optional): The new name of the user.
        email (str, optional): The new email of the user.
        password (str, optional): The new password of the user.
        role_id (int, optional): The new role's ID.

    Returns:
        User: The updated user instance.

    Raises:
        HTTPException: If the user is not found.
    """
    user = UserService.update_user(user_id, name, email, password, role_id)
    if user:
        return user
    raise HTTPException(status_code=404, detail="User not found")


@router.delete("/{user_id}")
def delete_user(user_id: int) -> dict:
    """
    Delete a user by ID.

    Args:
        user_id (int): The ID of the user to delete.

    Returns:
        dict: A confirmation message if the user was deleted.

    Raises:
        HTTPException: If the user is not found.
    """
    if UserService.delete_user(user_id):
        return {"message": "User deleted successfully"}
    raise HTTPException(status_code=404, detail="User not found")


entonces como quedaria para el rpoyecto nuevo?