ecolabdata / ckanext-ecospheres

GNU Affero General Public License v3.0
3 stars 0 forks source link

Consolidation du chargement des vocabulaires en base #1

Open alhyss opened 2 years ago

alhyss commented 2 years ago

Concerne le module ckanext.ecospheres.vocabulary.loader, qui n'existe que sur la branche dev.

Pour le module ckanext.ecospheres.vocabulary.index, le fichier vocabularies.yaml et tous les modules de ckanext.ecospheres.vocabulary.parser, les versions de référrence sont celles de la branche spatial.

Schéma PostgreSQL

Il paraîtrait préférable que les vocabulaires aient un espace de nommage/schéma distinct du reste des données de PostgreSQL.

Pour l'heure, le nom de ce schéma est défini par la constante ckanext.ecospheres.vocabulary.parser.model.SQL_SCHEMA et il s'agit de 'vocabulary'. Il pourrait être envisagé d'en faire un paramètre de configuration. Je suis partie du principe que ce schéma existera lorsque j'ai mis en place ce que j'évoque ci-après.

À noter qu'il faudra peut-être ajouter la création de ce schéma à la liste des commandes à passer pour préparer la base de données lors de l'installation de CKAN (avec CREATE et USAGE pour le rôle ckan) si le rôle ckan n'a pas CREATE sur la base.

Fonction à utiliser pour l'import et la normalisation des vocabulaires

Pour mémoire, c'est le module ckanext.ecospheres.vocabulary.index qui fournit les méthodes à utiliser en amont pour récupérer les vocabulaires et les normaliser avant chargement en base. L'en-tête du fichier explique leur fonctionnement. Je l'ai beaucoup précisée ces derniers jours.

La fonction load_vocab du module loader utilise actuellement la méthode VocabularyIndex.load_and_dump, qui - en plus d'importer et harmoniser les vocabulaires - en fait des exports JSON stockés en local. Ces exports ne présentent pas un grand intérêt en production.

Il serait préférable d'utiliser simplement la méthode VocabularyIndex.load.

Gestion des erreurs

Actuellement, load_vocab se contente de regarder si l'attribut data du résultat renvoyé par VocabularyIndex.load_and_dump n'est pas vide et génère une erreur générique si c'est le cas. C'est dommage, car les objets ckanext.ecospheres.vocabulary.parser.result.VocabularyParsingResult renvoyés par VocabularyIndex.load et VocabularyIndex.load_and_dump fournissent beaucoup plus d'informations sur les problèmes rencontrés.

La valeur booléenne du VocabularyParsingResult indique si une erreur critique a eu lieu, c'est-à-dire une erreur qui a complètement empêché la récupération du vocabulaire. Elle vaut True si et seulement si le résultat est exploitable. Dans ce cas, l'attribut data sera toujours un cluster de données exploitable, alors qu'il vaut None en cas d'erreur critique.

L'attribut status_code du VocabularyParsingResult est plus précis. Sa valeur entière est 0 en l'absence de toute erreur, 1 en cas d'erreur critique, 2 s'il y a eu des erreurs, mais aucune erreur critique.

L'attribut log liste les erreurs rencontrées à proprement parler (sous classes d'Exception). Dès lors que status_code n'est pas 0, cette liste ne sera par construction jamais vide. En cas d'erreur critique, celle-ci est toujours la dernière de la liste, car elle stoppe l'exécution.

On peut imaginer une analyse du résultat de ce type :


result = VocabularyIndex.load('ecospheres_theme')
if not result:
    # en cas d'erreur critique, on abandonne le traitement sur le vocabulaire
    raise result.log[-1]
elif result.status_code != 0:
    for error in result.log:
        ...
        # log des erreurs pour info, mais poursuite du traitement sur le vocabulaire

Connaissance de la structure des tables nécessaire à leur création

Désormais, les pseudo-tables listées par l'attribut VocabularyParsingResult.data ont un attribut sql qui :

Autrement dit, load_vocab ne fera plus :

            for table_name in vocab_data.keys():

                #les tables echosphere spatial, echosphere_hierarchy et 
                #echosphere_regex ont des schemas de données differents.
                #donc il faut les gérer individuellement. 
                if re.match(REGEX_PATTERN_ECOSPHERE_SPATIAL,table_name):
                    _table=_get_spatial_schema_table(table_name)
                elif re.match(REGEX_PATTERN_ECOSPHERE_HIERARCHY,table_name):
                    _table=_get_hierarchy_schema_table(table_name)
                elif re.match(REGEX_PATTERN_ECOSPHERE_REGEX,table_name):
                    _table=_get_regex_schema_table(table_name)
                else:
                    _table = _get_generic_schema(table_name)

                __create_table_and_load_data(table_name=table_name,
                                             table_schema=_table,
                                             data=vocab_data) 

