Creando un wrapper con Python de la API del Geocodificador SCN

7 minuto de lectura

En la siguiente entrada voy a exponer un caso sencillo de acceso y consumo a datos geográficos a través de una API pública usando el lenguaje de programación Python.

Python dispone de librerías como urlib o requests para realizar peticiones http a servicios Rest API. El siguiente código es un ejemplo de cómo podríamos obtener los datos de la API que vamos a usar dentro de un script.

import json
from urllib import request

url = 'https://geocoder-5-ign.larioja.org/v1/search?text=Sevilla'

with request.urlopen(url) as resp:
    data = json.loads(resp.read().decode('utf-8'))
    print(data)

Intentando como siempre dar un paso más, en esta ocasión usaremos el paradigma de programación orientado a objetos (POO) y algunos aspectos básicos del desarrollo dirigido por test (TDD o Test-Driven Development) para crear un wrapper que permita abstraer el acceso a la API y personalizar tanto los datos obtenidos (atributos) como la forma de obtenerlos y manejarlos (métodos).

La API: Servicio de Geocodificación de direcciones del Sistema Cartográfico Nacional

Gracias a unos tuits de Gonzalo López de IDERioja @gonzalo_lpgc conocí el Servicio de Geocodificación de direcciones del Sistema Cartográfico Nacional (España). Según la documentación, esta Interfaz de Programación de Aplicaciones de geocodificación pública está diseñada para “buscar y obtener a partir de sus datos identificativos, la localización geográfica de cualquier dirección que se encuentre ubicada dentro del territorio español”.

Poder abrir este tipo de recursos geográficos y hacerlo en esta ocasión mediante un servicio de estas características permite la posibilidad de implementar de funciones de geocodificación dentro aplicaciones escritas en diferentes lenguajes. Muy muy interesante ¿verdad?

Como punto a su favor, hay también que decir que el servicio está basado en Pelias, un geocodificador modular de código abierto sobre Elasticsearch desarrollado por Mapzen.

Sin entrar en mucho detalle, los endpoints del Geocodificador de SCN permiten realizar:

  • Geocodificación directa (/v1/search): Método que obtiene los datos y la ubicación de una dirección o lugar, a partir de sus datos identificativos o de referencia.
  • Geocodificación inversa (/v1/reverse): Método que busca las direcciones más próximas a un punto geográfico determinado.
  • Autocompletar (/v1/autocomplete): Método que obtiene resultados en tiempo real sin necesidad de completar todos los datos de identificación.
  • Geocodificación estructurada (/v1/search/structured): (beta) Método que encuentra un lugar a partir de datos estructurados en calle, número, ciudad, etc.
  • Lugar (/v1/place): Método que obtiene detalles sobre un lugar devuelto por una consulta anterior.

En este primer acercamiento se va a implementar una clase que permite almacenar atributos y datos vinculados con la geocodificación directa e indirecta a partir de los parámetros básicos.

Test-Driven Development

Aunque he realizado (pocos) algunos pinitos de testing en JavaScript, tenía mucho interés en comenzar a aprender cómo realizar desarrollos basados en pruebas.

Esta técnica de diseño e implementación de software implica escribir las pruebas primero (Test First Development) y posteriormente refactorizar (refactoring). Debemos partir de unos casos de usos, escribir la prueba, verificar que falla, realizar la implementación, lanzar las pruebas de nuevo y refactorizar.

Lifecycle of the Test-Driven Development method. Fuente: Wikipedia Lifecycle of the Test-Driven Development method. Fuente: Wikipedia

Python cuenta con la librería estándar unittest para pruebas unitarias. A pesar de ello, las consultas previas sobre este tema apuntaban al uso del paquete de terceros pytest. Este marco de pruebas no necesitar crear clases como en unittest y el manejo aserciones (asserts) parece ser más sencillo.

La mejor forma de usar librerías externas en crear un entorno virtual en Python e instalarlas. Llevo algún tiempo usado PyCharm y la gestión de entornos virtuales es bastante sencilla en este sentido

Creación de la clase GeocoderSCN

El wrapper o adaptador de la REST API estará compuesto por una clase que he llamado GeocoderSCN. En sus atributos no solo se va a almacenar el GeoJSON de la API, es decir aquellos elementos puntuales que coincidan con la dirección indicada. Quiero también tener disponible la URL de la petición, el texto de búsqueda, el código de la petición HTTP, el total de coincidencias obtenidas y un registro de posibles errores.

Lo primero que vamos a hacer es crear el archivo que almacenará la clase. En vez de usar el método reservado init como constructor de la clase definiremos los atributos con el módulo dataclasses y el decorador dataclass.

from dataclasses import dataclass

@dataclass
class GeocoderSCN:
    """Class for saving the information from the SCN geocoder"""
    searchtext: str

Escribiendo los test

Nuestro primer test está destinado a crear los atributos de la clase. Los script pruebas deben teber el prefijo ‘test_’ o bien ser almacenados en un carpeta denominada ‘test’

Para lanzarlos se usa el comando pytest o se ejecuta desde el framework.

# test/test_geocoderscn.py

def test_attributes():
    my_geocoderSCN = GeocoderSCN('Plaza de las Tendillas 1 Córdoba')
    assert my_geocoderSCN.search_text != ''
    assert my_geocoderSCN.endpoint is None
    assert my_geocoderSCN.feature_count == 0
    assert my_geocoderSCN.api_data is None
    assert my_geocoderSCN.status == 0
    assert my_geocoderSCN.error is False
    assert my_geocoderSCN.messages is None

Al ejecutarlo vemos que el test falla.

Test sin pasar

