This commit is contained in:
David Arranz 2025-11-30 10:43:57 +01:00
parent cfa08ea61d
commit 7551f9d5ca
26 changed files with 575 additions and 208 deletions

1
.gitignore vendored
View File

@ -72,5 +72,6 @@ FACTUGES.FDB
# ===========================
# Otros
# ===========================
out
last_execution*.txt
*.json

View File

@ -1,38 +1,31 @@
# syntax=docker/dockerfile:1.4
# Usa una imagen base de Python
FROM python:3.12.6-slim-bookworm AS python_script
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
APP_HOME="/app"
WORKDIR ${APP_HOME}
# Instalar librerías cliente Firebird
RUN apt-get update
RUN apt-get install libfbclient2 -y
# Establece el directorio de trabajo dentro del contenedor
WORKDIR /opt/uecko_sync_app
# Copiamos solo lo necesario para instalar el paquete
COPY pyproject.toml setup.cfg README.md ./
COPY app ./app
# Copia los archivos del proyecto al contenedor
COPY . .
COPY ./.env.production ./.env
RUN pip install --no-cache-dir .
# Instala las dependencias de Python
RUN pip install --no-cache-dir -r requirements.txt
# Copiar enviroment (se sobreescribe en compose)
#COPY enviroment/ ./enviroment
# Instala cron en el contenedor
RUN apt-get update && apt-get install -y cron nano
# Volumen para logs persistentes
#VOLUME ["/app/logs"]
# Copia el archivo de cron dentro del contenedor
COPY cronjob /etc/cron.d/cronjob
# Da permisos de ejecución al archivo cronjob
RUN chmod 0644 /etc/cron.d/cronjob
# Aplica la configuración de cronjob
RUN crontab /etc/cron.d/cronjob
# Crea un archivo log para cron
RUN touch /var/log/cron.log
#RUN mkdir -p /var/log/uecko_sync_app
#RUN touch /var/log/uecko_sync_app/uecko_sync_app.log
# Comando para iniciar cron y mantener el contenedor en ejecución
CMD cron && tail -f /var/log/cron.log
# Entrypoint genérico
#CMD ["python", "-m", "sync_factuges_main"]
CMD ["factuges-sync"]

126
README.md Normal file
View File

@ -0,0 +1,126 @@
# Factuges Sync
Proceso ETL para sincronizar datos desde una base de datos Legacy hacia MariaDB.
Incluye dos modos de sincronización:
- `factuges`
- `verifactu`
En desarrollo **no se utiliza Docker**.
En producción **sí se utiliza Docker + cron**.
---
# 1. Requisitos previos
- Python 3.11 o superior
- Git instalado
- Acceso a las bases de datos necesarias para las pruebas
- (Opcional) Librerías/cliente de Firebird si la BD legacy las requiere
---
# 2. Clonar el repositorio
```bash
git clone <URL_DEL_REPO> factuges-sync
cd factuges-sync
```
# 3. Crear y activar entorno virtual
```bash
python3 -m venv venv
source venv/bin/activate
```
En Windows:
```powershell
venv\Scripts\activate
```
# 4. Instalar dependencias del proyecto
Este proyecto se instala mediante setup.cfg + pyproject.toml.
```bash
pip install --upgrade pip
pip install -e .
```
Esto crea el comando factuges-sync disponible en tu entorno virtual.
# 5. Configurar entorno de desarrollo
El código carga automáticamente:
* environment/dev.env cuando ENV=dev
* Variables del contenedor en producción (ENV=prod)
Por tanto, **en desarrollo**:
```bash
export ENV=dev
```
(En Windows: set ENV=dev)
Revisa/edita:
```bash
environment/dev.env
```
Ejemplo:
```ini
DB_HOST=localhost
DB_USER=root
DB_PASS=123
SYNC_INTERVAL_MINUTES=5
```
# 6. Ejecutar el job en desarrollo
Puedes ejecutar cualquiera de los dos modos.
**Opción A: usando el CLI**
```bash
# Modo FACTUGES
factuges-sync factuges
# Modo VERIFACTU
factuges-sync verifactu
```
**Opción B: llamando directamente a los módulos Python**
```bash
python -m app.sync_factuges_main
python -m app.sync_verifactu_main
```
# 7. Logs en desarrollo
Si el proyecto escribe logs en logs/:
```bash
tail -f logs/job.log
```
# 8. Producción (resumen)
En producción se usa Docker y cron.
Build:
```bash
./scripts/build.sh <company>
```
Cron job:
```bash
*/5 * * * * docker run --rm -e ENV=prod -e SYNC_MODE=factuges myimage:latest
```

