full adjustment
This commit is contained in:
113
README.md
113
README.md
@@ -27,6 +27,115 @@ una vez importado el model, lo lee, lo pasa por la logica en la que se descratan
|
|||||||
|
|
||||||
`notas de Gerardo`
|
`notas de Gerardo`
|
||||||
se quedaron implementadas los servicios crud de las funciones que seran publicos, ajustados a el tipo de usuario ('Root', 'Admin')
|
se quedaron implementadas los servicios crud de las funciones que seran publicos, ajustados a el tipo de usuario ('Root', 'Admin')
|
||||||
lo mas dificil de implementar para el back va a ser, implementar las funciones de ajuste "automatico" de cargado de files, con un url, desde las listas, del sat y las publicaciones del diario.
|
lo mas dificil de implementar para el back va a ser, implementar las funciones de ajuste "automatico" de cargado de file con un url, desde las listas del sat y las publicaciones del diario.
|
||||||
|
|
||||||
si el tiempo nos cae encima (que es lo mas seguro, voy a concentrarme en trabajar en las listas de el sat(edos, efos y creditos), para tratar de dejar enpoints funcionales).
|
si el tiempo nos cae encima (que es lo mas seguro, voy a concentrarme en trabajar en las listas de el sat(edos, efos y creditos), para tratar de dejar enpoints funcionales).
|
||||||
|
|
||||||
|
|
||||||
|
# Flujo del tomado automatico de las listas del sat
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PIPELINE DE TRANSFORMACIÓN │
|
||||||
|
│ (CSV → JSONB → Base de Datos) │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ SAT ORIGEN │
|
||||||
|
│ archivo.csv │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LECTOR POR LOTES (chunks) │
|
||||||
|
│ ┌──────────────────────┐ │
|
||||||
|
│ │ Bloque 1: filas 1-100│ │
|
||||||
|
│ │ Bloque 2: filas 101-200│ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ │ Bloque N: últimas │ │
|
||||||
|
│ └──────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ TRANSFORMADOR POR BLOQUE │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ 1. Agrupar │───▶│ 2. Limpiar │───▶│ 3. Armar │───▶│ 4. Validar │ │
|
||||||
|
│ │ por RFC │ │ fechas │ │ JSONB │ │ esquema │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ FUNCIONES REUTILIZABLES │ │
|
||||||
|
│ │ • normalize_date() → convierte números Excel y strings a ISO │ │
|
||||||
|
│ │ • get_latest_status() → identifica el estatus más reciente │ │
|
||||||
|
│ │ • build_jsonb() → construye la estructura final │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ OUTPUT: JSONB POR RFC │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ { │
|
||||||
|
│ "rfc": "ASA110718HJ5", │
|
||||||
|
│ "nombre": "ASESORÍA Y SERVICIOS ACANTO, S.A. DE C.V.", │
|
||||||
|
│ "ultimo_estatus": { │
|
||||||
|
│ "situacion": "definitivo", │
|
||||||
|
│ "fecha": "2020-06-24", │
|
||||||
|
│ "oficio": "500-05-2020-13598" │
|
||||||
|
│ }, │
|
||||||
|
│ "historial": [...], │
|
||||||
|
│ "raw_metadata": {...} │
|
||||||
|
│ } │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ REPOSITORIO / SERVICE LAYER │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ interface EfosRepository │ │
|
||||||
|
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
||||||
|
│ │ │ save_batch() │ │ find_by_rfc() │ │ │
|
||||||
|
│ │ │ update_status() │ │ search_by_name() │ │ │
|
||||||
|
│ │ │ get_statistics() │ │ count_by_status() │ │ │
|
||||||
|
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────┼─────────────────┐ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ PostgreSQL │ │ SQLite │ │ MongoDB │ │
|
||||||
|
│ │ (producción) │ │ (dev/local) │ │ (alternativa│ │
|
||||||
|
│ └─────────────┘ └─────────────┘ │ JSON nativo)│ │
|
||||||
|
│ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ FRONT-END API (FastAPI) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ GET /contribuyentes/{rfc} → consulta por RFC │
|
||||||
|
│ GET /contribuyentes?q={nombre} → búsqueda por nombre │
|
||||||
|
│ GET /contribuyentes?status={x} → filtrar por situación │
|
||||||
|
│ GET /stats → contar por status (rojo/amarillo/verde) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
# 09/04/2026 - queda pendiente;
|
||||||
|
- revisar, verificar y tabajar todos los cruds.
|
||||||
|
- los que trabajan completamente bien, son los de usuarios y los de clients.
|
||||||
|
- la bd, funciona correctamente y las migraciones estan en su top.
|
||||||
|
- en este punto el endpoint de tomado automatico de las listas del sat, ya funcionan con el url del CSV. falta ajustar que detecte en que bd escriba, por el momento solo funciona con EDOS.
|
||||||
|
@ @
|
||||||
|
* Nota importante *
|
||||||
|
- !para hacer chunks del cargado de la lista de Efos(articulo69-b) usar una libreria y no hacerle caso al ENFERMO DE HUGO, para hacerlo a mano!
|
||||||
|
|
||||||
|
|
||||||
|
urls importantes:
|
||||||
|
https://wu1agsprosta001.blob.core.windows.net/agsc-publicaciones/Datos_abiertos/Documents_AGAFF/Listado_completo_69-B.csv
|
||||||
|
* https://wu1agsprosta001.blob.core.windows.net/agsc-publicaciones/Datos_abiertos/Documents_AGGC/Listado_69_B_Bis_Completo.csv
|
||||||
|
|
||||||
|
* https://www.sat.gob.mx/minisitio/DatosAbiertos/contribuyentes_publicados.html#collapseTwo1
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,56 +1,74 @@
|
|||||||
from typing import List, Dict, Any
|
#helpers/csv_mapper
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
from app.modules.edos.schema import EdosUpload
|
from app.modules.edos.schema import EdosUpload
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
|
||||||
class CSVMapper:
|
class CSVMapper:
|
||||||
""" Mapper: filter and mapping only necesary data, based on a schema"""
|
""" Mapper: filter and mapping only necesary data, based on a schema"""
|
||||||
|
|
||||||
FIELD_MAPPING = {
|
FIELD_MAPPING = {
|
||||||
'csv_column_name': 'schema_field',
|
'RFC': 'numero',
|
||||||
'No.' : 'publicacion_sat',
|
'Nombre del Contribuyente': 'razon_social',
|
||||||
'RFC' : 'numero',
|
'Situación del contribuyente': 'situacion',
|
||||||
'Nombre del Contribuyente' : 'razon_social',
|
'Número y fecha de oficio global definitivo SAT': 'numero_definitivo',
|
||||||
'Situación del contribuyente' : 'situacion',
|
'Publicación página SAT definitivo': 'fecha_definitivo',
|
||||||
'Número y fecha de oficio global definitivo SAT' : 'numero_definitivo',
|
'Número y fecha de oficio global definitivo DOF': 'numero_def_dof',
|
||||||
'Publicación página SAT definitivo' : 'fecha_definitivo',
|
'Publicación DOF definitivo': 'fecha_def_dof',
|
||||||
'Número y fecha de oficio global definitivo DOF' : 'numero_def_dof',
|
'Número y fecha de oficio global de sentencia favorable SAT': 'numero_fav_sat',
|
||||||
'Publicación DOF definitivo' : 'fecha_def_dof',
|
'Publicación página SAT sentencia favorable': 'publicacion_sat',
|
||||||
'Número y fecha de oficio global de sentencia favorable SAT' : 'publicacion_dof',
|
'Publicación DOF sentencia favorable': 'publicacion_dof',
|
||||||
'Publicación página SAT sentencia favorable' : 'numero_fav_sat',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
REQUIRED_FIELD = [ 'publicacion_sat', 'numero', 'razon_social', 'situacion'] #Campos obligatorios
|
REQUIRED_FIELD = [ 'numero', 'razon_social', 'situacion'] #Campos obligatorios
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_by_schema(raw_rows: List[Dict]) -> List[Dict]:
|
def map_rows(csv_rows: List[Dict]) -> List[Dict]:
|
||||||
""" Filter only fields defined on schema"""
|
mapped_rows = []
|
||||||
filtered = []
|
|
||||||
|
|
||||||
for row in raw_rows:
|
for row in csv_rows:
|
||||||
mapped_row = {}
|
mapped = {}
|
||||||
for csv_field, schema_field in CSVMapper.FIELD_MAPPING.items():
|
|
||||||
if csv_field in row and row[csv_field]:
|
for csv_field, bd_field in CSVMapper.FIELD_MAPPING.items():
|
||||||
mapped_row[schema_field] = CSVMapper._clean_value(
|
value = row.get(csv_field, '')
|
||||||
row[csv_field], schema_field
|
|
||||||
)
|
if bd_field == 'razon_social':
|
||||||
if all(field in mapped_row for field in CSVMapper.REQUIRED_FIELD):
|
value = value.replace('\n', ' ').replace('\r', ' ')
|
||||||
filtered.append(mapped_row)
|
|
||||||
else:
|
if bd_field == 'situacion':
|
||||||
print(f"omited fields: {mapped_row}")
|
value = CSVMapper._normalize_situacion(value)
|
||||||
return filtered
|
|
||||||
|
if 'fecha' in bd_field and value:
|
||||||
|
value = CSVMapper._parse_date(value)
|
||||||
|
|
||||||
|
mapped[bd_field] = value if value else None
|
||||||
|
|
||||||
|
missing = [f for f in CSVMapper.REQUIRED_FIELD if not mapped.get(f)]
|
||||||
|
if missing:
|
||||||
|
print(f"Fila omitida - faltan: {missing}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
mapped_rows.append(mapped)
|
||||||
|
|
||||||
|
return mapped_rows
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _clean_value(value: str, field_type: str) -> Any:
|
def _normalize_situacion(value: str) -> str:
|
||||||
""" Cleanen and typified values"""
|
""" converts text of CVS to format ENUM"""
|
||||||
if not value or value.strip() == '':
|
if not value:
|
||||||
|
return None
|
||||||
|
if 'Sentencia Favorable' in value:
|
||||||
|
return 'sentencia_favorable'
|
||||||
|
if 'Definitivo' in value:
|
||||||
|
return 'definitivo'
|
||||||
|
return value.lower()
|
||||||
|
@staticmethod
|
||||||
|
def _parse_date(value: str) -> Optional[date]:
|
||||||
|
"""Converts DD/MM/YYYY to date"""
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value.strip(), '%dd%m%Y').date()
|
||||||
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if 'date' in field_type:
|
|
||||||
from datetime import datetime
|
|
||||||
return datetime.strptime(value.strip(), '%Y-%m-%d')
|
|
||||||
elif 'amount' in field_type or 'price' in field_type:
|
|
||||||
return float(value.strip().replace(',','.'))
|
|
||||||
elif 'int' in field_type.lower():
|
|
||||||
return int(value.strip())
|
|
||||||
else:
|
|
||||||
return value.strip()
|
|
||||||
|
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
|
#helper/db_adapter
|
||||||
|
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
from database import sessionLocal
|
from database import sessionLocal
|
||||||
from app.modules.edos.models import EDOS
|
from app.modules.edos.models import EDOS
|
||||||
|
|
||||||
class DBAdapter:
|
class DBAdapter:
|
||||||
""" Adapter: insert cleen data on a db"""
|
""" Adapter: insert cleen data on a db"""
|
||||||
|
|
||||||
def __inti__(self):
|
def __init__(self):
|
||||||
self.db = sessionLocal()
|
self.db = sessionLocal()
|
||||||
|
|
||||||
def insert_filtered_data(self, mapped_rows: List[Dict]) -> Dict:
|
def insert_filtered_data(self, mapped_rows: List[Dict]) -> Dict:
|
||||||
""" Insert only data filtered and mapped"""
|
""" Insert only data filtered and mapped"""
|
||||||
|
print(f" [DBAdapter] Recibidas {len(mapped_rows)} filas")
|
||||||
|
|
||||||
|
if mapped_rows:
|
||||||
|
print(f" [DBAdapter] Primera fila: {mapped_rows[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
'total_processed': len(mapped_rows),
|
'total_processed': len(mapped_rows),
|
||||||
'inserted': 0,
|
'inserted': 0,
|
||||||
@@ -19,8 +28,11 @@ class DBAdapter:
|
|||||||
for row in mapped_rows:
|
for row in mapped_rows:
|
||||||
try:
|
try:
|
||||||
#verified dupled data
|
#verified dupled data
|
||||||
|
|
||||||
|
unique_field_value = row.get('numero')
|
||||||
|
|
||||||
existing = self.db.query(EDOS).filter_by(
|
existing = self.db.query(EDOS).filter_by(
|
||||||
unique_field=row.get('unique_field')
|
numero=unique_field_value
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
@@ -28,8 +40,11 @@ class DBAdapter:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
instance = EDOS(**row)
|
instance = EDOS(**row)
|
||||||
|
|
||||||
self.db.add(instance)
|
self.db.add(instance)
|
||||||
|
print(f" [DBAdapter] Added instance for {row.get('numero')}")
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
print(f" [DBAdapter] Committed successfully")
|
||||||
results['inserted'] += 1
|
results['inserted'] += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
@@ -37,6 +52,6 @@ class DBAdapter:
|
|||||||
'row': row,
|
'row': row,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
})
|
})
|
||||||
self.db.close()
|
self.db.close()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#helper/csv_mapper
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import requests
|
import requests
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
@@ -5,6 +7,10 @@ from io import StringIO
|
|||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
#
|
||||||
|
import pandas as pd
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -17,6 +23,21 @@ class Settings:
|
|||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
|
def _find_header_row(self, rows, delimiter):
|
||||||
|
""" Search row with RFC an name"""
|
||||||
|
|
||||||
|
for idx, row in enumerate(rows):
|
||||||
|
#Normalice: Convert to String, clean space, mayus
|
||||||
|
clean_row = [str(cell).strip().upper() for cell in row]
|
||||||
|
|
||||||
|
has_rfc = any('RFC' in cell for cell in clean_row)
|
||||||
|
has_nombre =any('NOMBRE' in cell for cell in clean_row)
|
||||||
|
|
||||||
|
if has_rfc and has_nombre:
|
||||||
|
return idx
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class CSVExtractor:
|
class CSVExtractor:
|
||||||
"""Extract raw data of CSV"""
|
"""Extract raw data of CSV"""
|
||||||
|
|
||||||
@@ -36,46 +57,40 @@ class CSVExtractor:
|
|||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read_csv(content: str) -> List[Dict[str, Any]]:
|
def read_csv(content):
|
||||||
""" Converts CSV to list of dictionary"""
|
""" Converts CSV to list of dictionary"""
|
||||||
csv_file = StringIO(content)
|
for delimiter in[',', ';', '\t']:
|
||||||
#detect delimiter auto
|
|
||||||
sample = csv_file.read(1024)
|
|
||||||
csv_file.seek(0)
|
|
||||||
try:
|
|
||||||
dialect = csv.Sniffer().sniff(sample)
|
|
||||||
reader =csv.DictReader(csv_file, dialect=dialect)
|
|
||||||
except:
|
|
||||||
csv_file.seek(0)
|
|
||||||
reader = csv.DictReader(csv_file)
|
|
||||||
|
|
||||||
rows = [row for row in reader]
|
|
||||||
|
|
||||||
if rows:
|
try:
|
||||||
clean_row = {}
|
lines = content.split('\n')
|
||||||
|
|
||||||
for key, value in rows[0].items():
|
header_row_idx = None
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
if 'RFC' in line.upper():
|
||||||
|
header_row_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if header_row_idx is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if key is not None:
|
df = pd.read_csv(
|
||||||
clean_key = key.strip().replace('"', '').replace("'", "").replace('\ufeff', '')
|
StringIO(content),
|
||||||
else:
|
delimiter=delimiter,
|
||||||
clean_key = ''
|
encoding='utf-8',
|
||||||
|
skiprows=header_row_idx,
|
||||||
clean_row[clean_key] = value
|
dtype=str,
|
||||||
|
keep_default_na=False,
|
||||||
|
na_filter=False)
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
df.columns = df.columns.str.strip()
|
||||||
new_row = {}
|
|
||||||
for old_key, value in row.items():
|
|
||||||
if old_key is not None:
|
|
||||||
new_key = old_key.strip().replace('"', '').replace("'", "").replace('\ufeff', '')
|
|
||||||
else:
|
|
||||||
new_key = ''
|
|
||||||
#FIX: manage value None for avoid error strip
|
|
||||||
if value is not None and isinstance(value, str):
|
|
||||||
new_value = value.strip()
|
|
||||||
else:
|
|
||||||
new_value = value
|
|
||||||
|
|
||||||
new_row[new_key] = value
|
df = df.dropna(how='all')
|
||||||
rows[i] = new_row
|
result = df.to_dict('records')
|
||||||
return rows
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error with delimiter '{delimiter}': {e}")
|
||||||
|
continue
|
||||||
|
raise ValueError("Don't read CSV with pandas")
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,12 +16,12 @@ class EDOS(Base):
|
|||||||
__tablename__="edos"
|
__tablename__="edos"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, nullable=False)
|
id = Column(Integer, primary_key=True, nullable=False)
|
||||||
numero = Column(Integer, nullable=False)
|
numero = Column(String(25), nullable=False)
|
||||||
razon_social = Column(String(100), nullable=False)
|
razon_social = Column(String(100), nullable=False)
|
||||||
situacion = Column(SQLEnum(Situacion, name="situcion", create_type=False), nullable=False)
|
situacion = Column(SQLEnum(Situacion, name="situcion", create_type=False), nullable=False)
|
||||||
numero_definitivo = Column(String(60), nullable=False)
|
numero_definitivo = Column(String(60), nullable=False)
|
||||||
fecha_definitivo = Column(Date, nullable=True )
|
fecha_definitivo = Column(Date, nullable=True )
|
||||||
publicaccion_sat = Column(Date, nullable=True)
|
publicacion_sat = Column(Date, nullable=True)
|
||||||
numero_def_dof = Column(String(100), nullable=True)
|
numero_def_dof = Column(String(100), nullable=True)
|
||||||
fecha_def_dof = Column(Date, nullable=True)
|
fecha_def_dof = Column(Date, nullable=True)
|
||||||
publicacion_dof = Column(Date, nullable=True)
|
publicacion_dof = Column(Date, nullable=True)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ async def import_csv(data: Dict[str, Any] = Body(..., example={"url": "http://ex
|
|||||||
|
|
||||||
|
|
||||||
csv_url = data.get('url')
|
csv_url = data.get('url')
|
||||||
dry_run = data.get('dry_run', True)
|
dry_run = data.get('dry_run', False)
|
||||||
max_rows = data.get('max_rows')
|
max_rows = data.get('max_rows')
|
||||||
|
|
||||||
if not csv_url:
|
if not csv_url:
|
||||||
@@ -26,6 +26,7 @@ async def import_csv(data: Dict[str, Any] = Body(..., example={"url": "http://ex
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
result = XMACSVService.process_csv_from_url(
|
result = XMACSVService.process_csv_from_url(
|
||||||
csv_url= csv_url,
|
csv_url= csv_url,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class EdosUpload(BaseModel):
|
|||||||
situacion : Situacion
|
situacion : Situacion
|
||||||
numero_definitivo : str
|
numero_definitivo : str
|
||||||
fecha_definitivo : date
|
fecha_definitivo : date
|
||||||
publicaccion_sat : date
|
publicacion_sat : date
|
||||||
numero_def_dof : str
|
numero_def_dof : str
|
||||||
fecha_def_dof : date
|
fecha_def_dof : date
|
||||||
publicacion_dof : date
|
publicacion_dof : date
|
||||||
|
|||||||
@@ -4,71 +4,95 @@ from typing import Optional
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from app.modules.edos.models import EDOS
|
from app.modules.edos.models import EDOS
|
||||||
from app.modules.edos.schema import EdosResponse, EdosUpload, messageResponse
|
from app.modules.edos.schema import EdosResponse, EdosUpload, messageResponse
|
||||||
|
|
||||||
|
|
||||||
from app.helpers.csv_mapper import CSVMapper
|
from app.helpers.csv_mapper import CSVMapper
|
||||||
from app.helpers.db_adapter import DBAdapter
|
from app.helpers.db_adapter import DBAdapter
|
||||||
from app.helpers.extractCsv import CSVExtractor
|
from app.helpers.extractCsv import CSVExtractor
|
||||||
|
|
||||||
class XMACSVService:
|
class XMACSVService:
|
||||||
""" Orquester"""
|
"""Orchestrator: extract, map, adapt pipeline"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_csv_from_url(
|
def process_csv_from_url(
|
||||||
csv_url: str,
|
csv_url: str,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
max_rows: int = None
|
max_rows: int = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
""" flow download and parsing, filter and adapt"""
|
"""Flow: download CSV, parse, map to schema, insert into database"""
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# X - EXTRACT
|
# =========================================================
|
||||||
print(f"📥 Descargando CSV: {csv_url}")
|
# X - EXTRACT: Download and parse CSV
|
||||||
|
# =========================================================
|
||||||
|
print(f"Downloading CSV: {csv_url}")
|
||||||
|
|
||||||
# ✅ Ahora download_csv retorna (content, encoding)
|
|
||||||
content, encoding = CSVExtractor.download_csv(csv_url)
|
content, encoding = CSVExtractor.download_csv(csv_url)
|
||||||
print(f" ✅ Descargado {len(content)} bytes, encoding: {encoding}")
|
print(f" Downloaded {len(content)} bytes, encoding: {encoding}")
|
||||||
|
|
||||||
|
# Debug: show first lines
|
||||||
|
print("First 10 lines of CSV:")
|
||||||
|
lines = content.split('\n')
|
||||||
|
for i, line in enumerate(lines[:10]):
|
||||||
|
print(f"Line {i}: {repr(line[:200])}")
|
||||||
|
|
||||||
# Parsear CSV
|
# Parse CSV to raw rows
|
||||||
raw_rows = CSVExtractor.read_csv(content)
|
raw_rows = CSVExtractor.read_csv(content)
|
||||||
print(f" ✅ Parseadas {len(raw_rows)} filas")
|
print(f" Parsed {len(raw_rows)} rows")
|
||||||
|
|
||||||
if not raw_rows:
|
if not raw_rows:
|
||||||
return {
|
return {
|
||||||
'total_extracted': 0,
|
'total_extracted': 0,
|
||||||
'total_mapped': 0,
|
'total_mapped': 0,
|
||||||
'inserted': 0,
|
'inserted': 0,
|
||||||
'errors': ['No se encontraron datos en el CSV']
|
'dry_run': dry_run,
|
||||||
|
'errors': ['No data found in CSV']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mostrar columnas encontradas
|
print(f" Columns: {list(raw_rows[0].keys())}")
|
||||||
print(f" 📋 Columnas: {list(raw_rows[0].keys())}")
|
|
||||||
|
|
||||||
# M - MAP (si tienes mapper)
|
# =========================================================
|
||||||
# Por ahora, usar datos crudos
|
# M - MAP: Transform CSV rows to database schema
|
||||||
mapped_rows = raw_rows
|
# =========================================================
|
||||||
|
mapped_rows = CSVMapper.map_rows(raw_rows)
|
||||||
|
print(f" Mapped {len(mapped_rows)} rows to database schema")
|
||||||
|
|
||||||
if max_rows:
|
if max_rows:
|
||||||
mapped_rows = mapped_rows[:max_rows]
|
mapped_rows = mapped_rows[:max_rows]
|
||||||
print(f" 🔒 Limitado a {max_rows} filas")
|
print(f" Limited to {max_rows} rows")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# A - ADAPT: Insert into database
|
||||||
|
# =========================================================
|
||||||
|
inserted_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
# A - ADAPT
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
print(f" 💾 Insertando {len(mapped_rows)} filas en BD")
|
print(f" Inserting {len(mapped_rows)} rows into database")
|
||||||
# Aquí iría la inserción en BD
|
adapter = DBAdapter()
|
||||||
|
result = adapter.insert_filtered_data(mapped_rows)
|
||||||
|
inserted_count = result.get('inserted', 0)
|
||||||
|
errors = result.get('errors', [])
|
||||||
|
print(f" Inserted: {inserted_count}")
|
||||||
|
print(f" Duplicates skipped: {result.get('duplicates_skiped', 0)}")
|
||||||
|
if errors:
|
||||||
|
print(f" Errors: {len(errors)}")
|
||||||
|
else:
|
||||||
|
print(f" DRY RUN - No data inserted")
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Return response
|
||||||
|
# =========================================================
|
||||||
return {
|
return {
|
||||||
'total_extracted': len(raw_rows),
|
'total_extracted': len(raw_rows),
|
||||||
'total_mapped': len(mapped_rows),
|
'total_mapped': len(mapped_rows),
|
||||||
'inserted': len(mapped_rows) if not dry_run else 0,
|
'inserted': inserted_count,
|
||||||
'dry_run': dry_run,
|
'dry_run': dry_run,
|
||||||
'columns': list(raw_rows[0].keys()) if raw_rows else [],
|
'columns': list(raw_rows[0].keys()) if raw_rows else [],
|
||||||
'sample': raw_rows[:2] if raw_rows else []
|
'sample': mapped_rows[:2] if mapped_rows else [],
|
||||||
|
'errors': errors[:10] if errors else [] # Limit errors in response
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error: {str(e)}")
|
print(f"Error: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise
|
raise
|
||||||
54
migrations/versions/876421e4eea2_adjustmen_of_bds.py
Normal file
54
migrations/versions/876421e4eea2_adjustmen_of_bds.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""adjustmen of BDs
|
||||||
|
|
||||||
|
Revision ID: 876421e4eea2
|
||||||
|
Revises: c01f58ebc7d4
|
||||||
|
Create Date: 2026-04-09 20:28:03.853032
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '876421e4eea2'
|
||||||
|
down_revision: Union[str, None] = 'c01f58ebc7d4'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('edos', 'numero',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.String(length=25),
|
||||||
|
existing_nullable=False)
|
||||||
|
op.alter_column('edos', 'fecha_definitivo',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('license', 'deleted_at',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=True)
|
||||||
|
op.drop_column('location', 'country_id')
|
||||||
|
op.drop_column('location', 'city_id')
|
||||||
|
op.drop_column('location', 'state_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('location', sa.Column('state_id', sa.INTEGER(), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('location', sa.Column('city_id', sa.INTEGER(), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('location', sa.Column('country_id', sa.INTEGER(), autoincrement=False, nullable=False))
|
||||||
|
op.alter_column('license', 'deleted_at',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('edos', 'fecha_definitivo',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('edos', 'numero',
|
||||||
|
existing_type=sa.String(length=25),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
32
migrations/versions/c242c2733a7f_adjustmen_of_bdsv2.py
Normal file
32
migrations/versions/c242c2733a7f_adjustmen_of_bdsv2.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""adjustmen of BDsV2
|
||||||
|
|
||||||
|
Revision ID: c242c2733a7f
|
||||||
|
Revises: 876421e4eea2
|
||||||
|
Create Date: 2026-04-09 20:37:27.188549
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c242c2733a7f'
|
||||||
|
down_revision: Union[str, None] = '876421e4eea2'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('edos', sa.Column('publicacion_sat', sa.Date(), nullable=True))
|
||||||
|
op.drop_column('edos', 'publicaccion_sat')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('edos', sa.Column('publicaccion_sat', sa.DATE(), autoincrement=False, nullable=True))
|
||||||
|
op.drop_column('edos', 'publicacion_sat')
|
||||||
|
# ### end Alembic commands ###
|
||||||
30
migrations/versions/d91de687dac7_full_bd.py
Normal file
30
migrations/versions/d91de687dac7_full_bd.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""full bd
|
||||||
|
|
||||||
|
Revision ID: d91de687dac7
|
||||||
|
Revises: c242c2733a7f
|
||||||
|
Create Date: 2026-04-09 21:02:22.168790
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'd91de687dac7'
|
||||||
|
down_revision: Union[str, None] = 'c242c2733a7f'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user