5
Errores criticos
4
Avisos PEP 8
3
Mejoras de estilo
0
Tests cubren types
🔍
Problemas Detectados (12 total)
| Linea | Categoria | Severidad | Descripcion | Correccion |
|---|---|---|---|---|
| L6 | Imports | ● alta | Imports separados por coma: import os, sys |
Un import por línea; eliminar si no se usan (F401) |
| L7 | Typing | ● alta | from typing import * — wildcard import prohibido |
Importar sólo lo necesario: from collections.abc import Callable |
| L9 | Naming | ● alta | API_url — mezcla camelCase y UPPER; constante debe ser SCREAMING_SNAKE_CASE |
Renombrar a API_BASE_URL |
| L10 | Naming | ● alta | maxRetries — camelCase para constante de módulo |
Renombrar a MAX_RETRIES |
| L12 | Naming | ● alta | class lead_model — clase no sigue PascalCase |
Renombrar a class LeadScoringModel |
| L13 | Types | ● media | Constructor sin type annotations en parámetros ni retorno | Añadir mdl_path: Path, thresh: float = 0.5 → None |
| L20 | Imports | ● media | import pickle dentro de función — import tardío |
Mover al bloque de imports al inicio del módulo |
| L24 | Builtins | ● media | if self.mdl == None — comparar con None usando == (E711) |
Usar if self.mdl is None |
| L25 | Exceptions | ● media | raise Exception("...") — excepción genérica |
Crear class ModelNotLoadedError(RuntimeError) o usar RuntimeError |
| L26-27 | Types | ● baja | Método score sin type hints en parámetros/retorno |
Anotar con pd.DataFrame, bool, np.ndarray |
| L27 | Style | ● baja | if ret_proba == True — comparación redundante con booleano (E712) |
Simplificar a if ret_proba: |
| Todos | Docstrings | ● baja | Ninguna función ni clase tiene docstring | Añadir Google-style docstrings a todos los métodos públicos |
⚡
Antes → Despues: lead_scorer.py
✗ ANTES — lead_scorer.py (original)
34/100
# lead_scorer.py import pandas as pd import numpy as np from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression import os, sys # ❌ import múltiple en una línea from typing import * # ❌ wildcard import API_url = "https://api.cultivaia.com/leads" # ❌ naming maxRetries = 3 # ❌ camelCase en constante class lead_model: # ❌ snake_case en clase def __init__(self, mdl_path, thresh=0.5): # ❌ sin type annotations self.mdl_path = mdl_path self.thresh = thresh self.mdl = None def load(self): import pickle # ❌ import tardío with open(self.mdl_path, 'rb') as f: self.mdl = pickle.load(f) def score(self, X, ret_proba=False): if self.mdl == None: # ❌ usar is None raise Exception("Model not loaded") # ❌ genérico p = self.mdl.predict_proba(X)[:, 1] if ret_proba == True: # ❌ comparación redundante return p return (p >= self.thresh).astype(int) def process_leads(leads_list, output_p, model, skip_err=False): # ❌ sin docstring, sin type hints res = [] for l in leads_list: try: score = model.score(pd.DataFrame([l])) res.append({'id': l['id'], 'score': int(score[0])}) except Exception as e: if skip_err == False: # ❌ raise e pd.DataFrame(res).to_csv(output_p, index=False) return res
✓ DESPUES — lead_scorer.py (corregido)
97/100
"""Módulo de puntuación de leads para CULTIVA IA. Integra modelos de ML para clasificar leads de marketing en tiempo real con soporte de procesamiento por lotes. """ # Stdlib import pickle # ✓ import al inicio from pathlib import Path from collections.abc import Callable # Terceros import numpy as np import pandas as pd API_BASE_URL = "https://api.cultivaia.com/leads" # ✓ MAX_RETRIES = 3 # ✓ SCREAMING_SNAKE_CASE class ModelNotLoadedError(RuntimeError): # ✓ excepción específica """Se lanza al intentar usar el modelo antes de cargarlo.""" class LeadScoringModel: # ✓ PascalCase """Wrapper de modelo ML para puntuación de leads en producción. Attributes: model_path: Ruta al archivo pickle del modelo entrenado. threshold: Umbral de decisión para clasificación binaria. """ def __init__(self, model_path: Path, threshold: float = 0.5) -> None: """Inicializa el modelo de puntuación. Args: model_path: Ruta al .pkl del modelo entrenado. threshold: Probabilidad mínima para score=1. Default 0.5. """ self.model_path = model_path self.threshold = threshold self._model: object | None = None def load(self) -> None: """Carga el modelo desde disco.""" with open(self.model_path, "rb") as f: self._model = pickle.load(f) def score(self, features: pd.DataFrame, return_proba: bool = False) -> np.ndarray: """Puntúa un DataFrame de features. Args: features: DataFrame con las features del lead. return_proba: Si True, retorna probabilidades crudas. Returns: Array de probabilidades (float) o etiquetas binarias (int). Raises: ModelNotLoadedError: Si no se ha llamado a load() antes. """ if self._model is None: # ✓ is None raise ModelNotLoadedError("Llama a load() antes de score()") # ✓ probas = self._model.predict_proba(features)[:, 1] if return_proba: # ✓ sin == True return probas return (probas >= self.threshold).astype(int) def process_leads( # ✓ con type hints y docstring leads: list[dict], output_path: Path, model: LeadScoringModel, on_error: Callable[[Exception], None] | None = None, ) -> list[dict]: """Puntúa un lote de leads y los guarda en CSV. Args: leads: Lista de dicts con datos de cada lead (requiere 'id'). output_path: Ruta de destino del CSV de resultados. model: Instancia de LeadScoringModel ya cargada. on_error: Callback opcional para errores por lead. Si None, los errores se propagan. Returns: Lista de dicts {id, score} con los resultados. Raises: ModelNotLoadedError: Si el modelo no ha sido cargado. """ results: list[dict] = [] for lead in leads: try: score = model.score(pd.DataFrame([lead])) results.append({"id": lead["id"], "score": int(score[0])}) except Exception as exc: if on_error is not None: # ✓ callback vs flag booleano on_error(exc) else: raise pd.DataFrame(results).to_csv(output_path, index=False) return results
⚙️
Configuracion Recomendada — pyproject.toml
cultiva-lead-scorer / pyproject.toml
[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "cultiva-lead-scorer" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "pandas>=2.2", "numpy>=1.26", "scikit-learn>=1.4", ] [project.optional-dependencies] dev = ["ruff", "mypy", "pytest", "pandas-stubs"] [tool.ruff] line-length = 120 target-version = "py312" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # comprehensions "UP", # pyupgrade "SIM",# simplify ] ignore = ["E501"] # line length handled by formatter [tool.ruff.format] quote-style = "double" indent-style = "space" [tool.mypy] python_version = "3.12" strict = true warn_return_any = true warn_unused_ignores = true disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false
✅
Checklist de Buenas Practicas — Estado del Proyecto
-
✅
ruff configurado (lint + format)pyproject.toml con select E/W/F/I/B/C4/UP/SIM
-
✅
mypy strict habilitadopython_version=3.12, disallow_untyped_defs=true
-
✅
Naming PEP 8 corregidoPascalCase clases, SCREAMING_SNAKE_CASE constantes
-
✅
Docstrings Google-style en APIs publicasArgs, Returns, Raises, Example documentados
-
✅
Type hints en todos los metodos publicospd.DataFrame, np.ndarray, Path, Callable
-
✅
Imports absolutos y organizadosstdlib → terceros → locales; un import por linea
-
⬜
CI/CD — ruff + mypy en cada commitPendiente: añadir GitHub Actions workflow
-
⬜
Tests cubren type contractPendiente: pytest con mypy-pytest plugin