mais simplement :

            for table_name, table in vocab_data.items():

                if not table.sql:
                    logging.info(
                        'No SQL definition available for ' f'table "{table_name}" of vocabulary "{name}".')
                    continue

                __create_table_and_load_data(
                    table_name=table_name,
                    table_schema=table.sql,
                    data=vocab_data
                ) 
asboukerram commented 1 year ago

j'ai testé ce code comme tu l'as bien expliqué ci-dessus, mais il se trouve que je rencontre des erreurs, qui semblent provenir du format de l'object table.sql: "expected string or bytes-like object" Est-ce que tu as testé ce code sur ton environnement ? `

    for table_name, table in vocab_data.items():               
            if not table.sql:
                logging.info(
                    'No SQL definition available for ' f'table "{table_name}" of vocabulary "{name}".')
                continue

            __create_table_and_load_data(
                table_name=table_name,
                table_schema=table.sql,
                data=vocab_data
            ) ``
alhyss commented 1 year ago

@asboukerram Non, je n'ai pas testé ces lignes. Mais l'erreur est surprenante, considérant que si l'attribut sql n'est pas None il s'agit nécessairement d'un objet sqlalchemy.Table comme l'était auparavant la variable _table. Si vous n'avez pas déjà réglé le problème, est-ce qu'il serait possible d'avoir le détail de l'erreur ?

asboukerram commented 1 year ago

l'erreur de produit à la ligne

table_creation_sql = CreateTable(table_schema)

la fontion CreateTable ne reconnait pas le format de l'object reçu en paramètre et lève une exception: expected "string or bytes-like object"

asboukerram commented 1 year ago

j'ai aussi parfois cette erreur, il faut que je relance le conteneur que cette erreur disparaisse:

 Exception in thread Thread-13:
