Merge branch 'develop' into feature/frontend-v3

This commit is contained in:
Alexis
2026-04-06 12:38:11 +02:00
8 changed files with 207 additions and 10 deletions
+2
View File
@@ -13,6 +13,7 @@ from api.routers.docmf_validation import router as validation_router
from api.routers.auth import router as auth_router
from api.routers.history import router as history_router
from api.routers.test_mongo import router as test_mongo_router
from api.routers.docit2mf_build import router as docit2mf_router
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -39,3 +40,4 @@ app.include_router(validation_router, prefix="/api/criteria/doc-mf")
app.include_router(test_mongo_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
app.include_router(history_router, prefix="/api")
app.include_router(docit2mf_router, prefix="/api")
+69
View File
@@ -0,0 +1,69 @@
# models/docit2mf_models.py
from pydantic import BaseModel, field_validator
from typing import List, Tuple, Union
BlankCardInput = Union[int, Tuple[int, int], List[int]]
class DoCIT2MFRequest(BaseModel):
term: str
core: Tuple[float, float]
support: Tuple[float, float]
left_nodes_x: List[float]
left_blank_cards: List[BlankCardInput]
right_nodes_x: List[float]
right_blank_cards: List[BlankCardInput]
@field_validator("term")
def term_not_empty(cls, v):
if not v.strip():
raise ValueError("El término no puede estar vacío.")
return v
@field_validator("core")
def core_valid(cls, v):
a, b = v
if a > b:
raise ValueError("El núcleo debe cumplir a <= b.")
return v
@field_validator("support")
def support_valid(cls, v, info):
c, d = v
if c >= d:
raise ValueError("El soporte debe cumplir c < d.")
core = info.data.get("core")
if core:
a, b = core
if not (c <= a <= b <= d):
raise ValueError("El núcleo debe estar dentro del soporte.")
return v
@field_validator("left_blank_cards", "right_blank_cards")
def validate_cards(cls, v):
for item in v:
# Caso 1: entero
if isinstance(item, int):
if item < 0:
raise ValueError("Las cartas no pueden ser negativas.")
# Caso 2: lista o tupla [min,max]
elif isinstance(item, (list, tuple)):
if len(item) != 2:
raise ValueError("Los intervalos deben ser [min, max].")
lo, hi = item
if lo < 0 or hi < 0:
raise ValueError("Las cartas no pueden ser negativas.")
if lo > hi:
raise ValueError("Debe cumplirse min <= max.")
else:
raise ValueError("Formato inválido para cartas blancas.")
return v
class DoCIT2MFMultiRequest(BaseModel):
levels: List[DoCIT2MFRequest]
+1
View File
@@ -45,3 +45,4 @@ class DoCMFRequest(BaseModel):
class DoCMFMultiRequest(BaseModel):
levels: List[DoCMFRequest]
+21 -4
View File
@@ -1,8 +1,12 @@
from typing import List, Optional
from typing import List, Optional, Union
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
# -----------------------------
# MODELOS DE FUNCIONES DIFUSAS
# -----------------------------
class FuzzyTerm(BaseModel):
term: str
core: List[float]
@@ -11,18 +15,32 @@ class FuzzyTerm(BaseModel):
right_nodes: List[List[float]]
class IT2FuzzyTerm(BaseModel):
term: str
lower: FuzzyTerm
upper: FuzzyTerm
# -----------------------------
# HISTORIAL
# -----------------------------
class HistoryItem(BaseModel):
id: Optional[str] = Field(default=None, alias="_id")
name: str
created_at: datetime
results: List[FuzzyTerm]
results: List[Union[FuzzyTerm, IT2FuzzyTerm]]
class HistoryCreateRequest(BaseModel):
name: str
results: List[FuzzyTerm]
results: List[Union[FuzzyTerm, IT2FuzzyTerm]]
# -----------------------------
# USUARIOS
# -----------------------------
class UserCreate(BaseModel):
username: str
email: EmailStr
@@ -41,4 +59,3 @@ class UserInDB(BaseModel):
password_hash: str
token: Optional[str] = None
history: List[HistoryItem] = []
+24
View File
@@ -0,0 +1,24 @@
# routers/docit2mf_build.py
from fastapi import APIRouter, Depends, HTTPException
from api.models.docit2mf_models import DoCIT2MFMultiRequest
from api.services.docit2mf_build_service import build_it2mf_from_level
from api.utils.security import get_current_user
router = APIRouter(prefix="/criteria", tags=["criteria"])
@router.post("/doc-it2mf/build")
async def build_doc_it2mf(
request: DoCIT2MFMultiRequest,
current_user: dict = Depends(get_current_user)
):
results = []
try:
for level in request.levels:
results.append(build_it2mf_from_level(level))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"levels": results}
+3 -2
View File
@@ -3,7 +3,7 @@ from datetime import datetime
from bson import ObjectId
from api.database.mongodb import users_collection
from api.models.user_models import FuzzyTerm, HistoryCreateRequest
from api.models.user_models import FuzzyTerm, IT2FuzzyTerm, HistoryCreateRequest
from api.utils.security import get_current_user
router = APIRouter(prefix="/history", tags=["history"])
@@ -21,7 +21,7 @@ async def add_history_item(
"_id": history_item_id,
"name": data.name,
"created_at": datetime.utcnow(),
"results": [r.dict() for r in data.results],
"results": [r.dict() for r in data.results], # ahora soporta IT2MF
}
await users_collection.update_one(
@@ -35,6 +35,7 @@ async def add_history_item(
}
@router.delete("/delete/{history_item_id}")
async def delete_history_item(
history_item_id: str,
@@ -0,0 +1,61 @@
# services/docit2mf_build_service.py
from typing import List, Union
from api.models.docit2mf_models import DoCIT2MFRequest
from api.models.docmf_models import DoCMFRequest
from api.services.docmf_build_service import build_doc_mf_level
def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> List[int]:
"""
Devuelve una lista de enteros:
- Si el valor es un entero → se usa tal cual para LMF y UMF
- Si es un intervalo [min,max] → se usa min o max según mode
"""
result = []
for item in values:
if isinstance(item, int):
# valor fijo → mismo para LMF y UMF
result.append(item)
else:
lo, hi = item
result.append(lo if mode == "min" else hi)
return result
def build_it2mf_from_level(level: DoCIT2MFRequest):
# LMF (mínimos)
left_min = _extract_bounds(level.left_blank_cards, "min")
right_min = _extract_bounds(level.right_blank_cards, "min")
lower_level = DoCMFRequest(
term=level.term,
core=level.core,
support=level.support,
left_nodes_x=level.left_nodes_x,
left_blank_cards=left_min,
right_nodes_x=level.right_nodes_x,
right_blank_cards=right_min,
)
lower = build_doc_mf_level(lower_level)
# UMF (máximos)
left_max = _extract_bounds(level.left_blank_cards, "max")
right_max = _extract_bounds(level.right_blank_cards, "max")
upper_level = DoCMFRequest(
term=level.term,
core=level.core,
support=level.support,
left_nodes_x=level.left_nodes_x,
left_blank_cards=left_max,
right_nodes_x=level.right_nodes_x,
right_blank_cards=right_max,
)
upper = build_doc_mf_level(upper_level)
return {
"term": level.term,
"lower": lower,
"upper": upper
}
+25 -3
View File
@@ -1,4 +1,10 @@
def build_single_docmf(request):
# services/docmf_build_service.py
from api.models.docmf_models import DoCMFRequest
from api.models.user_models import FuzzyTerm
def build_single_docmf(request: DoCMFRequest):
a, b = request.core
c, d = request.support
@@ -12,7 +18,7 @@ def build_single_docmf(request):
if i == 0:
left_nodes.append((x, 0.0))
else:
acc += request.left_blank_cards[i-1] + 1
acc += request.left_blank_cards[i - 1] + 1
left_nodes.append((x, round(acc * YL, 4)))
# RIGHT
@@ -25,7 +31,7 @@ def build_single_docmf(request):
if i == 0:
right_nodes.append((x, 1.0))
else:
acc += request.right_blank_cards[i-1] + 1
acc += request.right_blank_cards[i - 1] + 1
right_nodes.append((x, round(1 - acc * YR, 4)))
return {
@@ -43,3 +49,19 @@ def build_docmf_multi(request):
result = build_single_docmf(level)
results.append(result)
return {"results": results}
def build_doc_mf_level(level: DoCMFRequest) -> FuzzyTerm:
"""
Adaptador para reutilizar build_single_docmf con el modelo DoCMFRequest.
Devuelve un FuzzyTerm, que es lo que espera el sistema IT2MF.
"""
result = build_single_docmf(level)
return FuzzyTerm(
term=result["term"],
core=list(result["core"]),
support=list(result["support"]),
left_nodes=[list(p) for p in result["left_nodes"]],
right_nodes=[list(p) for p in result["right_nodes"]],
)