Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d0a5d6e
para exportar la base de datos SQLite a un archivo SQL compatible con…
HeilyMadelay-hub Jun 16, 2025
7bdd093
para verificar la estructura y contenido de la base de datos SQLite.
HeilyMadelay-hub Jun 16, 2025
566c613
Crear la carpeta para los test de migracion
HeilyMadelay-hub Jun 16, 2025
25248ea
para exportar la base de datos SQLite a un archivo SQL compatible con…
HeilyMadelay-hub Jun 16, 2025
ca7490f
Actualiza configuración de base de datos, modelos y README; elimina a…
HeilyMadelay-hub Jul 15, 2025
7192369
WIP: cambios en database.py y exceptions.py
HeilyMadelay-hub Jul 15, 2025
5bcffd1
Merge branch 'migracion-mysql-pt-46' into migracion-mysql
HeilyMadelay-hub Jul 15, 2025
6ea2a6e
Testear los modelos antes de la migración
HeilyMadelay-hub Jul 26, 2025
5a628e5
crear archivo utilidad
HeilyMadelay-hub Jul 26, 2025
69e3695
Actualizado variables que no se deben subir
HeilyMadelay-hub Jul 26, 2025
43699d8
Documentacion y apuntes que voy haciendo
HeilyMadelay-hub Jul 26, 2025
63e33cc
test: update test_config_engine.py
HeilyMadelay-hub Jul 26, 2025
53857a3
Los atributos del git
HeilyMadelay-hub Jul 26, 2025
a6e7764
chore: remove old SQLite backups, scripts and unused images
HeilyMadelay-hub Jul 26, 2025
9ae84bf
fix: update database configuration files
HeilyMadelay-hub Jul 26, 2025
bfd243c
feat: add new utils scripts and test files
HeilyMadelay-hub Jul 26, 2025
f0681b2
Añadido test de desarrollo de pythonnowhere
HeilyMadelay-hub Jul 27, 2025
2230d20
Añadido documentacion de test de desarrollo de pythonnowhere
HeilyMadelay-hub Jul 27, 2025
e6cee7a
Añadido documentacion de test de desarrollo de pythonnowhere actuali…
HeilyMadelay-hub Jul 27, 2025
180dfb2
Terminado de testear la carpeta config
HeilyMadelay-hub Jul 27, 2025
5b3aecc
Backups de los modelos
HeilyMadelay-hub Jul 27, 2025
b2adc17
Comentando porque hemos corregido la clase pet de esa manera
HeilyMadelay-hub Jul 27, 2025
b860638
Documentacion de lo que significa cada linea a la hora de pasar a sql…
HeilyMadelay-hub Jul 30, 2025
050efb2
Listo para el testing
HeilyMadelay-hub Jul 30, 2025
92243d2
Listo para el testing
HeilyMadelay-hub Jul 30, 2025
4260d64
Documentacion de lo que estoy aprendiendo a testear modelos
HeilyMadelay-hub Jul 30, 2025
bcd4ec1
test modelos echos
HeilyMadelay-hub Sep 6, 2025
15f7085
test modelos echos
HeilyMadelay-hub Sep 6, 2025
cf40e70
test modelos echos
HeilyMadelay-hub Sep 6, 2025
21080c8
test modelos echos
HeilyMadelay-hub Sep 6, 2025
9e73505
test modelos echos
HeilyMadelay-hub Sep 6, 2025
48f32aa
test modelos echos
HeilyMadelay-hub Sep 6, 2025
79c33d9
test modelos echos
HeilyMadelay-hub Sep 6, 2025
766708e
test modelos echos
HeilyMadelay-hub Sep 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .env.example

This file was deleted.

10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Normaliza finales de línea a LF en el repo
* text=auto eol=lf

# Archivos que siempre deben ser binarios
*.png binary
*.jpg binary
*.exe binary

# Archivos que deben mantener CRLF (raro en Python, pero puede haber scripts de Windows)
*.bat text eol=crlf
Binary file modified .gitignore
Binary file not shown.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PetCareManager Backend


# Steps


## Requirements
- make
- docker
Expand All @@ -19,3 +23,10 @@
make urls
~~~


export MYSQL_USER=petcaremysql2
export MYSQL_PASSWORD=TU_CONTRASEÑA
export MYSQL_DATABASE=petcaremysql2
export MYSQL_HOST=petcaremysql2.mysql.pythonanywhere-services.com
export PYTHONANYWHERE=true
export Entorno=desarrollo
Empty file added __init__.py
Empty file.
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file makes the app directory a Python package
68 changes: 46 additions & 22 deletions app/config/database.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
"""
Creation of session for database connection
"""
Mantiene la conexion y sesiones a la base de datos

