From 85d023acec42a5440b14ad5cb2223f38801e6339 Mon Sep 17 00:00:00 2001 From: dfcarvajal Date: Fri, 12 Dec 2025 12:59:20 +0100 Subject: [PATCH] =?UTF-8?q?Primera=20prueba=20de=20c=C3=B3digo=20de=20micr?= =?UTF-8?q?oservicios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build.yml | 30 ++++++++++++++ .gitea/workflows/test.yml | 8 ++++ Dockerfile | 72 ++++++++++++++++++++++++++++++++ man/supervisord.conf | 23 +++++++++++ src/__init__.py | 0 src/flask.py | 85 ++++++++++++++++++++++++++++++++++++++ src/pyproject.toml | 33 +++++++++++++++ 7 files changed, 251 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/test.yml create mode 100644 Dockerfile create mode 100644 man/supervisord.conf create mode 100644 src/__init__.py create mode 100644 src/flask.py create mode 100644 src/pyproject.toml diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..69ab0ed --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,30 @@ +name: build + +on: + push: + branches: + - main + +jobs: + build: + runs-on: [k8s, kaniko] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Login a Nexus + env: + NEXUS_USER: ${{ secrets.NEXUS_USER }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: | + echo "$NEXUS_PASSWORD" | docker login nexus.rancherk3.duckdns.org -u "$NEXUS_USER" --password-stdin + + - name: Build & Push image with Kaniko + image: gcr.io/kaniko-project/executor:latest + command: + - /kaniko/executor + args: + - --context=. + - --dockerfile=./Dockerfile + - --destination=nexus.rancherk3.duckdns.org/tfm/microserviciospython:latest \ No newline at end of file diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..a92457f --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,8 @@ +name: test +on: [push] + +jobs: + echo: + runs-on: self-hosted + steps: + - run: echo "Gitea Actions funcionando!" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..050779b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# ========================= +# Etapa de Build +# ========================= +FROM python:3.12-slim AS build + +# Instalar dependencias de build mínimas +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3-venv \ + pip \ + && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Crear un entorno virtual Python para build +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Actualizar pip y herramientas de build +RUN pip install --upgrade pip setuptools wheel build pipenv + +# Copiar código y preparar build +ADD src /app +WORKDIR /app + +# Instalar dependencias y construir la rueda +RUN pipenv install --deploy --system || true +RUN python -m build -w + +# ========================= +# Etapa de Runtime +# ========================= +FROM python:3.12-slim + +ARG BUILD_DATE +LABEL org.label-schema.maintainer="Diego Fernández Carvajal (diego.fdezcarvajal@emtmadrid.es)" \ + org.label-schema.build-dockerfile="11/12/2025" \ + org.label-schema.build=$BUILD_DATE + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=Europe/Madrid +ENV PYTHONUNBUFFERED=1 +ENV PATH="/opt/venv/bin:$PATH" + +WORKDIR /app + +# Instalar dependencias mínimas de runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-venv \ + supervisor \ + tzdata \ + locales \ + && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ + && echo "es_ES.UTF-8 UTF-8" > /etc/locale.gen \ + && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \ + && locale-gen \ + && apt-get clean && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Copiar supervisord +COPY /man/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Copiar ruedas desde build +COPY --from=build /app/dist/*.whl /app/ + +# Crear entorno virtual para runtime +RUN python -m venv /opt/venv + +# Instalar ruedas en el entorno virtual +RUN pip install --upgrade pip setuptools wheel +RUN pip install /app/*.whl -t /app + +CMD ["/usr/bin/supervisord","-c","/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/man/supervisord.conf b/man/supervisord.conf new file mode 100644 index 0000000..7b02a24 --- /dev/null +++ b/man/supervisord.conf @@ -0,0 +1,23 @@ +[supervisord] +logfile=/tmp/supervisord.log ; supervisord log file +logfile_maxbytes=10MB ; maximum size of logfile before rotation +logfile_backups=2 ; number of backed up logfiles +loglevel=error ; info, debug, warn, trace +pidfile=/var/run/supervisord.pid ; pidfile location +nodaemon=true ; run supervisord as a daemon +minfds=1024 ; number of startup file descriptors +minprocs=200 ; number of process descriptors +user=root ; default user + +[unix_http_server] +file = /tmp/supervisor.sock ; supervisor unix http server sock file + +[program:microservicios] +command=python3 flask.py +directory=/app +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flask.py b/src/flask.py new file mode 100644 index 0000000..ca0b0a5 --- /dev/null +++ b/src/flask.py @@ -0,0 +1,85 @@ +from flask import Flask, jsonify, request +import os +import psycopg2 +import pandas as pd +import xgboost as xgb +import joblib + +app = Flask(__name__) + +# Configuración de la base de datos +DB_CONFIG = { + 'dbname': os.getenv('DB_NAME', 'postgres'), + 'user': os.getenv('DB_USER', 'postgres'), + 'password': os.getenv('DB_PASSWORD', 'tfmuocdfcarvajal'), + 'host': os.getenv('DB_HOST', '10.10.5.32'), + 'port': os.getenv('DB_PORT', '5432') +} + +# Ruta para entrenar o cargar el modelo +MODEL_PATH = 'modelo_xgb.joblib' + +def get_db_connection(): + conn = psycopg2.connect(**DB_CONFIG) + return conn + +def fetch_data(): + conn = get_db_connection() + query = """ + SELECT h3, hour, dow, total_events, total_nulos, nulo_rate + FROM demanda_h3_hour + """ + df = pd.read_sql(query, conn) + conn.close() + return df + +def train_model(df): + # Variables predictoras + X = df[['hour', 'dow', 'total_events']] + # Variable objetivo: nulo_rate > 0 -> 1, else 0 + y = (df['nulo_rate'] > 0).astype(int) + + model = xgb.XGBClassifier( + max_depth=4, n_estimators=100, learning_rate=0.1, use_label_encoder=False, eval_metric='logloss' + ) + model.fit(X, y) + joblib.dump(model, MODEL_PATH) + return model + +def load_model(): + try: + model = joblib.load(MODEL_PATH) + return model + except: + df = fetch_data() + return train_model(df) + +model = load_model() + +@app.route('/predict', methods=['GET']) +def predict(): + # Parámetros de la consulta + hour = int(request.args.get('hour')) + dow = int(request.args.get('dow')) + total_events = int(request.args.get('total_events', 1)) # valor por defecto si no se pasa + + X_pred = pd.DataFrame([[hour, dow, total_events]], columns=['hour', 'dow', 'total_events']) + prob = model.predict_proba(X_pred)[0][1] # Probabilidad de nulo + return jsonify({ + 'hour': hour, + 'dow': dow, + 'total_events': total_events, + 'predicted_nulo_prob': float(prob) + }) + +@app.route('/demand', methods=['GET']) +def demand(): + df = fetch_data() + X_pred = df[['hour', 'dow', 'total_events']] + df['predicted_nulo_prob'] = model.predict_proba(X_pred)[:,1].astype(float) + # Convertimos a JSON + result = df.to_dict(orient='records') + return jsonify(result) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/src/pyproject.toml b/src/pyproject.toml new file mode 100644 index 0000000..73f8372 --- /dev/null +++ b/src/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "microservicios" +version = "1.0.1" +requires-python = ">= 3.8" +description = "Microservicios para Estimaciones" +license={text = "EULA"} + +authors = [ + {name = "Diego Fernández", email = "carvajal.diego@gmail.com"} +] + +dependencies = [ + 'flask', + 'psycopg2-binary', + 'pandas', + 'xgboost', + 'scikit-learn', + 'joblib' +] + +[tool.setuptools] +include-package-data = true +py-modules = ["flask"] + +[tool.setuptools.packages] +find = {} + +[project.urls] + Documentation='https://https://gitea.tfmuocdfcarvajal.duckdns.org/TFM/microservicios_python' \ No newline at end of file