eco-back 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eco_back/__init__.py +4 -2
- eco_back/documento/__init__.py +6 -0
- eco_back/documento/anexos.py +481 -0
- eco_back/registro/__init__.py +7 -0
- eco_back/registro/trazabilidad.py +0 -0
- {eco_back-0.1.0.dist-info → eco_back-0.2.0.dist-info}/METADATA +14 -6
- {eco_back-0.1.0.dist-info → eco_back-0.2.0.dist-info}/RECORD +10 -6
- {eco_back-0.1.0.dist-info → eco_back-0.2.0.dist-info}/WHEEL +1 -1
- {eco_back-0.1.0.dist-info → eco_back-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {eco_back-0.1.0.dist-info → eco_back-0.2.0.dist-info}/top_level.txt +0 -0
eco_back/__init__.py
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
eco-back - Librería Python
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
6
|
|
|
7
7
|
# Módulos principales
|
|
8
8
|
from . import database
|
|
9
9
|
from . import api
|
|
10
|
+
from . import documento
|
|
11
|
+
from . import registro
|
|
10
12
|
|
|
11
|
-
__all__ = ["database", "api", "__version__"]
|
|
13
|
+
__all__ = ["database", "api", "documento", "registro", "__version__"]
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cliente para gestión de anexos y documentos UNP
|
|
3
|
+
|
|
4
|
+
Maneja el proceso de validación, escaneo y envío de documentos
|
|
5
|
+
a los servicios de la API UNP.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional, Dict, Any, Callable, BinaryIO, Union
|
|
18
|
+
from io import BytesIO
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Constantes
|
|
26
|
+
MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
|
27
|
+
MAX_RETRIES = 3
|
|
28
|
+
TIMEOUT = 30
|
|
29
|
+
|
|
30
|
+
# Extensiones MIME permitidas
|
|
31
|
+
ALLOWED_EXTENSIONS = {
|
|
32
|
+
'pdf': 'application/pdf',
|
|
33
|
+
'jpg': 'image/jpeg',
|
|
34
|
+
'jpeg': 'image/jpeg',
|
|
35
|
+
'png': 'image/png',
|
|
36
|
+
'doc': 'application/msword',
|
|
37
|
+
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
38
|
+
'xls': 'application/vnd.ms-excel',
|
|
39
|
+
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AnexoConfig:
|
|
45
|
+
"""
|
|
46
|
+
Configuración para el gestor de anexos
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
url_base: URL base del API UNP
|
|
50
|
+
max_size: Tamaño máximo de archivo en bytes (default: 10MB)
|
|
51
|
+
max_retries: Número máximo de reintentos (default: 3)
|
|
52
|
+
timeout: Timeout para las peticiones en segundos (default: 30)
|
|
53
|
+
scan_function: Función opcional para escaneo de archivos
|
|
54
|
+
diccionario_registro: Función para mapear tipos de documento de registro
|
|
55
|
+
diccionario_persona: Función para mapear tipos de documento de persona
|
|
56
|
+
diccionario_colectivo: Función para mapear tipos de documento de colectivo
|
|
57
|
+
"""
|
|
58
|
+
url_base: str
|
|
59
|
+
max_size: int = MAX_SIZE
|
|
60
|
+
max_retries: int = MAX_RETRIES
|
|
61
|
+
timeout: int = TIMEOUT
|
|
62
|
+
scan_function: Optional[Callable] = None
|
|
63
|
+
diccionario_registro: Optional[Callable] = None
|
|
64
|
+
diccionario_persona: Optional[Callable] = None
|
|
65
|
+
diccionario_colectivo: Optional[Callable] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DocumentoResult:
|
|
69
|
+
"""Resultado del procesamiento de un documento"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, status: str, mensaje: str, detalles: Optional[Dict] = None):
|
|
72
|
+
self.status = status
|
|
73
|
+
self.mensaje = mensaje
|
|
74
|
+
self.detalles = detalles or {}
|
|
75
|
+
self.exitoso = status in ['success', 'processing']
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
78
|
+
"""Convierte el resultado a diccionario"""
|
|
79
|
+
return {
|
|
80
|
+
'status': self.status,
|
|
81
|
+
'mensaje': self.mensaje,
|
|
82
|
+
'detalles': self.detalles,
|
|
83
|
+
'exitoso': self.exitoso
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AnexoManager:
|
|
88
|
+
"""
|
|
89
|
+
Gestor de anexos y documentos para la API UNP
|
|
90
|
+
|
|
91
|
+
Maneja el proceso completo de:
|
|
92
|
+
- Validación de archivos
|
|
93
|
+
- Escaneo antivirus (opcional)
|
|
94
|
+
- Envío asíncrono a la API UNP
|
|
95
|
+
|
|
96
|
+
Ejemplo:
|
|
97
|
+
>>> from eco_back.documento import AnexoManager, AnexoConfig
|
|
98
|
+
>>>
|
|
99
|
+
>>> config = AnexoConfig(url_base="https://api.unp.gov.co")
|
|
100
|
+
>>> manager = AnexoManager(config)
|
|
101
|
+
>>>
|
|
102
|
+
>>> # Subir archivo desde ruta
|
|
103
|
+
>>> with open("documento.pdf", "rb") as f:
|
|
104
|
+
... resultado = manager.guardar_anexo(
|
|
105
|
+
... archivo=f,
|
|
106
|
+
... nombre_archivo="documento.pdf",
|
|
107
|
+
... llave="12345",
|
|
108
|
+
... grupo="grupo1",
|
|
109
|
+
... tipo_documento="cedula",
|
|
110
|
+
... diccionario=1
|
|
111
|
+
... )
|
|
112
|
+
>>> print(resultado.mensaje)
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, config: AnexoConfig):
|
|
116
|
+
"""
|
|
117
|
+
Inicializa el gestor de anexos
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
config: Configuración del gestor
|
|
121
|
+
"""
|
|
122
|
+
self.config = config
|
|
123
|
+
|
|
124
|
+
def obtener_extension(self, nombre_archivo: str) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Obtiene la extensión del archivo (incluye el punto, ej: '.pdf')
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
nombre_archivo: Nombre del archivo
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Extensión del archivo en minúsculas (con punto)
|
|
133
|
+
"""
|
|
134
|
+
return os.path.splitext(nombre_archivo)[1].lower()
|
|
135
|
+
|
|
136
|
+
def validar_extension(self, extension: str) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Valida si la extensión es permitida
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
extension: Extensión a validar (con o sin punto)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True si es válida, False en caso contrario
|
|
145
|
+
"""
|
|
146
|
+
ext = extension.lstrip('.')
|
|
147
|
+
return ext in ALLOWED_EXTENSIONS
|
|
148
|
+
|
|
149
|
+
def obtener_mime_type(self, nombre_archivo: str) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Obtiene el tipo MIME basado en la extensión del archivo
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
nombre_archivo: Nombre del archivo
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Tipo MIME correspondiente
|
|
158
|
+
"""
|
|
159
|
+
ext = nombre_archivo.split('.')[-1].lower()
|
|
160
|
+
return ALLOWED_EXTENSIONS.get(ext, 'application/octet-stream')
|
|
161
|
+
|
|
162
|
+
def _escanear_archivo(self, ruta_archivo: str, extension: str) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Escanea el archivo en busca de amenazas
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
ruta_archivo: Ruta del archivo a escanear
|
|
168
|
+
extension: Extensión del archivo
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Diccionario con status y detalles del escaneo
|
|
172
|
+
"""
|
|
173
|
+
if self.config.scan_function:
|
|
174
|
+
try:
|
|
175
|
+
return self.config.scan_function(ruta_archivo, extension)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Error en escaneo: {e}")
|
|
178
|
+
return {'status': 'ERROR', 'details': str(e)}
|
|
179
|
+
|
|
180
|
+
# Sin función de escaneo, asumir limpio
|
|
181
|
+
return {'status': 'CLEAN', 'details': 'No scan configured'}
|
|
182
|
+
|
|
183
|
+
def guardar_anexo(
|
|
184
|
+
self,
|
|
185
|
+
archivo: Union[BinaryIO, BytesIO, bytes],
|
|
186
|
+
nombre_archivo: str,
|
|
187
|
+
llave: str,
|
|
188
|
+
grupo: str,
|
|
189
|
+
tipo_documento: str,
|
|
190
|
+
diccionario: int,
|
|
191
|
+
asincrono: bool = True
|
|
192
|
+
) -> DocumentoResult:
|
|
193
|
+
"""
|
|
194
|
+
Guarda y envía un anexo a la API UNP
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
archivo: Archivo a subir (file-like object o bytes)
|
|
198
|
+
nombre_archivo: Nombre original del archivo
|
|
199
|
+
llave: Identificador único del registro
|
|
200
|
+
grupo: Grupo del documento
|
|
201
|
+
tipo_documento: Tipo de documento a enviar
|
|
202
|
+
diccionario: Tipo de diccionario (1=registro, 2=persona, 3=colectivo)
|
|
203
|
+
asincrono: Si True, envía en hilo separado (default: True)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
DocumentoResult con el resultado del procesamiento
|
|
207
|
+
"""
|
|
208
|
+
temp_dir = tempfile.mkdtemp()
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# Validar extensión
|
|
212
|
+
extension = self.obtener_extension(nombre_archivo)
|
|
213
|
+
if not self.validar_extension(extension):
|
|
214
|
+
return DocumentoResult(
|
|
215
|
+
status='error',
|
|
216
|
+
mensaje=f"Extensión {extension} no permitida",
|
|
217
|
+
detalles={'extensiones_permitidas': list(ALLOWED_EXTENSIONS.keys())}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Leer contenido del archivo
|
|
221
|
+
if isinstance(archivo, bytes):
|
|
222
|
+
contenido = archivo
|
|
223
|
+
else:
|
|
224
|
+
contenido = archivo.read()
|
|
225
|
+
|
|
226
|
+
# Validar tamaño
|
|
227
|
+
if len(contenido) > self.config.max_size:
|
|
228
|
+
return DocumentoResult(
|
|
229
|
+
status='error',
|
|
230
|
+
mensaje=f"Archivo excede tamaño máximo de {self.config.max_size / 1024 / 1024}MB",
|
|
231
|
+
detalles={'tamaño': len(contenido), 'max_size': self.config.max_size}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Guardar temporalmente
|
|
235
|
+
with tempfile.NamedTemporaryFile(delete=False, dir=temp_dir, suffix=extension) as tmp:
|
|
236
|
+
tmp.write(contenido)
|
|
237
|
+
temp_path = tmp.name
|
|
238
|
+
|
|
239
|
+
# Escaneo antivirus
|
|
240
|
+
scan_result = self._escanear_archivo(temp_path, extension)
|
|
241
|
+
if scan_result['status'] != "CLEAN":
|
|
242
|
+
os.remove(temp_path)
|
|
243
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
244
|
+
return DocumentoResult(
|
|
245
|
+
status='blocked',
|
|
246
|
+
mensaje=f"Archivo {nombre_archivo} bloqueado por escaneo",
|
|
247
|
+
detalles=scan_result
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Renombrar archivo con timestamp
|
|
251
|
+
ext = extension.lstrip('.') or 'bin'
|
|
252
|
+
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
253
|
+
nuevo_nombre = f"{llave}_anexo_{timestamp}.{ext}"
|
|
254
|
+
destino = os.path.join(temp_dir, nuevo_nombre)
|
|
255
|
+
os.replace(temp_path, destino)
|
|
256
|
+
|
|
257
|
+
file_meta = {
|
|
258
|
+
'field': 'anexo',
|
|
259
|
+
'path': destino,
|
|
260
|
+
'original_name': nombre_archivo,
|
|
261
|
+
'tipo_documento': tipo_documento,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Enviar archivo
|
|
265
|
+
if asincrono:
|
|
266
|
+
threading.Thread(
|
|
267
|
+
target=self._enviar_documento,
|
|
268
|
+
args=(file_meta, llave, grupo, tipo_documento, temp_dir, diccionario)
|
|
269
|
+
).start()
|
|
270
|
+
|
|
271
|
+
return DocumentoResult(
|
|
272
|
+
status='processing',
|
|
273
|
+
mensaje='Documento en proceso de envío',
|
|
274
|
+
detalles={'archivo': nuevo_nombre}
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
# Envío síncrono
|
|
278
|
+
resultado = self._enviar_documento_sincrono(
|
|
279
|
+
file_meta, llave, grupo, tipo_documento, temp_dir, diccionario
|
|
280
|
+
)
|
|
281
|
+
return resultado
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Error en guardar_anexo: {e}")
|
|
285
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
286
|
+
return DocumentoResult(
|
|
287
|
+
status='error',
|
|
288
|
+
mensaje=str(e),
|
|
289
|
+
detalles={'error_type': type(e).__name__}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _obtener_tipo_documento(self, tipo: str, diccionario: int) -> Optional[str]:
|
|
293
|
+
"""
|
|
294
|
+
Obtiene el tipo de documento usando el diccionario correspondiente
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
tipo: Tipo de documento original
|
|
298
|
+
diccionario: Tipo de diccionario (1=registro, 2=persona, 3=colectivo)
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Tipo de documento mapeado o None
|
|
302
|
+
"""
|
|
303
|
+
if diccionario == 1 and self.config.diccionario_registro:
|
|
304
|
+
return self.config.diccionario_registro(tipo)
|
|
305
|
+
elif diccionario == 2 and self.config.diccionario_persona:
|
|
306
|
+
return self.config.diccionario_persona(tipo)
|
|
307
|
+
elif diccionario == 3 and self.config.diccionario_colectivo:
|
|
308
|
+
return self.config.diccionario_colectivo(tipo)
|
|
309
|
+
|
|
310
|
+
# Si no hay diccionario, devolver el tipo original
|
|
311
|
+
return tipo
|
|
312
|
+
|
|
313
|
+
def _obtener_endpoint(self, diccionario: int) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Obtiene el endpoint según el tipo de diccionario
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
diccionario: Tipo de diccionario (1=registro, 2=persona, 3=colectivo)
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Endpoint correspondiente
|
|
322
|
+
"""
|
|
323
|
+
endpoints = {
|
|
324
|
+
1: 'usuario',
|
|
325
|
+
2: 'persona',
|
|
326
|
+
3: 'colectivo'
|
|
327
|
+
}
|
|
328
|
+
return endpoints.get(diccionario, 'usuario')
|
|
329
|
+
|
|
330
|
+
def _enviar_documento(
|
|
331
|
+
self,
|
|
332
|
+
file_meta: Dict[str, str],
|
|
333
|
+
llave: str,
|
|
334
|
+
grupo: str,
|
|
335
|
+
tipo: str,
|
|
336
|
+
temp_dir: str,
|
|
337
|
+
diccionario: int
|
|
338
|
+
) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Envía el documento a la API UNP (versión asíncrona)
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
file_meta: Metadatos del archivo
|
|
344
|
+
llave: Identificador del registro
|
|
345
|
+
grupo: Grupo del documento
|
|
346
|
+
tipo: Tipo de documento
|
|
347
|
+
temp_dir: Directorio temporal
|
|
348
|
+
diccionario: Tipo de diccionario
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
resultado = self._enviar_documento_sincrono(
|
|
352
|
+
file_meta, llave, grupo, tipo, temp_dir, diccionario
|
|
353
|
+
)
|
|
354
|
+
if resultado.exitoso:
|
|
355
|
+
logger.info(f"Documento enviado exitosamente: {file_meta['original_name']}")
|
|
356
|
+
else:
|
|
357
|
+
logger.error(f"Error enviando documento: {resultado.mensaje}")
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Error en envío asíncrono: {e}")
|
|
360
|
+
finally:
|
|
361
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
362
|
+
|
|
363
|
+
def _enviar_documento_sincrono(
|
|
364
|
+
self,
|
|
365
|
+
file_meta: Dict[str, str],
|
|
366
|
+
llave: str,
|
|
367
|
+
grupo: str,
|
|
368
|
+
tipo: str,
|
|
369
|
+
temp_dir: str,
|
|
370
|
+
diccionario: int
|
|
371
|
+
) -> DocumentoResult:
|
|
372
|
+
"""
|
|
373
|
+
Envía el documento a la API UNP de forma síncrona
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
file_meta: Metadatos del archivo
|
|
377
|
+
llave: Identificador del registro
|
|
378
|
+
grupo: Grupo del documento
|
|
379
|
+
tipo: Tipo de documento
|
|
380
|
+
temp_dir: Directorio temporal
|
|
381
|
+
diccionario: Tipo de diccionario
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
DocumentoResult con el resultado del envío
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
if not os.path.exists(file_meta['path']):
|
|
388
|
+
return DocumentoResult(
|
|
389
|
+
status='error',
|
|
390
|
+
mensaje=f"Archivo no encontrado: {file_meta['path']}"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Leer contenido
|
|
394
|
+
with open(file_meta['path'], 'rb') as f:
|
|
395
|
+
content = f.read()
|
|
396
|
+
if len(content) > self.config.max_size:
|
|
397
|
+
return DocumentoResult(
|
|
398
|
+
status='error',
|
|
399
|
+
mensaje=f"Archivo {file_meta['original_name']} excede tamaño permitido"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Determinar MIME
|
|
403
|
+
mime_type = self.obtener_mime_type(file_meta['original_name'])
|
|
404
|
+
|
|
405
|
+
# Preparar archivos para envío
|
|
406
|
+
files = [
|
|
407
|
+
(file_meta['field'], (file_meta['original_name'], content, mime_type))
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
# Headers
|
|
411
|
+
headers = {
|
|
412
|
+
'User-Agent': 'EcoBack-DocumentSender/1.0',
|
|
413
|
+
'X-Request-ID': str(uuid.uuid4())
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Obtener endpoint y tipo de documento
|
|
417
|
+
endpoint = self._obtener_endpoint(diccionario)
|
|
418
|
+
tipo_docu = self._obtener_tipo_documento(tipo, diccionario)
|
|
419
|
+
|
|
420
|
+
# URL completa
|
|
421
|
+
url = f"{self.config.url_base}/api-doc/{endpoint}/guardaranexo"
|
|
422
|
+
|
|
423
|
+
# Datos del formulario
|
|
424
|
+
data = {
|
|
425
|
+
'llave': llave,
|
|
426
|
+
'grupo': grupo,
|
|
427
|
+
'tipo_documento': tipo_docu
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Envío con reintentos
|
|
431
|
+
for intento in range(self.config.max_retries):
|
|
432
|
+
try:
|
|
433
|
+
response = requests.post(
|
|
434
|
+
url,
|
|
435
|
+
files=files,
|
|
436
|
+
data=data,
|
|
437
|
+
headers=headers,
|
|
438
|
+
timeout=self.config.timeout
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if response.status_code in [200, 201]:
|
|
442
|
+
logger.info(f"Envío exitoso en intento {intento + 1}")
|
|
443
|
+
return DocumentoResult(
|
|
444
|
+
status='success',
|
|
445
|
+
mensaje='Documento enviado exitosamente',
|
|
446
|
+
detalles={
|
|
447
|
+
'intentos': intento + 1,
|
|
448
|
+
'response': response.json() if response.content else {}
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
elif 500 <= response.status_code < 600:
|
|
452
|
+
# Error del servidor, reintentar
|
|
453
|
+
logger.warning(f"Error del servidor ({response.status_code}), reintentando...")
|
|
454
|
+
time.sleep(2 ** intento)
|
|
455
|
+
continue
|
|
456
|
+
else:
|
|
457
|
+
# Error del cliente, no reintentar
|
|
458
|
+
return DocumentoResult(
|
|
459
|
+
status='error',
|
|
460
|
+
mensaje=f"Error en envío: {response.status_code}",
|
|
461
|
+
detalles={'response_text': response.text}
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
except requests.RequestException as e:
|
|
465
|
+
logger.error(f"Error de conexión en intento {intento + 1}: {e}")
|
|
466
|
+
if intento == self.config.max_retries - 1:
|
|
467
|
+
return DocumentoResult(
|
|
468
|
+
status='error',
|
|
469
|
+
mensaje='Máximo número de reintentos alcanzado',
|
|
470
|
+
detalles={'error': str(e)}
|
|
471
|
+
)
|
|
472
|
+
time.sleep(2 ** intento)
|
|
473
|
+
|
|
474
|
+
return DocumentoResult(
|
|
475
|
+
status='error',
|
|
476
|
+
mensaje='No se pudo enviar el documento después de todos los reintentos'
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
finally:
|
|
480
|
+
if os.path.exists(temp_dir):
|
|
481
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eco-back
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Librería Python eco-back
|
|
5
5
|
Author-email: EI UNP <ecosistema@unp.gov.co>
|
|
6
6
|
License: MIT
|
|
@@ -22,6 +22,8 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: psycopg2-binary>=2.9.0
|
|
23
23
|
Requires-Dist: geoalchemy2>=0.14.0
|
|
24
24
|
Requires-Dist: requests>=2.31.0
|
|
25
|
+
Requires-Dist: djangorestframework>=3.14.0
|
|
26
|
+
Requires-Dist: django>=4.0.0
|
|
25
27
|
Provides-Extra: dev
|
|
26
28
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
29
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -34,16 +36,16 @@ Requires-Dist: shapely>=2.0.0; extra == "geo"
|
|
|
34
36
|
Requires-Dist: geojson>=3.0.0; extra == "geo"
|
|
35
37
|
Dynamic: license-file
|
|
36
38
|
|
|
37
|
-
# eco-back
|
|
39
|
+
# eco-back
|
|
38
40
|
|
|
39
41
|
Librería Python para backend con soporte para PostgreSQL/PostGIS y clientes API
|
|
40
42
|
|
|
41
43
|
## Características
|
|
42
44
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- **Base de datos**: Conexión y operaciones con PostgreSQL
|
|
46
|
+
- **PostGIS**: Operaciones geoespaciales (puntos, polígonos, búsquedas por proximidad)
|
|
47
|
+
- **Cliente API**: Cliente HTTP genérico y cliente específico UNP
|
|
48
|
+
- **Patrón Repository**: Implementación de patrones de diseño para acceso a datos
|
|
47
49
|
|
|
48
50
|
## Descripción
|
|
49
51
|
|
|
@@ -51,6 +53,12 @@ eco-back es una librería modular que facilita el desarrollo de aplicaciones bac
|
|
|
51
53
|
|
|
52
54
|
## Instalación
|
|
53
55
|
|
|
56
|
+
### Desde PyPI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install eco-back
|
|
60
|
+
```
|
|
61
|
+
|
|
54
62
|
### Desde el código fuente
|
|
55
63
|
|
|
56
64
|
```bash
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
eco_back/__init__.py,sha256=
|
|
1
|
+
eco_back/__init__.py,sha256=iBHCjaKEjk8WemoNUlZQh4O3AEingB6rJU9fF0RVDAA,255
|
|
2
2
|
eco_back/api/__init__.py,sha256=9moiU5H8jGlva8k65bUK49LRIkE7bcLH1SK1dk23wcM,202
|
|
3
3
|
eco_back/api/client.py,sha256=b2J9ty7mRtPUsMD-YJui60B9GEa-85VAAb-sS5y1Rp8,8537
|
|
4
4
|
eco_back/api/config.py,sha256=dyERZY_4lXtRjhqLaHI0F4WlgPOQ_sw35eHNWGkQNLo,1590
|
|
@@ -9,8 +9,12 @@ eco_back/database/connection.py,sha256=wGafOl_0dpgtFeIwe1f6QBtyrQ2BiEbw50Yfs1E-R
|
|
|
9
9
|
eco_back/database/models.py,sha256=832FKC-4lfWWai0b_mtCv-H1MdHLJaaFY5NvsW8BqGs,1161
|
|
10
10
|
eco_back/database/postgis.py,sha256=oTuz8OzGeFiM3Aro3Zlid31t7tJJNSvqAUYTTaYUzoI,10954
|
|
11
11
|
eco_back/database/repository.py,sha256=NFh0pfvf7Pw878qOoVhR8ARMFC0CiJBQ_EwKshY8lOE,4433
|
|
12
|
-
eco_back
|
|
13
|
-
eco_back
|
|
14
|
-
eco_back
|
|
15
|
-
eco_back
|
|
16
|
-
eco_back-0.
|
|
12
|
+
eco_back/documento/__init__.py,sha256=0rIgJ52in_BMg7VYr2pVlu9EnTPftgdqJ4hOZU5I4j0,146
|
|
13
|
+
eco_back/documento/anexos.py,sha256=KIzGNEVJ6tV3awhq4WGiqb8eSV2PluRRZ0PRbqeO5VE,17511
|
|
14
|
+
eco_back/registro/__init__.py,sha256=CRDv6OSKzt1r-RnKSfv86Ceb7yfL2Fn-OhHGCx467lE,148
|
|
15
|
+
eco_back/registro/trazabilidad.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
eco_back-0.2.0.dist-info/licenses/LICENSE,sha256=XKKSDU9WlUEAyPNlRhq6e2xhVNpJc097JwPZJ1rUnRE,1077
|
|
17
|
+
eco_back-0.2.0.dist-info/METADATA,sha256=iIpsl-7xCh5pq6JxOYu7pJkHxORwlruAP_1F-Ydz7So,4736
|
|
18
|
+
eco_back-0.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
19
|
+
eco_back-0.2.0.dist-info/top_level.txt,sha256=ViJZv023782XNNFMcAsRC9iN1CU3tb8lP9wDAHacMyc,9
|
|
20
|
+
eco_back-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|