ckan-mte-ckan-dev-1  | Traceback (most recent call last):
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
ckan-mte-ckan-dev-1  |     self.run()
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/dcat/plugin.py", line 415, in run
ckan-mte-ckan-dev-1  | 2022-12-12 13:52:20,255 INFO  [ckan.config.middleware.flask_app]  200 /api/load-vocab render time 0.014 seconds
ckan-mte-ckan-dev-1  |     load_all_vocab(vocab_list=vocab_list)
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/loader.py", line 203, in load_vocab
ckan-mte-ckan-dev-1  |     vocab_data=VocabularyIndex.load(name).data
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/index.py", line 294, in load
ckan-mte-ckan-dev-1  |     return parser.__call__(**params)
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/parser/parsers.py", line 217, in basic_rdf
ckan-mte-ckan-dev-1  |     result = _result if _result is not None else VocabularyParsingResult(name)
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/parser/result.py", line 58, in __init__
ckan-mte-ckan-dev-1  |     self._data = VocabularyDataCluster(vocabulary)
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/parser/model.py", line 1091, in __init__
ckan-mte-ckan-dev-1  |     table = VocabularyLabelTable(
ckan-mte-ckan-dev-1  |   File "/srv/app/src_extensions/ckanext-ecospheres/ckanext/ecospheres/vocabulary/parser/model.py", line 692, in __init__
ckan-mte-ckan-dev-1  |     self.sql = sqlalchemy.Table(
ckan-mte-ckan-dev-1  |   File "<string>", line 2, in __new__
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/util/deprecations.py", line 130, in warned
ckan-mte-ckan-dev-1  |     return fn(*args, **kwargs)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 496, in __new__
ckan-mte-ckan-dev-1  |     metadata._remove_table(name, schema)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
ckan-mte-ckan-dev-1  |     compat.reraise(exc_type, exc_value, exc_tb)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 154, in reraise
ckan-mte-ckan-dev-1  |     raise value
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 491, in __new__
ckan-mte-ckan-dev-1  |     table._init(name, metadata, *args, **kw)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 590, in _init
ckan-mte-ckan-dev-1  |     self._init_items(*args)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 107, in _init_items
ckan-mte-ckan-dev-1  |     item._set_parent_with_dispatch(self)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/base.py", line 456, in _set_parent_with_dispatch
ckan-mte-ckan-dev-1  |     self._set_parent(parent)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 1507, in _set_parent
ckan-mte-ckan-dev-1  |     self._setup_on_memoized_fks(lambda fk: fk._set_remote_table(table))
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 1518, in _setup_on_memoized_fks
ckan-mte-ckan-dev-1  |     fn(fk)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 1507, in <lambda>
ckan-mte-ckan-dev-1  |     self._setup_on_memoized_fks(lambda fk: fk._set_remote_table(table))
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 2059, in _set_remote_table
ckan-mte-ckan-dev-1  |     self._link_to_col_by_colstring(parenttable, table, colname)
ckan-mte-ckan-dev-1  |   File "/usr/lib/python3.8/site-packages/sqlalchemy/sql/schema.py", line 1952, in _link_to_col_by_colstring
ckan-mte-ckan-dev-1  |     assert self.constraint._referred_table is table
ckan-mte-ckan-dev-1  | AssertionError
alhyss commented 1 year ago

Merci @asboukerram

Concernant la première erreur.

Il y a bien une coquille dans l'extrait de code en question. Il faut l'écrire comme ci-après pour ne pas avoir une erreur TypeError: Boolean value of this clause is not defined sur la commande if not table.sql:.

            for table_name, table in vocab_data.items():

                if table.sql is None:
                    logging.info(
                        'No SQL definition available for ' 
                        f'table "{table_name}" of vocabulary "{name}".'
                    )
                    continue

                __create_table_and_load_data(
                    table_name=table_name,
                    table_schema=table.sql,
                    data=vocab_data
                ) 

À corriger, donc, si vous ne l'aviez pas déjà fait, mais ce n'est pas la même erreur que celle que vous indiquez.

Sinon j'ai testé ça, qui fonctionne et confirme ainsi qu'il est bien possible d'exécuter CreateTable sur les valeurs de l'attribut sql :

from sqlalchemy.schema import CreateTable
from ckanext.ecospheres.vocabulary.index import VocabularyIndex
queries = []
for name in VocabularyIndex.names():
    if not name in ('insee_official_geographic_code',):
        result = VocabularyIndex.load(name)
        if not result:
             print(f'critical failure for "{name}"')
             continue
        for table_name, table in result.data.items():
            if table.sql is None:
                print(f'missing sql definition for "{table_name}"')
                continue
            queries.append(CreateTable(table.sql))
            print(f'"{table_name}" ok')

Comme vous pouvez le voir, j'ai exclu le vocabulaire des codes géographiques parce que c'est trop long. Est-ce que l'erreur apparaissait spécifiquement pour ce vocabulaire ?

Par ailleurs, même si CreateTable recevait un argument du mauvais type (j'ai essayé avec None, un entier et une chaîne de caractères), l'erreur qui apparaît est AttributeError: 'int' object has no attribute 'columns', pas expected string or bytes-like object. Et, encore une fois, il est par construction impossible que la valeur de l'attribut sql soit autre chose que None ou un objet sqlalchemy.Table - et c'est ce dernier type qui est attendu, pas des objets str ou bytes (qui provoquent la même erreur AttributeError: 'int' object has no attribute 'columns').

Bref, il semblerait que le problème soit ailleurs ?

Ce n'est peut-être pas ça, mais l'un des cas commun d'apparition de cette erreur TypeError: expected string or bytes-like object est l'application d'une des fonctions du module re sur None ou autre chose qu'un objet str ou bytes.

Notez que l'URL d'accès aux données des référentiels de coordonnées IGN (vocabulaire ign_crs) ne fonctionnait plus, et la structure des données a changé pour le vocabulaire inspire_theme. Je viens de faire les modifications nécessaires sur la branche spatial-for-merge. Si vous vous référez à ce que j'expliquais plus haut sur la gestion des erreurs, il s'agissait de deux cas où la valeur booléenne de result aurait été False / result.status_code vaut 1 / result.data vaut None. Il devrait s'agir d'une situation où les erreurs sont maîtrisées, mais, ce faisant, je me suis rendue compte que j'avais parlé partout de tester la valeur booléenne des objets VocabularyParsingResult pour détecter les erreurs critiques, alors que jusque-là c'était la valeur booléenne de l'attribut VocabularyParsingResult.status_code qui donnait cette information... J'ai changé ça : maintenant la valeur booléenne du VocabularyParsingResult est identique à celle de son attribut VocabularyParsingResult.status_code et tout ce que j'avais indiqué fonctionne. Il est possible que cette modification suffise à régler le problème.