View File

@ -1 +1 @@
2025-11-28 10:37:49
2025-11-30 09:07:15

45
app/cli.py Normal file
View File

@ -0,0 +1,45 @@
# app/cli.py
# CLI simple para seleccionar el entrypoint correcto.
# Permite:
# factuges-sync factuges
# factuges-sync verifactu
# SYNC_MODE=factuges factuges-sync
import argparse
import os
import subprocess
import sys
def main():
"""Selector de modos de sincronización."""
parser = argparse.ArgumentParser(description="Factuges Sync Dispatcher")
parser.add_argument(
"mode",
nargs="?",
choices=["factuges", "verifactu"],
help="Modo de sincronización",
)
args = parser.parse_args()
mode = args.mode or os.getenv("SYNC_MODE")
if mode not in ("factuges", "verifactu"):
print("Error: debes indicar modo: 'factuges' o 'verifactu'")
sys.exit(1)
if os.getenv("ENV") == "dev":
print("Running in development mode (no docker)")
module = (
"app.sync_factuges_main"
if mode == "factuges"
else "app.sync_verifactu_main"
)
# Ejecuta el módulo Python correspondiente
subprocess.run([sys.executable, "-m", module], check=True)
if __name__ == "__main__":
main()

View File

@ -1,48 +1,81 @@
import logging
import os
from os.path import join, dirname
from typing import Any, Dict, Optional
from dotenv import load_dotenv
def load_config():
dotenv_path = join(dirname(__file__), '../../.env')
load_dotenv(dotenv_path)
def _required(name: str) -> str:
"""
Devuelve el valor de la variable de entorno `name`.
Lanza RuntimeError si no existe o está vacía.
"""
value = os.getenv(name)
if value is None or value == "":
# Comentario: fail fast con mensaje claro
raise RuntimeError(f"Missing required environment variable: {name}")
return value
return {
'ENVIRONMENT': os.getenv('ENVIRONMENT'),
'LOCAL_TZ': os.getenv('LOCAL_TZ', 'Europe/Madrid'),
'LAST_RUN_PATH': os.getenv('LAST_RUN_PATH'),
# 'LOG_PATH': os.getenv('LOG_PATH', 'app.log'),
'FACTUGES_HOST': os.getenv('FACTUGES_HOST'),
'FACTUGES_PORT': os.getenv('FACTUGES_PORT'),
'FACTUGES_DATABASE': os.getenv('FACTUGES_DATABASE'),
'FACTUGES_USER': os.getenv('FACTUGES_USER'),
'FACTUGES_PASSWORD': os.getenv('FACTUGES_PASSWORD'),
def load_config() -> Dict[str, Any]:
"""
Carga la configuración desde variables de entorno.
'UECKO_MYSQL_HOST': os.getenv('UECKO_MYSQL_HOST'),
'UECKO_MYSQL_PORT': os.getenv('UECKO_MYSQL_PORT', 3306),
'UECKO_MYSQL_DATABASE': os.getenv('UECKO_MYSQL_DATABASE'),
'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'),
'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'),
- En dev: carga dev.env y luego valida.
- En prod: NO carga ningún .env, solo usa entorno del sistema/contendor.
- Si falta alguna variable requerida -> RuntimeError.
"""
'CTE_COMPANY_ID': os.getenv('CTE_COMPANY_ID'),
'CTE_SERIE': os.getenv('CTE_SERIE'),
'CTE_STATUS_INVOICE': os.getenv('CTE_STATUS_INVOICE'),
'CTE_IS_PROFORMA': os.getenv('CTE_IS_PROFORMA'),
'CTE_STATUS_VERIFACTU': os.getenv('CTE_STATUS_VERIFACTU'),
'CTE_LANGUAGE_CODE': os.getenv('CTE_LANGUAGE_CODE'),
'CTE_COUNTRY_CODE': os.getenv('CTE_COUNTRY_CODE'),
'CTE_IS_COMPANY': os.getenv('CTE_IS_COMPANY'),
'CTE_SYNC_RESULT_OK': os.getenv('CTE_SYNC_RESULT_OK'),
'CTE_SYNC_RESULT_FAIL': os.getenv('CTE_SYNC_RESULT_FAIL'),
env = os.getenv("ENV", "dev")
'VERIFACTU_BASE_URL': os.getenv('VERIFACTU_BASE_URL'),
'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'),
'VERIFACTU_NIFS_API_KEY': os.getenv('VERIFACTU_NIFS_API_KEY'),
if env == "dev":
dotenv_path = join(dirname(__file__), "../../enviroment/dev.env")
load_dotenv(dotenv_path)
elif env == "prod":
# En producción NO se carga archivo .env
# Las variables vienen del contenedor (docker run -e VAR=...)
pass
else:
raise RuntimeError(f"Unknown ENV: {env}")
# 'BREVO_API_KEY': os.getenv('BREVO_API_KEY'),
# 'BREVO_EMAIL_TEMPLATE': os.getenv("BREVO_EMAIL_TEMPLATE"),
# 'MAIL_FROM': os.getenv('MAIL_FROM'),
# 'MAIL_TO': os.getenv('MAIL_TO'),
config: Dict[str, Any] = {
# Opcionales (con valor por defecto)
"ENV": env,
"ENVIRONMENT": os.getenv("ENVIRONMENT", env),
"LOCAL_TZ": os.getenv("LOCAL_TZ", "Europe/Madrid"),
"LAST_RUN_PATH": _required("LAST_RUN_PATH"),
# FACTUGES (requeridas)
"FACTUGES_HOST": _required("FACTUGES_HOST"),
"FACTUGES_PORT": _required("FACTUGES_PORT"),
"FACTUGES_DATABASE": _required("FACTUGES_DATABASE"),
"FACTUGES_USER": _required("FACTUGES_USER"),
"FACTUGES_PASSWORD": _required("FACTUGES_PASSWORD"),
# UECKO MySQL (requeridas salvo puerto)
"UECKO_MYSQL_HOST": _required("UECKO_MYSQL_HOST"),
"UECKO_MYSQL_PORT": os.getenv("UECKO_MYSQL_PORT", "3306"),
"UECKO_MYSQL_DATABASE": _required("UECKO_MYSQL_DATABASE"),
"UECKO_MYSQL_USER": _required("UECKO_MYSQL_USER"),
"UECKO_MYSQL_PASSWORD": _required("UECKO_MYSQL_PASSWORD"),
# Constantes/CTE (requeridas)
"CTE_COMPANY_ID": _required("CTE_COMPANY_ID"),
"CTE_SERIE": _required("CTE_SERIE"),
"CTE_STATUS_INVOICE": _required("CTE_STATUS_INVOICE"),
"CTE_IS_PROFORMA": _required("CTE_IS_PROFORMA"),
"CTE_STATUS_VERIFACTU": _required("CTE_STATUS_VERIFACTU"),
"CTE_LANGUAGE_CODE": _required("CTE_LANGUAGE_CODE"),
"CTE_COUNTRY_CODE": _required("CTE_COUNTRY_CODE"),
"CTE_IS_COMPANY": _required("CTE_IS_COMPANY"),
"CTE_SYNC_RESULT_OK": _required("CTE_SYNC_RESULT_OK"),
"CTE_SYNC_RESULT_FAIL": _required("CTE_SYNC_RESULT_FAIL"),
# Verifactu (requeridas)
"VERIFACTU_BASE_URL": _required("VERIFACTU_BASE_URL"),
"VERIFACTU_API_KEY": _required("VERIFACTU_API_KEY"),
"VERIFACTU_NIFS_API_KEY": _required("VERIFACTU_NIFS_API_KEY"),
}
return config