create_engine usa la URL y config de tu clase para conectar.

sessionmaker crea sesiones para manejar transacciones y consultas.

scoped_session asegura que la sesión sea segura para contextos concurrentes.

get_db es una función generadora típica para frameworks web, para abrir y cerrar sesiones automáticamente.

import os
"""
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.orm import declarative_base, configure_mappers
from sqlalchemy.exc import ArgumentError, InvalidRequestError
from dotenv import load_dotenv
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from app.config.database_config import database_config # Importa la factory de configuración

# Crear instancia de configuración
config = database_config()

load_dotenv()
# Obtener la URL de conexión (MySQL o SQLite según entorno)
DATABASE_URL = config.obtener_conexion_url()

base_dir = os.path.dirname(os.path.realpath(__file__))
database_url = os.getenv("DATABASE_URL", f"sqlite:///{os.path.join(base_dir, '../develop.db')}")
# Obtener la configuración para el engine (pool, echo, etc)
ENGINE_CONFIG = config.obtener_config_engine()

# if os.getenv("TESTING") == "1":
# database_url = "sqlite:///:memory:"
# Crear el engine de SQLAlchemy
engine = create_engine(DATABASE_URL, **ENGINE_CONFIG)

engine = create_engine(database_url, echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Crear la sesión para interacción con la DB
SessionLocal = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
#sessionmaker(...) define cómo crear sesiones configuradas.
#scoped_session(...) garantiza que cada hilo/request use su propia sesión segura.
#La variable SessionLocal es un objeto que cuando se llama crea o retorna la sesión adecuada para ese contexto.

# Crear la clase base para los modelos
Base = declarative_base()

"""

Base = lista de tus tablas.

ORM = traductor entre Python y SQL.

Alembic = aplica cambios (migraciones) usando la info de Base.

💥 Si no defines Base, nada de esto funciona.

