Skip to content

Commit 16b03fa

Browse files
committed
Agrega endpoints de descarga completa de datos.
1 parent 5d49fd0 commit 16b03fa

File tree

7 files changed

+130
-12
lines changed

7 files changed

+130
-12
lines changed

.travis.yml

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ install:
66
- pip install -r requirements.txt -r requirements-dev.txt
77
- sudo apt-get update -qq
88
- sudo apt-get install -y openvpn
9-
env:
10-
- CFG_PATH=config/georef.example.cfg
119
script:
1210
- make code_checks
1311
- make test_mock

Makefile

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
CFG_PATH ?= config/georef.cfg
1+
# Makefile para georef-ar-api
2+
#
3+
# Contiene recetas para ejecutar tests, correr servidores de prueba
4+
# y generar la documentación.
5+
6+
CFG_PATH ?= config/georef.example.cfg
27
INDEX_NAME ?= all
38
INDEXER_PY = service.management.indexer
49

config/georef.example.cfg

+34
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ MAX_RESULT_LEN = 5000
1515
# a el valor index.max_result_window de Elasticsearch (default: 10000).
1616
MAX_RESULT_WINDOW = 10000
1717

18+
# URLs de endpoints de descarga completa de datos Por ejemplo, el
19+
# usuario puede acceder a /api/departamentos.csv para descargarse la
20+
# base total de departamentos. Internamente la api realiza un HTTP
21+
# redirect a las URLs configuradas en COMPLETE_DOWNLOAD_URLS. En caso
22+
# establecer una URL como None, se desactivará el endpoint asociado.
23+
24+
COMPLETE_DOWNLOAD_URLS = {
25+
'provincias': {
26+
'json': 'https://www.example.org',
27+
'csv': None,
28+
'geojson': None
29+
},
30+
'departamentos': {
31+
'json': None,
32+
'csv': None,
33+
'geojson': None
34+
},
35+
'municipios': {
36+
'json': None,
37+
'csv': None,
38+
'geojson': None
39+
},
40+
'localidades': {
41+
'json': None,
42+
'csv': None,
43+
'geojson': None
44+
},
45+
'calles': {
46+
# calles no tiene archivo geojson asociado
47+
'json': None,
48+
'csv': None
49+
}
50+
}
51+
1852
#------------------------------------------------------------
1953
# Configuración para indexación de datos
2054
#------------------------------------------------------------

service/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
with app.app_context():
1212
# Crear parsers de parámetros utilizando configuración de Flask app
1313
import service.params
14-
15-
import service.routes # noqa: E402,F401 pylint: disable=wrong-import-position
14+
# Crear rutas utilizando también configuración de Flask
15+
import service.routes # noqa: F401 pylint: disable=wrong-import-position

service/formatter.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -248,20 +248,28 @@ def create_param_error_response_bulk(errors):
248248
}), 400)
249249

250250

251-
def create_404_error_response(url_map):
251+
def create_404_error_response():
252252
"""Retorna un error HTTP con código 404.
253253
254-
Args:
255-
url_map (werkzeug.routing.Map): Mapa de URLs de la aplicación Flask.
256-
257254
Returns:
258255
flask.Response: Respuesta HTTP con error 404.
259256
260257
"""
261258
errors = [
262259
{
263260
'mensaje': strings.NOT_FOUND,
264-
'recursos_disponibles': list(str(r) for r in url_map.iter_rules())
261+
# El listado de recursos podría utilizar app.url_map, pero es mejor
262+
# listar los elementos más importantes manualmente para que la
263+
# respuesta sea de mayor utilidad para el usuario.
264+
'recursos_disponibles': [
265+
'/api/provincias',
266+
'/api/departamentos',
267+
'/api/municipios',
268+
'/api/localidades',
269+
'/api/calles',
270+
'/api/direcciones',
271+
'/api/ubicacion'
272+
]
265273
}
266274
]
267275