View File

@ -1,10 +1,10 @@
import logging
from datetime import date
from config import load_config
from app.config import load_config
import textwrap
from typing import Dict, Any, Optional
from decimal import Decimal, ROUND_HALF_UP
from utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, cents4, money_round, tax_fraction_from_code
from app.utils import limpiar_cadena, normalizar_telefono_con_plus, corregir_y_validar_email, normalizar_url_para_insert, map_tax_code, cents, cents4, money_round, tax_fraction_from_code
from striprtf.striprtf import rtf_to_text

View File

@ -1,10 +1,10 @@
import logging
from typing import Dict, Any
from uuid6 import uuid7
from config import load_config
from app.config import load_config
from . import sql_sentences as SQL
from . import normalizations as NORMALIZA
from utils import validar_nif
from app.utils import validar_nif
def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):

View File

@ -1,9 +1,8 @@
import logging
from uuid import uuid4
from typing import Dict, Any, Tuple, Optional, List, Iterable
from config import load_config
from app.config import load_config
from decimal import Decimal
from utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
from app.utils import validar_nif, estado_factura, crear_factura, TaxCatalog, unscale_to_str
from . import sql_sentences as SQL

View File

@ -1,12 +1,12 @@
import sys
import logging
from __version_sync_factuges__ import __version__
from app.__version_sync_factuges__ import __version__
from datetime import datetime
from dateutil import tz
from config import setup_logging, load_config
from db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges
from utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
from app.config import setup_logging, load_config
from app.db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges
from app.utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
def main():