"""

def get_db():
"""Provides a database session for dependency injection."""
"""
Dependency para usar en frameworks (FastAPI, Flask, etc)
Genera una sesión para cada request y la cierra después.
"""
db = SessionLocal()
try:
yield db
yield db # Devuelve la sesión para el request actual
finally:
db.close()

try:
configure_mappers()
except (ArgumentError, InvalidRequestError) as e:
print(f"Error configuring mappers: {e}")
db.close() # Esta línea cierra la sesión después del request
255 changes: 255 additions & 0 deletions app/config/database_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import os
from typing import Dict,Any
from .exceptions import (
DatabaseConfigError,
MissingConfigurationError,
InvalidConfigurationError,
ConnectionConfigError,
PoolConfigError,
EnvironmentError
)
from .enums import Entorno
from dotenv import load_dotenv

class DatabaseConfig:
"""
Clase para manejar la configuracion de base de datos
"""
REQUIRED_VARS = [

"MYSQL_USER",
"MYSQL_PASSWORD",
"MYSQL_DATABASE",
]

DEFAULT_VALUES = {
"MYSQL_HOST": "petcaremysql.mysql.pythonanywhere-services.com",
"MYSQL_PORT": "3306",
"MYSQL_CHARSET": "utf8mb4",
"USE_MYSQL": "true",
#Un pool de conexiones es un conjunto de conexiones abiertas a la base de datos que se mantienen disponibles para que las aplicaciones las reutilicen, en lugar de abrir y cerrar conexiones repetidamente
"MYSQL_POOL_SIZE": 10, # Suficiente para MVP
"MYSQL_MAX_OVERFLOW": 40, # 4x pool_size para picos
"MYSQL_POOL_TIMEOUT": 30, # 30 segundos es razonable
"MYSQL_POOL_RECYCLE": 3600, # Reciclar cada hora
"POOL_PRE_PING": True # Verificar conexiones
}

def __init__(self, cargar_dotenv=True):
"""Inicializa y valida la configuración"""
if cargar_dotenv: # Cargar variables de entorno desde .env si se indica
load_dotenv()
self.config = {} # Diccionario para almacenar la configuración
self.cargar_config()
self.validar_config()

def cargar_config(self) -> None:

# Primero, cargar valores requeridos
for var in self.REQUIRED_VARS:
self.config[var] = os.environ.get(var) # environ.get(var) obtiene el valor de la variable de entorno, o None si no existe

# Luego, cargar valores con defaults
for var, default in self.DEFAULT_VALUES.items():
self.config[var] = os.environ.get(var, default)

def validar_config(self) -> None:
self._validar_variables_requeridas()
self._validar_parametros_conexion()
self._validar_config_pool()

# Método privados solo para uso interno de la clase _
def _validar_variables_requeridas(self):
"""Valida que existan todas las variables requeridas"""

# Verificar si faltan variables requeridas a partir del diccionario de configuración
variables_inexistentes = [ # Se crea una lista con los nombres de las variables requeridas que no están definidas.Esto es una lista por comprensión creas listas a patir de un for con opc una condición
var for var in self.REQUIRED_VARS # Se recorre cada variable requerida definida en self.REQUIRED_VARS
if not self.config.get(var) # Se verifica si la variable no existe o tiene un valor "falsy" en self.config (por ejemplo: None, '', 0)
]#"Recorre cada var en self.REQUIRED_VARS, y si self.config.get(var) es falsy (None, '', 0...), entonces añade var a la lista."

# Si hay variables que faltan, lanzar excepción
if variables_inexistentes: # Si la lista no está vacía, significa que hay variables faltantes
raise MissingConfigurationError(variables_inexistentes) # Se lanza una excepción personalizada con la lista de variables faltantes

def _validar_parametros_conexion(self) -> None:
"""Valida los parámetros de conexión"""
# Validar host
host = self.config.get("MYSQL_HOST")
if host and (";" in host or "'" in host or '"' in host):
raise ConnectionConfigError("Host contiene caracteres no permitidos")

# Validar puerto
try:
port = int(self.config.get("MYSQL_PORT", "3306"))
if not (1024 <= port <= 65535):
raise ConnectionConfigError(f"Puerto {port} fuera del rango válido (1024-65535)")
except ValueError:
raise InvalidConfigurationError({"MYSQL_PORT": "Debe ser un número válido"})

def _validar_config_pool(self) -> None:
"""Valida la configuración del pool de conexiones"""
try:
pool_size = int(self.config.get("MYSQL_POOL_SIZE", "10"))
max_overflow = int(self.config.get("MYSQL_MAX_OVERFLOW", "20"))
pool_timeout = int(self.config.get("MYSQL_POOL_TIMEOUT", "30"))
pool_recycle = int(self.config.get("MYSQL_POOL_RECYCLE", "3600"))

if pool_size < 1:
raise PoolConfigError("pool_size debe ser mayor que 0")
if max_overflow < 0:
raise PoolConfigError("max_overflow no puede ser negativo")
if pool_timeout < 1:
raise PoolConfigError("pool_timeout debe ser mayor que 0")
if pool_recycle < 60:
raise PoolConfigError("pool_recycle debe ser al menos 60 segundos")

except ValueError as e:
raise InvalidConfigurationError({
"pool_config": f"Valores numéricos inválidos: {str(e)}"
})


def _validar_entorno(self) -> Entorno:
"""Determina y valida el entorno actual basado en variables de entorno"""

# Verificar si existe la variable PYTHONANYWHERE
pythonanywhere_value = os.getenv("PYTHONANYWHERE")

# Verificar si existe la variable Entorno
entorno_value = os.getenv("Entorno")

# Si no existe ninguna variable de entorno, lanzar excepción
if pythonanywhere_value is None and entorno_value is None:
raise MissingConfigurationError(["PYTHONANYWHERE", "Entorno"])

# Priorizar la variable Entorno si está definida porque es más explícita porque define el entorno directamente para el usuario para que no tenga que adivinar si está en PythonAnywhere o no
if entorno_value is not None:
entorno_value = entorno_value.lower().strip()

# Obtener valores válidos directamente del enum Entorno
entornos_validos = [entorno.value for entorno in Entorno]

if entorno_value not in entornos_validos:
raise InvalidConfigurationError({
"Entorno": f"Valor '{entorno_value}' no válido. Valores permitidos: {', '.join(entornos_validos)}"
})

# Buscar y retornar el enum correspondiente
for entorno in Entorno:
if entorno.value == entorno_value:
return entorno

# Si no hay variable Entorno, usar PYTHONANYWHERE
if pythonanywhere_value is not None:
if pythonanywhere_value == "1":
return Entorno.PRODUCCION
elif pythonanywhere_value == "0":
return Entorno.DESARROLLO
else:
raise InvalidConfigurationError({
"PYTHONANYWHERE": f"Valor '{pythonanywhere_value}' no válido. Valores permitidos: '0' (desarrollo) o '1' (producción)"
})

# Si llegamos aquí, hay un problema con la configuración
raise EnvironmentError("No se pudo determinar el entorno. Verifique las variables PYTHONANYWHERE o Entorno")


def obtener_conexion_url(self) -> str:
"""Construye y retorna la URL de conexión basada en el entorno"""
entorno = self.obtener_entorno() # Llama al método para validar el entorno para obtener el entorno actual

# En desarrollo, permitir SQLite si USE_MYSQL es false
if entorno == Entorno.DESARROLLO and self.config.get("USE_MYSQL", "true").lower() == "false": #"Si el entorno es de desarrollo y la variable de configuración USE_MYSQL está definida como 'false' (ignorando mayúsculas), entonces..."
return os.getenv("DATABASE_URL", "sqlite:///./develop.db") # Usa la URL de SQLite definida en el .env o una por defecto

# En cualquier otro caso, usar MySQL
return (
f"mysql://{self.config['MYSQL_USER']}:{self.config['MYSQL_PASSWORD']}"
f"@{self.config['MYSQL_HOST']}:{self.config['MYSQL_PORT']}"
f"/{self.config['MYSQL_DATABASE']}?charset={self.config['MYSQL_CHARSET']}"
)

# Método para obtener la configuración del motor de base de datos
# Este método retorna un diccionario con los parámetros necesarios para crear el engine de SQLAlchemy
# Que es una herramienta que permite interactuar con bases de datos de manera más sencilla para los desarrolladores

#El engine es el núcleo de la conexión a la base de datos
#Funciona como un "motor" que gestiona las conexiones
#Es el puente entre tu código Python y la base de datos MySQL
#¿Qué hace?

#Maneja el pool de conexiones
#Traduce código Python a SQL
#Gestiona las transacciones
#Optimiza el rendimiento
#Analogía: Imagina un hotel:

#El engine es como el sistema de gestión del hotel
#El pool son las habitaciones disponibles
#Las conexiones son como los huéspedes
#El pool_size es el número de habitaciones base
#El max_overflow son habitaciones extra para emergencias

def obtener_config_engine(self) -> Dict[str, Any]:
"""
Retorna la configuración para create_engine.
Este método construye y devuelve un diccionario con los parámetros necesarios
para crear el engine de SQLAlchemy, que es el componente que permite conectar
la aplicación con la base de datos.
Dependiendo del entorno (desarrollo o producción) y si se usa MySQL o SQLite,
se agregan configuraciones específicas como el tamaño del pool de conexiones,
tiempo de espera, etc.
"""

# Determina el entorno actual (DESARROLLO o PRODUCCION) a través del método privado.
entorno = self.obtener_entorno()

# Diccionario base de configuración del engine.
# La clave "echo" activa la impresión de todas las queries SQL en consola,
# lo cual es útil en desarrollo para debug.
config = {
"echo": True if entorno == Entorno.DESARROLLO else False,
}

# Verifica si se está en PRODUCCIÓN o si explícitamente se indicó usar MySQL.
# Esto incluye también el caso en desarrollo donde USE_MYSQL está en "true".
# En ese caso, se agregan parámetros específicos para el pool de conexiones.
if entorno != Entorno.DESARROLLO or self.config.get("USE_MYSQL", "true").lower() == "true":

# Agrega la configuración del pool al diccionario:
config.update({
# Número máximo de conexiones simultáneas mantenidas activas en el pool.
"pool_size": int(self.config.get("MYSQL_POOL_SIZE", "10")),

# Número máximo de conexiones adicionales que se pueden crear temporalmente
# cuando el pool está lleno.
"max_overflow": int(self.config.get("MYSQL_MAX_OVERFLOW", "20")),

# Tiempo máximo en segundos que se espera para obtener una conexión del pool
# antes de lanzar un error.
"pool_timeout": int(self.config.get("MYSQL_POOL_TIMEOUT", "30")),

# Tiempo (en segundos) después del cual una conexión del pool se recicla
# (se cierra y se abre una nueva). Ayuda a evitar desconexiones inactivas.
"pool_recycle": int(self.config.get("MYSQL_POOL_RECYCLE", "3600")),

# Verifica que la conexión esté viva antes de usarla. Evita errores con
# conexiones muertas.
"pool_pre_ping": True
})

# Devuelve el diccionario completo de configuración para el engine.
return config

def obtener_entorno(self) -> Entorno:
# método público para obtener el entorno validado
return self._validar_entorno()

def database_config() -> DatabaseConfig:
"""
Función helper para facilitar el uso. Factory function para crear una instancia de DatabaseConfig
Te da una forma clara y reutilizable de obtener la config de base de datos. Permite encapsular lógica futura si lo necesitás
Mejora la legibilidad y mantenibilidad del código
"""
return DatabaseConfig()
6 changes: 6 additions & 0 deletions app/config/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import Enum

class Entorno(Enum):
DESARROLLO = "desarrollo"
PRUEBAS = "pruebas"
PRODUCCION = "produccion"
Loading