eco-back 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0/src/eco_back.egg-info → eco_back-0.2.0}/PKG-INFO +14 -6
- {eco_back-0.1.0 → eco_back-0.2.0}/README.md +161 -155
- {eco_back-0.1.0 → eco_back-0.2.0}/pyproject.toml +3 -1
- eco_back-0.2.0/src/eco_back/__init__.py +13 -0
- eco_back-0.2.0/src/eco_back/documento/__init__.py +6 -0
- eco_back-0.2.0/src/eco_back/documento/anexos.py +481 -0
- eco_back-0.2.0/src/eco_back/registro/__init__.py +7 -0
- eco_back-0.2.0/src/eco_back/registro/trazabilidad.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0/src/eco_back.egg-info}/PKG-INFO +14 -6
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back.egg-info/SOURCES.txt +5 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back.egg-info/requires.txt +2 -0
- eco_back-0.2.0/tests/test_anexos.py +277 -0
- eco_back-0.1.0/src/eco_back/__init__.py +0 -11
- {eco_back-0.1.0 → eco_back-0.2.0}/LICENSE +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/MANIFEST.in +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/setup.cfg +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/api/__init__.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/api/client.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/api/config.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/api/registro.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/__init__.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/config.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/connection.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/models.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/postgis.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back/database/repository.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back.egg-info/dependency_links.txt +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/src/eco_back.egg-info/top_level.txt +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/tests/test_api.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/tests/test_database.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/tests/test_example.py +0 -0
- {eco_back-0.1.0 → eco_back-0.2.0}/tests/test_postgis.py +0 -0
|
@@ -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,155 +1,161 @@
|
|
|
1
|
-
# eco-back
|
|
2
|
-
|
|
3
|
-
Librería Python para backend con soporte para PostgreSQL/PostGIS y clientes API
|
|
4
|
-
|
|
5
|
-
## Características
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
## Descripción
|
|
13
|
-
|
|
14
|
-
eco-back es una librería modular que facilita el desarrollo de aplicaciones backend, proporcionando abstracciones para bases de datos geoespaciales y consumo de APIs REST.
|
|
15
|
-
|
|
16
|
-
## Instalación
|
|
17
|
-
|
|
18
|
-
### Desde
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install -
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
###
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
pip install -e
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
consecutivo
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
###
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
###
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
###
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
1
|
+
# eco-back
|
|
2
|
+
|
|
3
|
+
Librería Python para backend con soporte para PostgreSQL/PostGIS y clientes API
|
|
4
|
+
|
|
5
|
+
## Características
|
|
6
|
+
|
|
7
|
+
- **Base de datos**: Conexión y operaciones con PostgreSQL
|
|
8
|
+
- **PostGIS**: Operaciones geoespaciales (puntos, polígonos, búsquedas por proximidad)
|
|
9
|
+
- **Cliente API**: Cliente HTTP genérico y cliente específico UNP
|
|
10
|
+
- **Patrón Repository**: Implementación de patrones de diseño para acceso a datos
|
|
11
|
+
|
|
12
|
+
## Descripción
|
|
13
|
+
|
|
14
|
+
eco-back es una librería modular que facilita el desarrollo de aplicaciones backend, proporcionando abstracciones para bases de datos geoespaciales y consumo de APIs REST.
|
|
15
|
+
|
|
16
|
+
## Instalación
|
|
17
|
+
|
|
18
|
+
### Desde PyPI
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install eco-back
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Desde el código fuente
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Para desarrollo
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e ".[dev]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Uso
|
|
37
|
+
|
|
38
|
+
### Cliente de Consecutivos (UNP)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from eco_back.api import Consecutivo
|
|
42
|
+
|
|
43
|
+
# Crear cliente
|
|
44
|
+
client = Consecutivo(base_url="https://api.unp.example.com")
|
|
45
|
+
|
|
46
|
+
# Obtener consecutivo
|
|
47
|
+
consecutivo = client.obtener(origen=1)
|
|
48
|
+
if consecutivo:
|
|
49
|
+
print(f"Consecutivo: {consecutivo}")
|
|
50
|
+
|
|
51
|
+
client.close()
|
|
52
|
+
|
|
53
|
+
# Forma recomendada: usar context manager
|
|
54
|
+
with Consecutivo(base_url="https://api.unp.example.com") as client:
|
|
55
|
+
consecutivo = client.obtener(origen=1)
|
|
56
|
+
# Usar el consecutivo...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Cliente API Genérico
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from eco_back.api import APIConfig, APIClient
|
|
63
|
+
|
|
64
|
+
config = APIConfig(
|
|
65
|
+
base_url="https://api.example.com",
|
|
66
|
+
timeout=30,
|
|
67
|
+
headers={"Authorization": "Bearer token"}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
with APIClient(config) as client:
|
|
71
|
+
# GET request
|
|
72
|
+
data = client.get("/endpoint")
|
|
73
|
+
|
|
74
|
+
# POST request
|
|
75
|
+
result = client.post("/endpoint", json={"key": "value"})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Conexión básica a PostgreSQL
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from eco_back.database import DatabaseConfig, DatabaseConnection
|
|
82
|
+
|
|
83
|
+
config = DatabaseConfig(
|
|
84
|
+
host="localhost",
|
|
85
|
+
port=5432,
|
|
86
|
+
database="mi_base_datos",
|
|
87
|
+
user="usuario",
|
|
88
|
+
password="password"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
with DatabaseConnection(config) as db:
|
|
92
|
+
resultados = db.execute_query("SELECT * FROM tabla")
|
|
93
|
+
for row in resultados:
|
|
94
|
+
print(row)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Uso de PostGIS
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from eco_back.database import DatabaseConfig, DatabaseConnection, PostGISHelper
|
|
101
|
+
|
|
102
|
+
config = DatabaseConfig(
|
|
103
|
+
host="localhost",
|
|
104
|
+
port=5432,
|
|
105
|
+
database="mi_db_geo",
|
|
106
|
+
user="postgres",
|
|
107
|
+
password="password"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
with DatabaseConnection(config) as db:
|
|
111
|
+
postgis = PostGISHelper(db)
|
|
112
|
+
|
|
113
|
+
# Habilitar PostGIS
|
|
114
|
+
postgis.enable_postgis()
|
|
115
|
+
|
|
116
|
+
# Insertar un punto geográfico
|
|
117
|
+
punto_id = postgis.insert_point(
|
|
118
|
+
table_name="ubicaciones",
|
|
119
|
+
lat=40.4168,
|
|
120
|
+
lon=-3.7038,
|
|
121
|
+
data={"nombre": "Madrid", "tipo": "ciudad"}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Buscar puntos cercanos (5km)
|
|
125
|
+
cercanos = postgis.find_within_distance(
|
|
126
|
+
table_name="ubicaciones",
|
|
127
|
+
lat=40.4168,
|
|
128
|
+
lon=-3.7038,
|
|
129
|
+
distance_meters=5000
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Desarrollo
|
|
134
|
+
|
|
135
|
+
### Ejecutar tests
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pytest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Formatear código
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
black src/ tests/
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Linting
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
flake8 src/ tests/
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Type checking
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mypy src/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Licencia
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eco-back"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Librería Python eco-back"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -27,6 +27,8 @@ dependencies = [
|
|
|
27
27
|
"psycopg2-binary>=2.9.0",
|
|
28
28
|
"geoalchemy2>=0.14.0",
|
|
29
29
|
"requests>=2.31.0",
|
|
30
|
+
"djangorestframework>=3.14.0",
|
|
31
|
+
"django>=4.0.0",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eco-back - Librería Python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
|
|
7
|
+
# Módulos principales
|
|
8
|
+
from . import database
|
|
9
|
+
from . import api
|
|
10
|
+
from . import documento
|
|
11
|
+
from . import registro
|
|
12
|
+
|
|
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
|
|
@@ -18,6 +18,11 @@ src/eco_back/database/connection.py
|
|
|
18
18
|
src/eco_back/database/models.py
|
|
19
19
|
src/eco_back/database/postgis.py
|
|
20
20
|
src/eco_back/database/repository.py
|
|
21
|
+
src/eco_back/documento/__init__.py
|
|
22
|
+
src/eco_back/documento/anexos.py
|
|
23
|
+
src/eco_back/registro/__init__.py
|
|
24
|
+
src/eco_back/registro/trazabilidad.py
|
|
25
|
+
tests/test_anexos.py
|
|
21
26
|
tests/test_api.py
|
|
22
27
|
tests/test_database.py
|
|
23
28
|
tests/test_example.py
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests para el módulo de anexos y documentos
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
9
|
+
|
|
10
|
+
from eco_back.documento import AnexoManager, AnexoConfig, DocumentoResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def config_basica():
|
|
15
|
+
"""Configuración básica para tests"""
|
|
16
|
+
return AnexoConfig(
|
|
17
|
+
url_base="https://api.test.com",
|
|
18
|
+
max_size=1024 * 1024, # 1MB para tests
|
|
19
|
+
max_retries=2,
|
|
20
|
+
timeout=10
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def manager(config_basica):
|
|
26
|
+
"""Fixture del manager"""
|
|
27
|
+
return AnexoManager(config_basica)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_obtener_extension(manager):
|
|
31
|
+
"""Test de obtención de extensión"""
|
|
32
|
+
assert manager.obtener_extension("archivo.pdf") == ".pdf"
|
|
33
|
+
assert manager.obtener_extension("documento.PDF") == ".pdf"
|
|
34
|
+
assert manager.obtener_extension("imagen.JPG") == ".jpg"
|
|
35
|
+
assert manager.obtener_extension("sin_extension") == ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_validar_extension(manager):
|
|
39
|
+
"""Test de validación de extensiones"""
|
|
40
|
+
assert manager.validar_extension(".pdf") is True
|
|
41
|
+
assert manager.validar_extension("pdf") is True
|
|
42
|
+
assert manager.validar_extension(".jpg") is True
|
|
43
|
+
assert manager.validar_extension(".exe") is False
|
|
44
|
+
assert manager.validar_extension(".bat") is False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_obtener_mime_type(manager):
|
|
48
|
+
"""Test de obtención de tipo MIME"""
|
|
49
|
+
assert manager.obtener_mime_type("doc.pdf") == "application/pdf"
|
|
50
|
+
assert manager.obtener_mime_type("foto.jpg") == "image/jpeg"
|
|
51
|
+
assert manager.obtener_mime_type("foto.jpeg") == "image/jpeg"
|
|
52
|
+
assert manager.obtener_mime_type("imagen.png") == "image/png"
|
|
53
|
+
assert manager.obtener_mime_type("archivo.xyz") == "application/octet-stream"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_documento_result():
|
|
57
|
+
"""Test de DocumentoResult"""
|
|
58
|
+
resultado = DocumentoResult(
|
|
59
|
+
status="success",
|
|
60
|
+
mensaje="Todo bien",
|
|
61
|
+
detalles={"key": "value"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert resultado.status == "success"
|
|
65
|
+
assert resultado.exitoso is True
|
|
66
|
+
assert resultado.detalles["key"] == "value"
|
|
67
|
+
|
|
68
|
+
dict_result = resultado.to_dict()
|
|
69
|
+
assert dict_result["status"] == "success"
|
|
70
|
+
assert dict_result["exitoso"] is True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_documento_result_error():
|
|
74
|
+
"""Test de DocumentoResult con error"""
|
|
75
|
+
resultado = DocumentoResult(
|
|
76
|
+
status="error",
|
|
77
|
+
mensaje="Falló",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
assert resultado.exitoso is False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_guardar_anexo_extension_invalida(manager):
|
|
84
|
+
"""Test de rechazo por extensión inválida"""
|
|
85
|
+
archivo = BytesIO(b"contenido")
|
|
86
|
+
|
|
87
|
+
resultado = manager.guardar_anexo(
|
|
88
|
+
archivo=archivo,
|
|
89
|
+
nombre_archivo="malware.exe",
|
|
90
|
+
llave="123",
|
|
91
|
+
grupo="test",
|
|
92
|
+
tipo_documento="doc",
|
|
93
|
+
diccionario=1,
|
|
94
|
+
asincrono=False
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert resultado.exitoso is False
|
|
98
|
+
assert "no permitida" in resultado.mensaje
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_guardar_anexo_tamano_excedido(manager):
|
|
102
|
+
"""Test de rechazo por tamaño excedido"""
|
|
103
|
+
# Crear contenido que excede el límite
|
|
104
|
+
contenido = b"X" * (2 * 1024 * 1024) # 2MB (límite es 1MB)
|
|
105
|
+
|
|
106
|
+
resultado = manager.guardar_anexo(
|
|
107
|
+
archivo=contenido,
|
|
108
|
+
nombre_archivo="grande.pdf",
|
|
109
|
+
llave="123",
|
|
110
|
+
grupo="test",
|
|
111
|
+
tipo_documento="doc",
|
|
112
|
+
diccionario=1,
|
|
113
|
+
asincrono=False
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert resultado.exitoso is False
|
|
117
|
+
assert "excede tamaño" in resultado.mensaje
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_escanear_archivo_sin_funcion(manager):
|
|
121
|
+
"""Test de escaneo cuando no hay función configurada"""
|
|
122
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf") as tmp:
|
|
123
|
+
tmp.write(b"PDF content")
|
|
124
|
+
tmp.flush()
|
|
125
|
+
|
|
126
|
+
scan_result = manager._escanear_archivo(tmp.name, ".pdf")
|
|
127
|
+
assert scan_result['status'] == 'CLEAN'
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_escanear_archivo_con_funcion():
|
|
131
|
+
"""Test de escaneo con función personalizada"""
|
|
132
|
+
def scan_mock(path, ext):
|
|
133
|
+
return {'status': 'CLEAN', 'details': 'OK'}
|
|
134
|
+
|
|
135
|
+
config = AnexoConfig(
|
|
136
|
+
url_base="https://test.com",
|
|
137
|
+
scan_function=scan_mock
|
|
138
|
+
)
|
|
139
|
+
manager = AnexoManager(config)
|
|
140
|
+
|
|
141
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf") as tmp:
|
|
142
|
+
tmp.write(b"PDF content")
|
|
143
|
+
tmp.flush()
|
|
144
|
+
|
|
145
|
+
scan_result = manager._escanear_archivo(tmp.name, ".pdf")
|
|
146
|
+
assert scan_result['status'] == 'CLEAN'
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_obtener_endpoint(manager):
|
|
150
|
+
"""Test de obtención de endpoint según diccionario"""
|
|
151
|
+
assert manager._obtener_endpoint(1) == "usuario"
|
|
152
|
+
assert manager._obtener_endpoint(2) == "persona"
|
|
153
|
+
assert manager._obtener_endpoint(3) == "colectivo"
|
|
154
|
+
assert manager._obtener_endpoint(99) == "usuario" # Default
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_obtener_tipo_documento_sin_diccionario(manager):
|
|
158
|
+
"""Test de tipo de documento sin función de diccionario"""
|
|
159
|
+
tipo = manager._obtener_tipo_documento("cedula", 1)
|
|
160
|
+
assert tipo == "cedula" # Sin mapeo, devuelve original
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_obtener_tipo_documento_con_diccionario():
|
|
164
|
+
"""Test de tipo de documento con función de diccionario"""
|
|
165
|
+
def dict_func(tipo):
|
|
166
|
+
return {"cedula": "CC"}.get(tipo, tipo)
|
|
167
|
+
|
|
168
|
+
config = AnexoConfig(
|
|
169
|
+
url_base="https://test.com",
|
|
170
|
+
diccionario_registro=dict_func
|
|
171
|
+
)
|
|
172
|
+
manager = AnexoManager(config)
|
|
173
|
+
|
|
174
|
+
tipo = manager._obtener_tipo_documento("cedula", 1)
|
|
175
|
+
assert tipo == "CC"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@patch('eco_back.documento.anexos.requests.post')
|
|
179
|
+
def test_enviar_documento_sincrono_exitoso(mock_post, manager):
|
|
180
|
+
"""Test de envío síncrono exitoso"""
|
|
181
|
+
# Mock de respuesta exitosa
|
|
182
|
+
mock_response = Mock()
|
|
183
|
+
mock_response.status_code = 200
|
|
184
|
+
mock_response.json.return_value = {"success": True}
|
|
185
|
+
mock_response.content = b'{"success": true}'
|
|
186
|
+
mock_post.return_value = mock_response
|
|
187
|
+
|
|
188
|
+
# Crear archivo temporal
|
|
189
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
190
|
+
archivo_path = os.path.join(temp_dir, "test.pdf")
|
|
191
|
+
with open(archivo_path, 'wb') as f:
|
|
192
|
+
f.write(b"PDF content")
|
|
193
|
+
|
|
194
|
+
file_meta = {
|
|
195
|
+
'field': 'anexo',
|
|
196
|
+
'path': archivo_path,
|
|
197
|
+
'original_name': 'test.pdf',
|
|
198
|
+
'tipo_documento': 'cedula'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
resultado = manager._enviar_documento_sincrono(
|
|
202
|
+
file_meta=file_meta,
|
|
203
|
+
llave="123",
|
|
204
|
+
grupo="test",
|
|
205
|
+
tipo="cedula",
|
|
206
|
+
temp_dir=temp_dir,
|
|
207
|
+
diccionario=1
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert resultado.exitoso is True
|
|
211
|
+
assert resultado.status == "success"
|
|
212
|
+
assert mock_post.called
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@patch('eco_back.documento.anexos.requests.post')
|
|
216
|
+
def test_enviar_documento_con_reintentos(mock_post, manager):
|
|
217
|
+
"""Test de reintentos en caso de error del servidor"""
|
|
218
|
+
# Primera llamada falla con 500, segunda con 200
|
|
219
|
+
mock_response_error = Mock()
|
|
220
|
+
mock_response_error.status_code = 500
|
|
221
|
+
|
|
222
|
+
mock_response_ok = Mock()
|
|
223
|
+
mock_response_ok.status_code = 200
|
|
224
|
+
mock_response_ok.json.return_value = {"success": True}
|
|
225
|
+
mock_response_ok.content = b'{"success": true}'
|
|
226
|
+
|
|
227
|
+
mock_post.side_effect = [mock_response_error, mock_response_ok]
|
|
228
|
+
|
|
229
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
230
|
+
archivo_path = os.path.join(temp_dir, "test.pdf")
|
|
231
|
+
with open(archivo_path, 'wb') as f:
|
|
232
|
+
f.write(b"PDF content")
|
|
233
|
+
|
|
234
|
+
file_meta = {
|
|
235
|
+
'field': 'anexo',
|
|
236
|
+
'path': archivo_path,
|
|
237
|
+
'original_name': 'test.pdf',
|
|
238
|
+
'tipo_documento': 'cedula'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resultado = manager._enviar_documento_sincrono(
|
|
242
|
+
file_meta=file_meta,
|
|
243
|
+
llave="123",
|
|
244
|
+
grupo="test",
|
|
245
|
+
tipo="cedula",
|
|
246
|
+
temp_dir=temp_dir,
|
|
247
|
+
diccionario=1
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
assert resultado.exitoso is True
|
|
251
|
+
assert mock_post.call_count == 2
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@patch('eco_back.documento.anexos.requests.post')
|
|
255
|
+
def test_guardar_anexo_asincrono(mock_post, manager):
|
|
256
|
+
"""Test de guardado asíncrono"""
|
|
257
|
+
mock_response = Mock()
|
|
258
|
+
mock_response.status_code = 200
|
|
259
|
+
mock_response.json.return_value = {"success": True}
|
|
260
|
+
mock_response.content = b'{"success": true}'
|
|
261
|
+
mock_post.return_value = mock_response
|
|
262
|
+
|
|
263
|
+
contenido = b"PDF content"
|
|
264
|
+
|
|
265
|
+
resultado = manager.guardar_anexo(
|
|
266
|
+
archivo=contenido,
|
|
267
|
+
nombre_archivo="test.pdf",
|
|
268
|
+
llave="123",
|
|
269
|
+
grupo="test",
|
|
270
|
+
tipo_documento="cedula",
|
|
271
|
+
diccionario=1,
|
|
272
|
+
asincrono=True
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
assert resultado.exitoso is True
|
|
276
|
+
assert resultado.status == "processing"
|
|
277
|
+
assert "proceso" in resultado.mensaje
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|