View File

@ -1,7 +1,7 @@
import sys
import logging
from __version_sync_factuges__ import __version__
from app.__version_sync_factuges__ import __version__
from datetime import datetime
from dateutil import tz
from config import setup_logging, load_config

View File

@ -1,9 +1,13 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
import os
DEFAULT_PATH = "./last_execution.txt"
from datetime import datetime, timezone
from pathlib import Path
import os
from typing import Optional
DEFAULT_PATH = Path("last_execution.txt")
DEFAULT_FALLBACK = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
FMT = "%Y-%m-%d %H:%M:%S"
@ -14,22 +18,33 @@ def obtener_fecha_ultima_ejecucion(
) -> datetime:
"""
Lee la última fecha de ejecución desde `path` y la devuelve como aware (UTC).
Si no existe o hay error de parseo, devuelve `fallback` (por defecto 2024-01-01 UTC).
- Si el fichero no existe o el contenido es inválido, devuelve `fallback`.
- Si `fallback` es None, usa DEFAULT_FALLBACK (2024-01-01T00:00:00Z).
"""
if fallback is None:
fallback = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
# Comentario: fallback explícito para evitar lógica duplicada en llamadas
effective_fallback = fallback or DEFAULT_FALLBACK
if path is None:
return effective_fallback
# 2. Convertimos str -> Path
path = Path(path)
try:
with open(path, "r", encoding="utf8") as f:
fecha_str = f.read().strip()
# Se guarda como texto sin tz; interpretamos como UTC
dt_naive = datetime.strptime(fecha_str, FMT)
text = path.read_text(encoding="utf-8").strip()
if not text:
# Comentario: fichero vacío -> usamos fallback
return effective_fallback
dt_naive = datetime.strptime(text, FMT)
return dt_naive.replace(tzinfo=timezone.utc)
except FileNotFoundError:
return fallback
return effective_fallback
except ValueError:
# Formato inválido en el archivo -> usar fallback
return fallback
# Comentario: formato inválido en el archivo -> fallback
return effective_fallback
def actualizar_fecha_ultima_ejecucion(

View File

@ -2,28 +2,29 @@ import logging
import brevo_python
from brevo_python.rest import ApiException
from config import setup_brevo
from app.config import setup_brevo
from brevo_python.rest import ApiException
from config import load_config
from app.config import load_config
def send_orders_mail(inserted_orders):
config = load_config()
try:
configuration = setup_brevo(config)
api_instance = brevo_python.TransactionalEmailsApi(brevo_python.ApiClient(configuration))
api_instance = brevo_python.TransactionalEmailsApi(brevo_python.ApiClient(configuration))
for order in inserted_orders:
for order in inserted_orders:
send_smtp_email = brevo_python.SendSmtpEmail(
to=[{'email':config['MAIL_TO']}],
to=[{'email': config['MAIL_TO']}],
subject=f"Nuevo pedido del distribuidor {order["dealer_name"]}",
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
params={
"customer_reference": order["customer_reference"],
"customer_reference": order["customer_reference"],
"dealer_name": order["dealer_name"]
},
)
api_response = api_instance.send_transac_email(send_smtp_email)
)
api_response = api_instance.send_transac_email(send_smtp_email)
logging.info(msg=api_response)
except ApiException as e:
logging.error(msg=e)

View File

@ -1 +1,6 @@
*/5 * * * * /usr/local/bin/python /opt/uecko_sync_app/app/main.py >> /var/log/cron.log 2>&1
*/5 * * * * docker run --rm \
--env-file /opt/my_project/config/prod.env \
-v /opt/my_project/logs:/app/logs \
my_project:latest

