.
This commit is contained in:
parent
cfa08ea61d
commit
7551f9d5ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -72,5 +72,6 @@ FACTUGES.FDB
|
|||||||
# ===========================
|
# ===========================
|
||||||
# Otros
|
# Otros
|
||||||
# ===========================
|
# ===========================
|
||||||
|
out
|
||||||
last_execution*.txt
|
last_execution*.txt
|
||||||
*.json
|
*.json
|
||||||
|
|||||||
47
Dockerfile
47
Dockerfile
@ -1,38 +1,31 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
# Usa una imagen base de Python
|
# 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 update
|
||||||
RUN apt-get install libfbclient2 -y
|
RUN apt-get install libfbclient2 -y
|
||||||
|
|
||||||
# Establece el directorio de trabajo dentro del contenedor
|
# Copiamos solo lo necesario para instalar el paquete
|
||||||
WORKDIR /opt/uecko_sync_app
|
COPY pyproject.toml setup.cfg README.md ./
|
||||||
|
COPY app ./app
|
||||||
|
|
||||||
# Copia los archivos del proyecto al contenedor
|
RUN pip install --no-cache-dir .
|
||||||
COPY . .
|
|
||||||
COPY ./.env.production ./.env
|
|
||||||
|
|
||||||
# Instala las dependencias de Python
|
# Copiar enviroment (se sobreescribe en compose)
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
#COPY enviroment/ ./enviroment
|
||||||
|
|
||||||
# Instala cron en el contenedor
|
# Volumen para logs persistentes
|
||||||
RUN apt-get update && apt-get install -y cron nano
|
#VOLUME ["/app/logs"]
|
||||||
|
|
||||||
# Copia el archivo de cron dentro del contenedor
|
# Entrypoint genérico
|
||||||
COPY cronjob /etc/cron.d/cronjob
|
#CMD ["python", "-m", "sync_factuges_main"]
|
||||||
|
CMD ["factuges-sync"]
|
||||||
# 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
|
|
||||||
|
|||||||
126
README.md
Normal file
126
README.md
Normal 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
|
||||||
|
```
|
||||||
@ -1 +1 @@
|
|||||||
2025-11-28 10:37:49
|
2025-11-30 09:07:15
|
||||||
45
app/cli.py
Normal file
45
app/cli.py
Normal 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()
|
||||||
@ -1,48 +1,81 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from os.path import join, dirname
|
from os.path import join, dirname
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def _required(name: str) -> str:
|
||||||
dotenv_path = join(dirname(__file__), '../../.env')
|
"""
|
||||||
load_dotenv(dotenv_path)
|
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'),
|
def load_config() -> Dict[str, Any]:
|
||||||
'FACTUGES_PORT': os.getenv('FACTUGES_PORT'),
|
"""
|
||||||
'FACTUGES_DATABASE': os.getenv('FACTUGES_DATABASE'),
|
Carga la configuración desde variables de entorno.
|
||||||
'FACTUGES_USER': os.getenv('FACTUGES_USER'),
|
|
||||||
'FACTUGES_PASSWORD': os.getenv('FACTUGES_PASSWORD'),
|
|
||||||
|
|
||||||
'UECKO_MYSQL_HOST': os.getenv('UECKO_MYSQL_HOST'),
|
- En dev: carga dev.env y luego valida.
|
||||||
'UECKO_MYSQL_PORT': os.getenv('UECKO_MYSQL_PORT', 3306),
|
- En prod: NO carga ningún .env, solo usa entorno del sistema/contendor.
|
||||||
'UECKO_MYSQL_DATABASE': os.getenv('UECKO_MYSQL_DATABASE'),
|
- Si falta alguna variable requerida -> RuntimeError.
|
||||||
'UECKO_MYSQL_USER': os.getenv('UECKO_MYSQL_USER'),
|
"""
|
||||||
'UECKO_MYSQL_PASSWORD': os.getenv('UECKO_MYSQL_PASSWORD'),
|
|
||||||
|
|
||||||
'CTE_COMPANY_ID': os.getenv('CTE_COMPANY_ID'),
|
env = os.getenv("ENV", "dev")
|
||||||
'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'),
|
|
||||||
|
|
||||||
'VERIFACTU_BASE_URL': os.getenv('VERIFACTU_BASE_URL'),
|
if env == "dev":
|
||||||
'VERIFACTU_API_KEY': os.getenv('VERIFACTU_API_KEY'),
|
dotenv_path = join(dirname(__file__), "../../enviroment/dev.env")
|
||||||
'VERIFACTU_NIFS_API_KEY': os.getenv('VERIFACTU_NIFS_API_KEY'),
|
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'),
|
config: Dict[str, Any] = {
|
||||||
# 'BREVO_EMAIL_TEMPLATE': os.getenv("BREVO_EMAIL_TEMPLATE"),
|
# Opcionales (con valor por defecto)
|
||||||
# 'MAIL_FROM': os.getenv('MAIL_FROM'),
|
"ENV": env,
|
||||||
# 'MAIL_TO': os.getenv('MAIL_TO'),
|
"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
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from config import load_config
|
from app.config import load_config
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
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
|
from striprtf.striprtf import rtf_to_text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from uuid6 import uuid7
|
from uuid6 import uuid7
|
||||||
from config import load_config
|
from app.config import load_config
|
||||||
from . import sql_sentences as SQL
|
from . import sql_sentences as SQL
|
||||||
from . import normalizations as NORMALIZA
|
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):
|
def sync_invoices_factuges(conn_factuges, conn_mysql, last_execution_date):
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
|
||||||
from typing import Dict, Any, Tuple, Optional, List, Iterable
|
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 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
|
from . import sql_sentences as SQL
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from __version_sync_factuges__ import __version__
|
from app.__version_sync_factuges__ import __version__
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
from config import setup_logging, load_config
|
from app.config import setup_logging, load_config
|
||||||
from db import get_mysql_connection, get_factuges_connection, sync_invoices_factuges
|
from app.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.utils import obtener_fecha_ultima_ejecucion, actualizar_fecha_ultima_ejecucion, log_system_metrics, send_orders_mail
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from __version_sync_factuges__ import __version__
|
from app.__version_sync_factuges__ import __version__
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
from config import setup_logging, load_config
|
from config import setup_logging, load_config
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
from __future__ import annotations
|
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"
|
FMT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
@ -14,22 +18,33 @@ def obtener_fecha_ultima_ejecucion(
|
|||||||
) -> datetime:
|
) -> datetime:
|
||||||
"""
|
"""
|
||||||
Lee la última fecha de ejecución desde `path` y la devuelve como aware (UTC).
|
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:
|
try:
|
||||||
with open(path, "r", encoding="utf8") as f:
|
text = path.read_text(encoding="utf-8").strip()
|
||||||
fecha_str = f.read().strip()
|
if not text:
|
||||||
# Se guarda como texto sin tz; interpretamos como UTC
|
# Comentario: fichero vacío -> usamos fallback
|
||||||
dt_naive = datetime.strptime(fecha_str, FMT)
|
return effective_fallback
|
||||||
|
|
||||||
|
dt_naive = datetime.strptime(text, FMT)
|
||||||
return dt_naive.replace(tzinfo=timezone.utc)
|
return dt_naive.replace(tzinfo=timezone.utc)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return fallback
|
return effective_fallback
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Formato inválido en el archivo -> usar fallback
|
# Comentario: formato inválido en el archivo -> fallback
|
||||||
return fallback
|
return effective_fallback
|
||||||
|
|
||||||
|
|
||||||
def actualizar_fecha_ultima_ejecucion(
|
def actualizar_fecha_ultima_ejecucion(
|
||||||
|
|||||||
@ -2,28 +2,29 @@ import logging
|
|||||||
import brevo_python
|
import brevo_python
|
||||||
|
|
||||||
from brevo_python.rest import ApiException
|
from brevo_python.rest import ApiException
|
||||||
from config import setup_brevo
|
from app.config import setup_brevo
|
||||||
from brevo_python.rest import ApiException
|
from brevo_python.rest import ApiException
|
||||||
from config import load_config
|
from app.config import load_config
|
||||||
|
|
||||||
|
|
||||||
def send_orders_mail(inserted_orders):
|
def send_orders_mail(inserted_orders):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
configuration = setup_brevo(config)
|
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(
|
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"]}",
|
subject=f"Nuevo pedido del distribuidor {order["dealer_name"]}",
|
||||||
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
|
template_id=int(config["BREVO_EMAIL_TEMPLATE"]),
|
||||||
params={
|
params={
|
||||||
"customer_reference": order["customer_reference"],
|
"customer_reference": order["customer_reference"],
|
||||||
"dealer_name": order["dealer_name"]
|
"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)
|
logging.info(msg=api_response)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logging.error(msg=e)
|
logging.error(msg=e)
|
||||||
|
|||||||
5
cronjob
5
cronjob
@ -1 +1,6 @@
|
|||||||
*/5 * * * * /usr/local/bin/python /opt/uecko_sync_app/app/main.py >> /var/log/cron.log 2>&1
|
*/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
50
enviroment/dev.env
Normal 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
4
enviroment/prod.env
Normal 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
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
83
readme.md
83
readme.md
@ -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
|
|
||||||
@ -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
113
scripts/build.sh
Executable 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
19
scripts/release.sh
Normal 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
70
setup.cfg
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user