Es el momento de añadir los atributos a nuestra clase.

@dataclass
class GeocoderSCN:
    """Class for saving the information from the SCN geocoder"""
    searchtext: str
    endpoint: str = None
    feature_count: int = 0
    api_data: str = None
    status: int = 0
    error: bool = False
    messages: str = None

Lanzamos de nuevo los test y vemos que han pasado.

Test pasados

Función de geolocalización directa.

La documentación de la API nos indica que las búsquedas de geocodificación directa. Vemos un ejemplo.

https://geocoder-5-ign.larioja.org/v1/search?text=Plaza de las Tendillas 1 Córdoba

resultados de geocidifiación directa

Existe también una aplicación gráfica para testear los servicios basados en Pelias que ofrece una interfaz más amigable además de la representación de los resultados en un mapa.

https://pelias.github.io/compare/

Programa de pruebas de servicios basados en Pelias

La API de devuelve un GeoJSON donde encontramos una información general de la búsqueda (geocoding) y a continuación los datos y coordenadas de cada uno de los resultados. La petición estará implementada dentro de la clase en un método denominada search. Como librerías necesarias se usará urllib y json. El método evaluará los resultados y almacenará no solo el GeoJSON de respuesta sino los datos de interés (endpoint, total de resultados y posibles errores).

Como parámetros del método search() se pasará por defecto:

  • La URL de la API (api=API) pasando una variable global con la dirección actual del servicio.
  • El parámetro layers=’address’ que corresponde con datos sobre puntos con una dirección postal. Existe también las opciones de calles, vías y carreteras (street) o topónimos, puntos de interés y nombres propios de las direcciones (venue)
  • El parámetro size con el número deseado de resultados que por defecto son 10.

Implementamos un primer test con las aserciones esperadas. En esta ocasión dentro de los test usaremos las fixtures de pytest que preinicializan datos, son reaprovechables y disminuyen las líneas de código.

@pytest.fixture
def my_GeocoderSCN():
    my_geocoderSCN = GeocoderSCN('Acera Fuente de los Picadores 2 Córdoba')
    return my_geocoderSCN

@pytest.fixture
def my_GeocoderSCN_search(my_GeocoderSCN):
    my_GeocoderSCN.search()
    return my_GeocoderSCN

....

def test_search(my_GeocoderSCN_search):
    assert my_GeocoderSCN_search.search_text != ''
    assert my_GeocoderSCN_search.endpoint != ''
    assert my_GeocoderSCN_search.api_data is not None
    assert my_GeocoderSCN_search.feature_count > 0
    assert my_GeocoderSCN_search.status == 200
    assert my_GeocoderSCN_search.error is False
    assert my_GeocoderSCN_search.messages == "it's all OK!"

El código del método test válido es el siguiente:


    def search(self, api=API, layers='address', size=10):
        text_parse = parse.quote_plus(self.search_text)
        self.endpoint = f'{API}/search?text={self.search_text}&layers={layers}&size={size}'
        url = f'{api}/search?text={text_parse}&layers={layers}&size={size}'
        try:
            with request.urlopen(url) as resp:
                self.status = resp.status
                self.api_data = json.loads(resp.read().decode('utf-8'))
                self.feature_count = len(self.api_data['features'])
                self.messages = "it's all OK!"

        except HTTPError as e:
            self.error =  True
            self.status = e.code
            self.messages = e

        except URLError as e:
            self.error = True
            self.messages = e.reason

Nuestra clase incorpora dos métodos más:

  • reverse() para realizar la geocodificación inversa
  • get_list() Un sencillo método para obtener una lista de diccionarios con los datos básicos (longitud, latitud, etiqueta y fuente)

Todo el código está disponible en GitHub

Y todo esto ¿para qué?

Personalmente tenía ganas de indagar en las posibilidades de la programación orientada a objetos desde Python. Contar con un objeto en el que almacenar los atributos que modelemos y definir tus propios métodos creo que tiene mucha utilidad.

Cuando conocí esta REST API lo primero que me vino a la cabeza fue montar un pluging de QGIS. La cuestión está en que ya existe un complemento llamado Pelias Geocoding con todas las funcionalidades incorpoadas al que solo hace falta añadir la url proveedor.

Pluging Pelias Geocoder

Este descubrimiento fue algo posterior. A pesar de ello y sún las experiencas anteriores de desarrollo complementos con acceso a datos públicos ya habían aparecedo algunas preguntas en mi cabeza:

  • ¿Y sí cambia la dirección de la API?
  • ¿Cómo gestiono los posibles errores de conexión o falta de parámetros?
  • ¿Y si necesito sacar solo unos datos fuera del GeoJSON?
  • ¿Podría reutilizar el código fuera del pluging de QGIS, por ejemplo con Jupiter Lab?

Todas estas preguntas me llevaron a poner en marcha este pequeño ‘side-project’ para abstraer el de acceso a la API mediante un clase y añadir test del desarrollo. La librería código puede puede incluso convertirse en una librería pública y ser instalada y usada por cualquier usuario. Ya hay una similar denominada pyPelias para cualquier servicio que use Pelias. He probado a usarla la que yo he creado con Jupyter Lab y pienso que puede dar mucho juego.

Otro gran tema está en los tests. Ahora mismo programar pensando en los tests ha sido más lento pero esto se debe más a mi desconocimiento inicial del Test-Driven Development. Los beneficios obtenidos superan con creces esta lentitud inicial. Me ha hecho ser más preciso al definir los caso de uso, he aprovechado más el código testeado y aunque siempre puede haber fallos, mi sensación es que el código es más robusto y limpio.

Comentar