50
enviroment/dev.env Normal file
View File

@ -0,0 +1,50 @@
ENVIRONMENT = development
LOCAL_TZ = Europe/Madrid
LAST_RUN_PATH = ./app.last_run.txt
#LOG_PATH = ./app.log
#DESARROLLO ACANA
#FACTUGES_HOST = 192.168.0.105
#FACTUGES_PORT = 3050
#FACTUGES_DATABASE = C:\Codigo Acana\Output\Debug\Database\FACTUGES.FDB
#FACTUGES_USER = sysdba
#FACTUGES_PASSWORD = masterkey
#CONFIGURACION ACANA
#CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
#VERIFACTU_API_KEY = vf_test_ei8WYAvEq5dhSdEyQVjgCS8NZaNpEK2BljSHSUXf+Y0=
#DESARROLLO ALONSO Y SAL
FACTUGES_HOST = 192.168.0.146
FACTUGES_PORT = 3050
FACTUGES_DATABASE = C:\Codigo Arribas2\Output\Debug\Database\FACTUGES.FDB
FACTUGES_USER = sysdba
FACTUGES_PASSWORD = masterkey
#CONFIGURACION ACANA
CTE_COMPANY_ID = '019a9667-6a65-767a-a737-48234ee50a3a'
#CTE_COMPANY_ID_PRODUCCION = '019ac4f2-9502-731a-a6db-525475e85bc7'
VERIFACTU_API_KEY = vf_test_C03HL2F0X5OXSDRunjNFoMxD4IrRfK3kCC8PfcvCENI=
#DESARROLLO
UECKO_MYSQL_HOST = localhost
UECKO_MYSQL_PORT = 3306
UECKO_MYSQL_DATABASE = uecko_erp_sync
UECKO_MYSQL_USER = rodax
UECKO_MYSQL_PASSWORD = rodax
CTE_SERIE = 'F25/'
CTE_STATUS_INVOICE = 'issued'
CTE_IS_PROFORMA = 0
CTE_STATUS_VERIFACTU = 'Pendiente'
CTE_LANGUAGE_CODE = 'es' #En uecko vendrá de su ficha
CTE_COUNTRY_CODE = 'es' #En uecko vendrá de su ficha
CTE_IS_COMPANY = 1
CTE_SYNC_RESULT_OK = 1
CTE_SYNC_RESULT_FAIL = 2
VERIFACTU_BASE_URL = https://api.verifacti.com/
VERIFACTU_NIFS_API_KEY = vfn_osYpNdqSzAdTAHpazXG2anz4F3o0gfbSb5FFrCBZcno=
#BREVO_API_KEY = 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
#BREVO_EMAIL_TEMPLATE = 1
#MAIL_FROM = 'no-reply@presupuestos.uecko.com'
#MAIL_TO = 'soporte@rodax-software.com'

4
enviroment/prod.env Normal file
View File

@ -0,0 +1,4 @@
DB_HOST=prod-db
DB_USER=prod_user
DB_PASS=supersecret
RUN_INTERVAL_MINUTES=5

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

View File