service/routes.py

+35-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"""
66

77
from functools import wraps
8-
from flask import request, Blueprint
8+
from flask import current_app, request, redirect, Blueprint
99
from service import app, normalizer, formatter
10+
from service import names as N
1011

1112

1213
def disable_cache(f):
@@ -29,9 +30,39 @@ def decorated_func(*args, **kwargs):
2930
return decorated_func
3031

3132

33+
def add_complete_downloads(bp, urls):
34+
"""Agrega endpoints de descarga completa de datos a un Flask Blueprint.
35+
36+
Args:
37+
bp (flask.Blueprint): Objeto donde agregar los endpoints de descarga.
38+
urls (dict): Diccionario con tipos de entidades como claves, y
39+
diccionarios como valores. Cada subdiccionario debe contener, por
40+
cada formato (CSV, JSON, GEOJSON), una URL a donde redirigir (o
41+
None para no agregar el endpoint). Ver el archivo
42+
georef.example.cfg para más detalles.
43+
44+
"""
45+
entities = [N.STATES, N.DEPARTMENTS, N.MUNICIPALITIES, N.LOCALITIES,
46+
N.STREETS]
47+
formats = ['json', 'csv', 'geojson']
48+
49+
for entity in entities:
50+
entity_urls = urls[entity]
51+
52+
for fmt in formats:
53+
if entity_urls.get(fmt):
54+
url = entity_urls[fmt]
55+
# e.g: /provincias.csv
56+
endpoint = '{}-{}'.format(entity, fmt)
57+
rule = '/{}.{}'.format(entity, fmt)
58+
59+
bp.add_url_rule(rule, endpoint,
60+
lambda location=url: redirect(location))
61+
62+
3263
@app.errorhandler(404)
3364
def handle_404(_):
34-
return formatter.create_404_error_response(app.url_map)
65+
return formatter.create_404_error_response()
3566

3667

3768
@app.errorhandler(405)
@@ -42,6 +73,8 @@ def handle_405(_):
4273
# API v1.0
4374
bp_v1_0 = Blueprint('georef_v1.0', __name__)
4475

76+
add_complete_downloads(bp_v1_0, current_app.config['COMPLETE_DOWNLOAD_URLS'])
77+
4578

4679
@bp_v1_0.route('/provincias', methods=['GET', 'POST'])
4780
def get_states():

tests/test_mock_routes.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import logging
2+
from unittest import TestCase
3+
from service import app
4+
5+
6+
logging.getLogger('georef').setLevel(logging.CRITICAL)
7+
8+
9+
class RoutesTest(TestCase):
10+
def setUp(self):
11+
app.testing = True
12+
self.app = app.test_client()
13+
14+
def test_v1_0_endpoints(self):
15+
"""Los endpoints con prefijo /api/v1.0 deberían existir incluso si no
16+
se cuenta con más de una versión de la API."""
17+
urls = [
18+
'/api/v1.0/provincias',
19+
'/api/v1.0/departamentos',
20+
'/api/v1.0/municipios',
21+
'/api/v1.0/localidades',
22+
'/api/v1.0/direcciones',
23+
'/api/v1.0/ubicacion'
24+
]
25+
26+
validations = [
27+
self.app.options(url).status_code == 200
28+
for url in urls
29+
]
30+
31+
self.assertTrue(all(validations), list(zip(urls, validations)))
32+
33+
def test_complete_download_redirect(self):
34+
"""La API debería permitir la descarga total de datos por recurso. Las
35+
descargas se implementan como una redirección a una URL donde se
36+
almacenan los datos a descargarse (HTTP 302). La configuración de
37+
ejemplo de la API utiliza una URL de ejemplo para /provincias.json."""
38+
resp = self.app.get('/api/provincias.json')
39+
self.assertTrue(resp.status_code == 302 and
40+
resp.headers['Location'] == 'https://www.example.org')

0 commit comments

Comments
 (0)