diff --git a/pydatajson/__init__.py b/pydatajson/__init__.py index e162251..e470a7d 100644 --- a/pydatajson/__init__.py +++ b/pydatajson/__init__.py @@ -9,7 +9,15 @@ from .core import DataJson from .helpers import parse_repeating_time_interval from . import helpers +import logging __author__ = """Datos Argentina""" __email__ = 'datos@modernizacion.gob.ar' __version__ = '0.4.15' + +""" +Logger base para librería pydatajson +https://docs.python.org/2/howto/logging.html#configuring-logging-for-a-library +""" +logger = logging.getLogger('pydatajson') +logger.addHandler(logging.NullHandler()) diff --git a/pydatajson/backup.py b/pydatajson/backup.py index 8be7387..cd81790 100644 --- a/pydatajson/backup.py +++ b/pydatajson/backup.py @@ -8,7 +8,7 @@ from __future__ import with_statement import os import traceback -from pprint import pprint +import logging import pydatajson from .helpers import ensure_dir_exists @@ -16,6 +16,8 @@ CATALOGS_DIR = "" +logger = logging.getLogger('pydatajson') + def make_catalogs_backup(catalogs, local_catalogs_dir="", include_metadata=True, include_data=True, @@ -55,9 +57,8 @@ def make_catalogs_backup(catalogs, local_catalogs_dir="", include_metadata=include_metadata, include_metadata_xlsx=include_metadata_xlsx, include_data=include_data) - except Exception as e: - print("ERROR en {}".format(catalog)) - traceback.print_exc() + except Exception: + logger.exception("ERROR en {}".format(catalog)) elif isinstance(catalogs, dict): for catalog_id, catalog in catalogs.iteritems(): @@ -68,9 +69,9 @@ def make_catalogs_backup(catalogs, local_catalogs_dir="", include_metadata=include_metadata, include_metadata_xlsx=include_metadata_xlsx, include_data=include_data) - except Exception as e: - print("ERROR en {} ({})".format(catalog, catalog_id)) - traceback.print_exc() + except Exception: + logger.exception( + "ERROR en {} ({})".format(catalog, catalog_id)) def make_catalog_backup(catalog, catalog_id=None, local_catalogs_dir="", @@ -100,8 +101,9 @@ def make_catalog_backup(catalog, catalog_id=None, local_catalogs_dir="", catalog_identifier = catalog_id if catalog_id else catalog["identifier"] if include_metadata: - print("Descargando catálogo {}".format(catalog_identifier.ljust(30)), - end="\r") + logger.info( + "Descargando catálogo {}".format( + catalog_identifier.ljust(30))) # catálogo en json catalog_path = get_catalog_path(catalog_identifier, local_catalogs_dir) @@ -120,8 +122,8 @@ def make_catalog_backup(catalog, catalog_id=None, local_catalogs_dir="", distributions_num = len(distributions) for index, distribution in enumerate(distributions): - print("Descargando distribución {} de {} ({})".format( - index + 1, distributions_num, catalog_identifier), end="\r") + logger.info("Descargando distribución {} de {} ({})".format( + index + 1, distributions_num, catalog_identifier)) dataset_id = distribution["dataset_identifier"] distribution_id = distribution["identifier"] diff --git a/pydatajson/ckan_reader.py b/pydatajson/ckan_reader.py index 9006d20..88711e7 100644 --- a/pydatajson/ckan_reader.py +++ b/pydatajson/ckan_reader.py @@ -17,6 +17,8 @@ import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +logger = logging.getLogger('pydatajson') + ABSOLUTE_PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(ABSOLUTE_PROJECT_DIR, "schemas", @@ -29,9 +31,6 @@ RAW_SUPER_THEMES = json.load(super_themes) SUPER_THEMES = {row["label"]: row["id"] for row in RAW_SUPER_THEMES} -logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', - datefmt='%m/%d/%Y %I:%M:%S') - def read_ckan_catalog(portal_url): """Convierte los metadatos de un portal disponibilizados por la Action API @@ -61,7 +60,7 @@ def read_ckan_catalog(portal_url): for index, pkg in enumerate(packages_list): # progreso (necesario cuando son muchos) msg = "Leyendo dataset {} de {}".format(index + 1, num_packages) - print(msg, end="\r") + logger.info(msg) # agrega un nuevo dataset a la lista packages.append(portal.call_action( @@ -81,7 +80,7 @@ def read_ckan_catalog(portal_url): catalog["themeTaxonomy"] = map_groups_to_themes(groups) except: - logging.error( + logger.exception( 'Error al procesar el portal %s', portal_url, exc_info=True) return catalog @@ -101,7 +100,7 @@ def map_status_to_catalog(status): try: catalog[catalog_key] = status[status_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en el endpoint de status. No se puede completar catalog['%s'].""", status_key, catalog_key) @@ -116,11 +115,11 @@ def map_status_to_catalog(status): try: catalog['publisher'][publisher_key] = status[status_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en el endpoint de status. No se puede completar catalog['publisher'['%s'].""", status_key, publisher_key) else: - logging.info(""" + logger.info(""" No hay ninguna información sobre catalog['publisher'] en el endpoint de 'status'.""") @@ -158,7 +157,7 @@ def map_package_to_dataset(package, portal_url): try: dataset[dataset_key] = package[package_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en el endpoint 'package_show' para el package '%s'. No se puede completar dataset['%s'].""", package_key, package['name'], dataset_key) @@ -174,7 +173,7 @@ def map_package_to_dataset(package, portal_url): try: dataset['publisher'][publisher_key] = package[package_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en el endpoint 'package_show' para el package '%s'. No se puede completar dataset['publisher']['%s'].""", package_key, package['name'], publisher_key) @@ -190,7 +189,7 @@ def map_package_to_dataset(package, portal_url): try: dataset['contactPoint'][contact_key] = package[package_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en el endpoint 'package_show' para el package '%s'. No se puede completar dataset['contactPoint']['%s'].""", package_key, package['name'], contact_key) @@ -219,7 +218,7 @@ def add_temporal(dataset, package): ] if len(temporal) > 1: - logging.info(""" + logger.info(""" Se encontro mas de un valor de cobertura temporal en 'extras' para el 'package' '%s'. No se puede completar dataset['temporal'].\n %s""", package['name'], temporal) @@ -227,7 +226,7 @@ def add_temporal(dataset, package): try: dataset["temporal"] = temporal[0] except KeyError: - logging.warn(""" + logger.exception(""" Se encontró '%s' como cobertura temporal, pero no es mapeable a un 'temporal' conocido. La clave no se pudo completar.""", temporal[0]) @@ -239,7 +238,7 @@ def add_temporal(dataset, package): extra["key"] != "Cobertura temporal"] if almost_temporal: - logging.warn(""" + logger.warn(""" Se encontraron claves con nombres similares pero no idénticos a "Cobertura temporal" en 'extras' para el 'package' '%s'. Por favor, considere corregirlas: @@ -254,12 +253,12 @@ def add_superTheme(dataset, package): ] if len(super_theme) == 0: - logging.info(""" + logger.info(""" No se encontraron valores de temática global en 'extras' para el 'package' '%s'. No se puede completar dataset['superTheme'].""", package['name']) elif len(super_theme) > 1: - logging.info(""" + logger.info(""" Se encontro mas de un valor de temática global en 'extras' para el 'package' '%s'. No se puede completar dataset['superTheme'].\n %s""", package['name'], super_theme) @@ -267,7 +266,7 @@ def add_superTheme(dataset, package): try: dataset["superTheme"] = [SUPER_THEMES[super_theme[0]]] except KeyError: - logging.warn(""" + logger.exception(""" Se encontró '%s' como temática global, pero no es mapeable a un 'superTheme' conocido. La clave no se pudo completar.""", super_theme[0]) @@ -279,7 +278,7 @@ def add_superTheme(dataset, package): extra["key"] != "Temática global"] if almost_super_theme: - logging.warn(""" + logger.warn(""" Se encontraron claves con nombres similares pero no idénticos a "Temática global" en 'extras' para el 'package' '%s'. Por favor, considere corregirlas: \n%s""", package['name'], almost_accrual) @@ -294,12 +293,12 @@ def add_accrualPeriodicity(dataset, package): ] if len(accrual) == 0: - logging.info(""" + logger.info(""" No se encontraron valores de frecuencia de actualización en 'extras' para el 'package' '%s'. No se puede completar dataset['accrualPeriodicity'].""", package['name']) elif len(accrual) > 1: - logging.info(""" + logger.info(""" Se encontro mas de un valor de frecuencia de actualización en 'extras' para el 'package' '%s'. No se puede completar dataset['accrualPeriodicity'].\n %s""", package['name'], accrual) @@ -307,7 +306,7 @@ def add_accrualPeriodicity(dataset, package): try: dataset["accrualPeriodicity"] = FREQUENCIES[accrual[0]] except KeyError: - logging.warn(""" + logger.exception(""" Se encontró '%s' como frecuencia de actualización, pero no es mapeable a una 'accrualPeriodicity' conocida. La clave no se pudo completar.""", accrual[0]) @@ -319,7 +318,7 @@ def add_accrualPeriodicity(dataset, package): extra["key"] != "Frecuencia de actualización"] if almost_accrual: - logging.warn(""" + logger.warn(""" Se encontraron claves con nombres similares pero no idénticos a "Frecuencia de actualización" en 'extras' para el 'package' '%s'. Por favor, considere corregirlas:\n%s""", package['name'], almost_accrual) @@ -351,7 +350,7 @@ def map_resource_to_distribution(resource, portal_url): try: distribution[distribution_key] = resource[resource_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en la metadata del 'resource' '%s'. No se puede completar distribution['%s'].""", resource_key, resource['name'], distribution_key) @@ -382,7 +381,7 @@ def map_group_to_theme(group): try: theme[theme_key] = group[group_key] except: - logging.info(""" + logger.exception(""" La clave '%s' no está en la metadata del 'group' '%s'. No se puede completar theme['%s'].""", group_key, theme['name'], theme_key) diff --git a/pydatajson/ckan_utils.py b/pydatajson/ckan_utils.py index b853020..cb63393 100644 --- a/pydatajson/ckan_utils.py +++ b/pydatajson/ckan_utils.py @@ -4,11 +4,14 @@ import json import re +import logging from datetime import time from dateutil import parser, tz from .helpers import title_to_name from . import custom_exceptions as ce +logger = logging.getLogger('pydatajson') + def append_attribute_to_extra(package, dataset, attribute, serialize=False): value = dataset.get(attribute) @@ -83,8 +86,8 @@ def map_dataset_to_package(catalog, dataset, owner_org, catalog_id=None, try: label = _get_theme_label(catalog, theme) package['tags'].append({'name': label}) - except Exception as e: - print(e) + except Exception: + logger.exception('Theme no presente en catálogo.') continue else: package['groups'] = package.get('groups', []) + [ diff --git a/pydatajson/core.py b/pydatajson/core.py index 3c8701b..19ef86d 100644 --- a/pydatajson/core.py +++ b/pydatajson/core.py @@ -17,6 +17,7 @@ import re import sys import warnings +import logging from collections import OrderedDict from datetime import datetime @@ -36,6 +37,9 @@ from . import transformation from . import backup +logger = logging.getLogger('pydatajson') + + ABSOLUTE_PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) CENTRAL_CATALOG = "http://datos.gob.ar/data.json" DATA_FORMATS = [ @@ -182,11 +186,11 @@ def remove_dataset(self, identifier): for index, dataset in enumerate(self["dataset"]): if dataset["identifier"] == identifier: self["dataset"].pop(index) - print("Dataset {} en posicion {} fue eliminado.".format( + logger.info("Dataset {} en posicion {} fue eliminado.".format( identifier, index)) return - print("No se encontro el dataset {}.".format(identifier)) + logger.warning("No se encontro el dataset {}.".format(identifier)) def remove_distribution(self, identifier, dataset_identifier=None): for dataset in self["dataset"]: @@ -195,11 +199,11 @@ def remove_distribution(self, identifier, dataset_identifier=None): (not dataset_identifier or dataset["identifier"] == dataset_identifier)): dataset["distribution"].pop(index) - print("Distribution {} del dataset {} en posicion {} fue eliminada.".format( + logger.info("Distribution {} del dataset {} en posicion {} fue eliminada.".format( identifier, dataset["identifier"], index)) return - print("No se encontro la distribucion {}.".format(identifier)) + logger.warning("No se encontro la distribucion {}.".format(identifier)) def is_valid_catalog(self, catalog=None): catalog = catalog or self @@ -1157,13 +1161,13 @@ def main(): full_res = dj_instance.validate_catalog(datajson_file) pretty_full_res = json.dumps( full_res, indent=4, separators=(",", ": ")) - print(bool_res) - print(pretty_full_res) + logger.info(bool_res) + logger.info(pretty_full_res) except IndexError as errmsg: format_str = """ {}: pydatajson.py fue ejecutado como script sin proveer un argumento """ - print(format_str.format(errmsg)) + logger.error(format_str.format(errmsg)) if __name__ == '__main__': diff --git a/pydatajson/federation.py b/pydatajson/federation.py index 13703a5..c2bd144 100644 --- a/pydatajson/federation.py +++ b/pydatajson/federation.py @@ -11,7 +11,7 @@ from .ckan_utils import map_dataset_to_package, map_theme_to_group from .search import get_datasets -logger = logging.getLogger(__name__) +logger = logging.getLogger('pydatajson.federation') def push_dataset_to_ckan(catalog, owner_org, dataset_origin_identifier, @@ -86,9 +86,9 @@ def remove_harvested_ds_from_ckan(catalog, portal_url, apikey, for harvested_id in harvested_ids: try: remove_dataset_from_ckan(harvested_id, portal_url, apikey) - print("{} eliminado de {}".format(harvested_id, catalog_id)) - except: - print("{} de {} no existe.".format(harvested_id, catalog_id)) + logger.info("{} eliminado de {}".format(harvested_id, catalog_id)) + except Exception: + logger.exception("{} de {} no existe.".format(harvested_id, catalog_id)) def remove_datasets_from_ckan(portal_url, apikey, filter_in=None, diff --git a/pydatajson/indicators.py b/pydatajson/indicators.py index b23ca9b..b80dcfc 100644 --- a/pydatajson/indicators.py +++ b/pydatajson/indicators.py @@ -9,6 +9,7 @@ from __future__ import print_function, absolute_import, unicode_literals, with_statement +import logging import json import os from datetime import datetime @@ -23,6 +24,8 @@ ABSOLUTE_PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) CATALOG_FIELDS_PATH = os.path.join(ABSOLUTE_PROJECT_DIR, "fields") +logger = logging.getLogger('pydatajson') + def generate_catalogs_indicators(catalogs, central_catalog=None, validator=None): @@ -569,7 +572,7 @@ def datasets_equal(dataset, other, fields_dataset=None, other_distributions = other.get("distribution") if len(dataset_distributions) != len(other_distributions): - print("{} distribuciones en origen y {} en destino".format( + logger.info("{} distribuciones en origen y {} en destino".format( len(dataset_distributions), len(other_distributions))) dataset_is_equal = False diff --git a/pydatajson/readers.py b/pydatajson/readers.py index 3dc2d58..f3d3d9a 100644 --- a/pydatajson/readers.py +++ b/pydatajson/readers.py @@ -30,7 +30,7 @@ import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -global_logger = logging.getLogger() +pydj_logger = logging.getLogger('pydatajson.readers') def read_catalog_obj(catalog): @@ -217,7 +217,7 @@ def read_xlsx_catalog(xlsx_path_or_url, logger=None): dict: El diccionario que resulta de procesar xlsx_path_or_url. """ - logger = logger or global_logger + logger = logger or pydj_logger assert isinstance(xlsx_path_or_url, string_types) parsed_url = urlparse(xlsx_path_or_url) @@ -271,7 +271,7 @@ def _get_dataset_index(catalog, dataset_identifier, dataset_title, logger=None): """Devuelve el índice de un dataset en el catálogo en función de su identificador""" - logger = logger or global_logger + logger = logger or pydj_logger matching_datasets = [] for idx, dataset in enumerate(catalog["catalog_dataset"]): @@ -293,10 +293,10 @@ def _get_dataset_index(catalog, dataset_identifier, dataset_title, many_dsets_msg = "Hay mas de un dataset con el identifier {}: {}".format( dataset_identifier, matching_datasets) if len(matching_datasets) == 0: - print(no_dsets_msg) + logger.error(no_dsets_msg) return None elif len(matching_datasets) > 1: - print(many_dsets_msg) + logger.error(many_dsets_msg) return None else: return matching_datasets[0] @@ -308,7 +308,7 @@ def _get_distribution_indexes(catalog, dataset_identifier, dataset_title, """Devuelve el índice de una distribución en su dataset en función de su título, junto con el índice de su dataset padre en el catálogo, en función de su identificador""" - logger = logger or global_logger + logger = logger or pydj_logger dataset_index = _get_dataset_index( catalog, dataset_identifier, dataset_title) if dataset_index is None: @@ -361,7 +361,7 @@ def read_local_xlsx_catalog(xlsx_path, logger=None): Returns: dict: Diccionario con los metadatos de un catálogo. """ - logger = logger or global_logger + logger = logger or pydj_logger assert xlsx_path.endswith(".xlsx"), """ El archivo a leer debe tener extensión XLSX.""" @@ -409,7 +409,7 @@ def read_local_xlsx_catalog(xlsx_path, logger=None): catalog, distribution["dataset_identifier"], distribution["dataset_title"], logger) if dataset_index is None: - print("""La distribucion con ID '{}' y titulo '{}' no se + logger.error("""La distribucion con ID '{}' y titulo '{}' no se pudo asignar a un dataset, y no figurara en el data.json de salida.""".format( distribution["distribution_identifier"], distribution["distribution_title"])) @@ -431,14 +431,14 @@ def read_local_xlsx_catalog(xlsx_path, logger=None): logger) if dataset_index is None: - print("""No se encontro el dataset '{}' especificado para el campo + logger.error("""No se encontro el dataset '{}' especificado para el campo '{}' (fila #{} de la hoja "Field"). Este campo no figurara en el data.json de salida.""".format( unidecode(field["dataset_title"]), unidecode(field["field_title"]), idx + 2)) elif distribution_index is None: - print("""No se encontro la distribucion '{}' especificada para el campo + logger.error("""No se encontro la distribucion '{}' especificada para el campo '{}' (fila #{} de la hoja "Field"). Este campo no figurara en el data.json de salida.""".format( unidecode(field["distribution_title"]), unidecode(field["field_title"]), diff --git a/pydatajson/search.py b/pydatajson/search.py index c2b28e4..3822408 100644 --- a/pydatajson/search.py +++ b/pydatajson/search.py @@ -376,7 +376,6 @@ def get_catalog_metadata(catalog, exclude_meta_fields=None): def _filter_dictionary(dictionary, filter_in=None, filter_out=None): - # print(filter_in, filter_out) if filter_in: # chequea que el objeto tenga las propiedades de filtro positivo for key, value in iteritems(filter_in): diff --git a/pydatajson/validation.py b/pydatajson/validation.py index 4175970..c27f033 100644 --- a/pydatajson/validation.py +++ b/pydatajson/validation.py @@ -11,6 +11,7 @@ import os import platform import mimetypes +import logging from collections import Counter try: @@ -30,6 +31,8 @@ DEFAULT_CATALOG_SCHEMA_FILENAME = "catalog.json" EXTENSIONS_EXCEPTIONS = ["zip", "php", "asp", "aspx"] +logger = logging.getLogger('pydatajson') + def create_validator(schema_filename=None, schema_dir=None): """Crea el validador necesario para inicializar un objeto DataJson. @@ -304,8 +307,8 @@ def iter_custom_errors(catalog): if len(dups) > 0: yield ce.DownloadURLRepetitionError(dups) - except Exception as e: - print(e) + except Exception: + logger.exception("Error de validación.") def _find_dups(elements): diff --git a/pydatajson/writers.py b/pydatajson/writers.py index e4303c2..2badc0b 100644 --- a/pydatajson/writers.py +++ b/pydatajson/writers.py @@ -23,6 +23,8 @@ from . import helpers +logger = logging.getLogger('pydatajson') + def write_tables(tables, path, column_styles=None, cell_styles=None, tables_fields=None, tables_names=None): @@ -79,7 +81,7 @@ def write_table(table, path, column_styles=None, cell_styles=None): # si la tabla está vacía, no escribe nada if len(table) == 0: - logging.warning("Tabla vacia: no se genera ninguna archivo.") + logger.warning("Tabla vacia: no se genera ninguna archivo.") return # Sólo sabe escribir listas de diccionarios con información tabular @@ -100,7 +102,7 @@ def write_table(table, path, column_styles=None, cell_styles=None): def _write_csv_table(table, path): if len(table) == 0: - print("No se puede crear un CSV con una tabla vacía.") + logger.error("No se puede crear un CSV con una tabla vacía.") return headers = table[0].keys() @@ -201,7 +203,7 @@ def _write_xlsx_table(tables, path, column_styles=None, cell_styles=None, def _list_table_to_ws(wb, table, table_name=None, column_styles=None, cell_styles=None, fields=None): if len(table) == 0 and not fields: - print("No se puede crear una hoja Excel con una tabla vacía.") + logger.error("No se puede crear una hoja Excel con una tabla vacía.") return elif len(table) == 0 and fields: # la primer fila de la tabla está vacía