@ -1,83 +0,0 @@
Instalar Python en Ubuntu (alias de Python3)
--------------------------------------------
sudo apt-get install python-is-python3
sudo apt install python3.12-venv
Instalar el cliente de FirebirdSQL 2.0/2.1 en Ubuntu
----------------------------------------------------
- Opción 1. Revisar si el cliente Firebird 2.1 (libfbclient2) está en repositorios. Si aparece, se puede instalar directamente:
sudo apt-get update
apt-cache search firebird | grep client
sudo apt-get install libfbclient2
- Opción 2. No aparece en el repositorio de Ubuntu. Instalación manual -> investigar
Crear el entorno por primera vez:
---------------------------------
python -m venv venv
source venv/bin/activate <-- en linux
.\venv\Scripts\activat <-- en Windows
Meter librerias requeridas al entorno creado
--------------------------------------------
pip3 install -r requirements.txt
Lanzar el entorno para hacer pruebas del script:
-----------------------------------------------
Linux:
source venv/bin/activate
python app/main.py
Windows:
.\venv\Scripts\activat
python app\main.py
----
git clone ssh://git@wopr.rodax-software.com:30001/uecko/presupuestador-web---scripts-sync.git uecko-sync-scripts
cd uecko-sync-scripts/
cp .env-sample .env
pip install -r requirements.txt
python3 -m venv env
sudo apt install python3.11-venv
python3 -m venv env
source venv/bin/activate
pip3 install -r requirements.txt
python3 factuges_catalog_to_json_file.py
> Reconstruir imagen docker
docker compose up --build -d
> Instalar Firebird 2.1
1. Descargar paquete: https://master.dl.sourceforge.net/project/firebird/firebird-linux-amd64/2.1.7-Release/FirebirdSS-2.1.7.18553-0.amd64.tar.gz?viasf=1
2. Descomprimir: tar -xvf <paquete>
3. Lanzar instalación: sudo ./install.sh
4. Si da error, da igual. El caso es que en /opt/firebird estén los ficheros y en <lib> las librerías.
5. Crear enlaces simbólicos:
Busque la librería libfbclient.so.2.m.n (m.n es el nro. menor de versión más el nro. de actualización) en /opt/firebird/lib del equipo donde está instalado el servidor Firebird. Cópiela a /usr/lib en el cliente.
Cree enlaces simbólicos usando los siguientes comandos:
ln -s /usr/lib/libfbclient.so.2.m.n /usr/lib/libfbclient.so.2
ln -s /usr/lib/libfbclient.so.2 /usr/lib/libfbclient.so
reemplazando 2.m.n con su número de versión, por ejemplo 2.1.7
Si Ud. está ejecutando aplicaciones que esperan que las librerías antiguas estén presentes, cree también los siguientes enlaces simbólicos:
ln -s /usr/lib/libfbclient.so /usr/lib/libgds.so.0
ln -s /usr/lib/libfbclient.so /usr/lib/libgds.so
Copie el archivo firebird.msg a /opt/firebird
En el perfil por defecto del sistema, o usando setenv() desde una consola, cree la variable de entorno FIREBIRD y apúntela al directorio /opt/firebird, para permitir a las rutinas de la API localizar los mensajes.
Para ello, editar con sudo nano /etc/profile y añadir FIREBIRD=/opt/firebird
6. sudo apt-get install libncurses5

View File

@ -1,27 +0,0 @@
about-time==4.2.1
alive-progress==3.1.5
bcrypt==4.1.3
black==24.8.0
brevo-python==1.1.2
certifi==2024.8.30
cffi==1.16.0
click==8.1.7
colorama==0.4.6
cryptography==42.0.8
fdb==2.0.2
future==0.18.3
grapheme==0.6.0
mypy-extensions==1.0.0
mysql-connector-python==8.4.0
packaging==24.1
paramiko==3.4.0
pathspec==0.12.1
platformdirs==4.3.6
psutil==6.0.0
pycparser==2.22
PyNaCl==1.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.0
six==1.16.0
sshtunnel==0.4.0
urllib3==2.2.3

113
scripts/build.sh Executable file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_VERSION="1.0.0"
# ================================================
# FACTUGES SYNC - Docker Build Script
# -----------------------------------------------
# Uso:
# ./build.sh <company> [--load]
# ================================================
# ---------- 1. Validación de parámetros ----------
if [[ $# -eq 0 || "$1" == --* ]]; then
echo "❌ ERROR: Falta el parámetro <company>"
echo "Uso: ./build.sh <company> [--load]"
exit 1
fi
COMPANY="$1"
LOAD=false
if [[ "${2:-}" == "--load" ]]; then
LOAD=true
fi
# ---------- 2. Directorios ----------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(realpath "${SCRIPT_DIR}/..")"
OUT_DIR="${PROJECT_DIR}/out/${COMPANY}"
mkdir -p "$OUT_DIR"
cd "$PROJECT_DIR"
# ---------- 3. Versión del proyecto ----------
IMAGE_NAME="factuges-sync"
IMAGE_VERSION=$(sed -n 's/^version[[:space:]]*=[[:space:]]*\(.*\)$/\1/p' setup.cfg | head -n1)
IMAGE_VERSION="${IMAGE_VERSION:-0.0.0}"
IMAGE_TAG_VERSION="${IMAGE_NAME}:${COMPANY}-${IMAGE_VERSION}"
IMAGE_TAG_LATEST="${IMAGE_NAME}:${COMPANY}-latest"
# ---------- 4. Info del sistema ----------
DATE=$(date +'%Y%m%d-%H%M%S')
ISO_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
USER_NAME=$(whoami)
echo ""
echo "-------------------------------------------------------"
echo " FACTUGES SYNC Build Script v${SCRIPT_VERSION}"
echo " Compañía: ${COMPANY}"
echo " Version: ${IMAGE_VERSION}"
echo " Imagen: ${IMAGE_TAG_VERSION}"
echo " Latest tag: ${IMAGE_TAG_LATEST}"
echo " Load: ${LOAD}"
echo "-------------------------------------------------------"
echo ""
# ---------- 5. Build de la imagen Docker ----------
echo "📦 Construyendo imagen Docker..."
docker build \
-t "${IMAGE_TAG_VERSION}" \
-t "${IMAGE_TAG_LATEST}" \
.
echo "✔ Build OK: ${IMAGE_TAG_VERSION}"
echo ""
# ---------- 6. Generar manifest JSON ----------
MANIFEST_FILE="${OUT_DIR}/manifest-${IMAGE_VERSION}-${DATE}.json"
cat > "$MANIFEST_FILE" <<EOF
{
"company": "${COMPANY}",
"image_version": "${IMAGE_VERSION}",
"image_tag_version": "${IMAGE_TAG_VERSION}",
"image_tag_latest": "${IMAGE_TAG_LATEST}",
"build_time": "${ISO_DATE}",
"user": "${USER_NAME}"
}
EOF
echo "📄 Manifest generado: ${MANIFEST_FILE}"
echo ""
# ---------- 7. LOAD opcional ----------
if [[ "$LOAD" == true ]]; then
echo "📥 Subiendo imagen al servidor..."
TAR_FILE="${OUT_DIR}/${IMAGE_NAME}-${COMPANY}-latest.tar"
docker save -o "${TAR_FILE}" "${IMAGE_TAG_LATEST}"
scp -P 49152 "${TAR_FILE}" \
rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/
ssh -p 49152 rodax@vps-2.rodax-software.com \
"docker load -i /opt/factuges/${COMPANY}/$(basename "$TAR_FILE")"
echo "✔ Imagen cargada en producción"
fi
# ---------- 8. Resumen ----------
echo ""
echo "-------------------------------------------------------"
echo "🎯 Resultado final para '${COMPANY}'"
echo "✔ Build OK: ${IMAGE_TAG_VERSION}"
if [[ "$LOAD" == true ]]; then
echo "✔ Load OK"
fi
echo "🧩 Script version: ${SCRIPT_VERSION}"
echo "-------------------------------------------------------"
echo ""

19
scripts/release.sh Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
# Comentario: genera el siguiente tag semántico automáticamente
# Formato: major.minor.patch
CURRENT=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
IFS='.' read -r MAJ MIN PAT <<< "$CURRENT"
# Incremento patch por defecto
PAT=$((PAT + 1))
NEXT="${MAJ}.${MIN}.${PAT}"
echo "Creating release tag: ${NEXT}"
git tag "${NEXT}"
git push origin "${NEXT}"

70
setup.cfg Normal file
View File

@ -0,0 +1,70 @@
[metadata]
name = factuges-sync
version = 0.1.0
description = ETL job to sync data from legacy DB to MariaDB
author = Rodax Software
author_email = info@rodax-software.com
long_description = file: README.md
long_description_content_type = text/markdown
url = https://factuges.app
license = "Propietaria"
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3.11
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
packages = find:
package_dir =
= .
python_requires = >=3.11
install_requires =
about-time==4.2.1
alive-progress==3.1.5
bcrypt==4.1.3
brevo-python==1.1.2
certifi==2024.8.30
cffi==1.16.0
click==8.1.7
colorama==0.4.6
cryptography==42.0.8
fdb==2.0.2
future==0.18.3
grapheme==0.6.0
mysql-connector-python==8.4.0
paramiko==3.4.0
psutil==6.0.0
pycparser==2.22
PyNaCl==1.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.0
six==1.16.0
sshtunnel==0.4.0
urllib3==2.2.3
uuid6
[options.extras_require]
dev =
black==24.8.0
mypy-extensions==1.0.0
packaging==24.1
pathspec==0.12.1
platformdirs==4.3.6
[options.entry_points]
console_scripts =
factuges-sync = app.cli:main
[options.packages.find]
where = .
[tool:pytest]
testpaths = tests
[flake8]
max-line-length = 88
exclude = .git,__pycache__,build,dist
[isort]
profile = black