MTES-MCT / metadata-postgresql

Plume : gestion des métadonnées du patrimoine PostgreSQL
https://mtes-mct.github.io/metadata-postgresql/
GNU Affero General Public License v3.0
1 stars 1 forks source link

Gestion de l'import depuis un CSW #3

Closed alhyss closed 2 years ago

alhyss commented 2 years ago

Pour échanger sur la manière dont on voit les choses avant d'implémenter chacun les parties qui nous occupent.

Je pense qu'il faut prévoir un nouveau paramètre utilisateur urlCsw, contenant une liste d'URL de base de CSW saisies par l'utilisateur. Par défaut on pourrait mettre l'URL du CSW de GéoIDE (http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable).

Telles que je vois les choses - mais ça et tout ce qui suit est totalement discutable - l'icône d'import de la barre d'outils se parerait d'une petite flèche donnant accès à un menu qui contiendrait à ce stade deux items : depuis un fichier - déjà implémenté - et depuis un service CSW (nom pas nécessairement définitif), qui est donc à implémenter.

Lorsque l'utilisateur clique sur Importer > depuis un service CSW, une boîte de dialogue ou équivalent s'ouvre.

Deux boutons d'actions en bas : Importer et Annuler.

Deux champs de saisie :

Il serait intéressant que le bouton Importer soit désactivé quand l'un des deux champs est vide.

Lorsque l'utilisateur a saisi une nouvelle URL de CSW, il serait bien de lui permettre de la sauvegarder dans urlCsw.

Je préciserai ça quand je vais concrètement implémenter le mapping des métadonnées INSPIRE, mais j'imagine que l'utilisateur pourrait choisir entre trois modes d'import :

  1. Mettre à jour avec les métadonnées distantes (valeur par défaut) : pour toutes les catégories de métadonnées renseignées dans la fiche du catalogue, la valeur de la fiche locale est remplacée par celle du catalogue ou ajoutée s'il n'y avait pas de valeur.
  2. Compléter avec les métadonnées distantes : pour toutes les catégories de métadonnées renseignées dans la fiche du catalogue qui n'ont pas de valeur dans la fiche locale, la valeur de la fiche distante est ajoutée.
  3. Remplacer par les métadonnées distantes : génère une nouvelle fiche à partir des métadonnées distantes. La différence avec l'option 1 est que les informations des catégories non renseignées dans la fiche du catalogue mais qui auraient pu exister en local vont être effacées.

Il y aurait aussi une option Enregistrer la configuration dans les métadonnées qui pourrait prendre la forme d'une case à cocher cochée par défaut.

À l'ouverture de la boîte de dialogue, tu pourras exécuter plume.rdf.metagraph.Metagraph.csw_parameters sur le graphe de métadonnées courant (pas d'autre argument que self), qui te renverra un tuple avec l'URL du CSW et l'identifiant de la fiche si une configuration avait préalablement été mémorisée, et None sinon. Cela permettra de pré-remplir les deux champs de configuration. À défaut, le champ d'URL prendra la première valeur de la liste d'urlCsw et le champ d'identifiant restera vide.

Quand l'utilisateur active l'import, on procède en trois temps :

Qu'en penses-tu ?

PS: Ne cherche pas les fonctions csw_parameters et metagraph_from_iso dans mon code, je ne les ai pas encore écrites.

WREATCHED commented 2 years ago

Rapidement à la première lecture 👍

Remarques :

Cette option devra-t-elle faire partie de la première version diffusée ?

alhyss commented 2 years ago
  • Je pourrais sauvegarder les informations dans la section de PLUMEdans le QGIS3.INI

Oui, tout à fait, le paramètre utilisateur urlCsw serait à sauvegarder dans QGIS3.ini.

  • Icone avec petite flèche ou faire une autre icone pour les imports CSW, ça se discute, moi j'aime bien les deux

Je dirais que c'est préférable avec un menu :

  • "Enregistrer la configuration dans les métadonnées", oui, mais pour sauvegarder le choix des trois options d'import ? ou autre chose

En fait je n'avais même pas prévu de sauvegarder ce paramètre-là :D (à voir, ce serait possible en créant une catégorie de métadonnées spécifique, mais je ne sais pas si c'est utile). Non, ce que je veux stocker, c'est l'URL du CSW du catalogue où se trouvent les métadonnées de la table, ainsi que l'identifiant de la fiche, pour éviter à l'utilisateur de devoir retourner chercher ces informations à chaque fois (spécialement l'identifiant de la fiche). Je me rends compte que j'ai oublié de l'écrire, mais c'est la fonction plume.rdf.metagraph.metagraph_from_iso qui se charge de ça.

  • Il faudrait aussi pouvoir supprimer des url créées non utilisées ou pouvoir les modifier ?

Pas nécessairement via cette boîte de dialogue-là. Je pense que ça devrait plutôt se faire dans ta boîte de dialogue de gestion de la configuration utilisateur. Si on commence à gérer la configuration à plusieurs endroits, j'ai peur que ça devienne vite illisible...

L'ajout à la liste de nouvelles URL pourrait d'ailleurs se faire aussi exclusivement dans la boîte de dialogue de gestion de la configuration. L'essentiel est que, dans l'interface d'import, l'utilisateur ait la possibilité d'utiliser des URL qui ne soient pas dans la liste, à voir si on lui permet ensuite (s'il le juge opportun) de sauvegarder son URL en cliquant sur un bouton ou s'il faut qu'il ouvre la boîte de dialogue du paramétrage pour le faire. Je n'ai pas de préférence a priori, d'autant qu'on parle d'une action très peu fréquente.

Cette option devra-t-elle faire partie de la première version diffusée ?

Pour moi oui. Ça va immédiatement rendre Plume beaucoup plus intéressant pour les services et, à l'inverse, diffuser une version qui ne proposerait pas ça risque de rebuter d'entrée. Ce serait aussi idiot que des services commencent à faire des copier-coller de métadonnées à la main, alors qu'on a prévu à court terme un mécanisme qui va leur faire gagner énormément de temps.

WREATCHED commented 2 years ago

OK, Bon, beh tu te rends compte qu'il y a du taf. Mais vu le contexte promotionnel, je ferais comme je peux, tu es d'accord, sinon c'est via ... 👎

alhyss commented 2 years ago

Je m'en rends compte.

Aucune urgence pour l'implémentation. Autant il serait souhaitable que ce soit là pour la première version officiellement mise en production, autant on peut faire sans pour la première version diffusée aux membres du GT. Et ce n'est pas non plus comme si tout était prêt de mon côté, loin de là.

WREATCHED commented 2 years ago

Juste pour faire un test dans qgis image

Et ça passe sans vpn dans la console, encore du temps à comprendre le pourquoi du comment

Fonctionne SANS VPN avec MetaSearch image

alhyss commented 2 years ago

Les requêtes qu'on va envoyer ont cette forme : http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable?service=CSW&REQUEST=GetRecordById&version=2.0.2&namespace=xmlns%3Acsw%3Dhttp%3A%2F%2Fwww.opengis.net%2Fcat%2Fcsw&outputFormat=application%2Fxml&outputSchema=http%3A%2F%2Fwww.isotc211.org%2F2005%2Fgmd&ElementSetName=full&Id=fr-120066022-jdd-23d6b4cd-5a3b-4e10-83ae-d8fdad9b04ab

Celle-ci est le résultat de :

from plume.iso.csw import getrecordbyid_request
request = getrecordbyid_request(
    'http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable',
    'fr-120066022-jdd-23d6b4cd-5a3b-4e10-83ae-d8fdad9b04ab'
    )

Edit: L'idéal serait d'avoir un bête équivalent à urllib.request.urlopen, mais où QGIS gère le proxy lui-même.

FERRATON commented 2 years ago

Pour les requêtes http en python sous QGIS la réponse décrite sur : https://gis.stackexchange.com/questions/343126/performing-sync-or-async-network-request-in-pyqgis me paraît toujours pertinente. Sinon pour répondre à Didier (cf plus haut) : WCS ≠ CSW... Bonne fêtes de fin d'année image

WREATCHED commented 2 years ago

Bonnes année 2022 à vous deux. Moi j'ai la reconstruction de ma maison, ce qui n'est pas une mince affaire.

image

WREATCHED commented 2 years ago

Bjr Alain, Oui, je sais mais pourquoi cette remarque ? C'est le serveur qui me donne cette réponse, pas moi. Ensuite le lien que tu donnes peut servir, mais c'est pour le téléchargement avec authentification ? Si tu as des pistes pour chuinter le VPN pas de soucis, je compte sur toi !

FERRATON commented 2 years ago

Dans ta capture d'écran tu as comme message 'impossible d'obtenir les spécifications du serveur WCS' (Web Coverage Service)...et dans le lien on voit que tu lui passe une adresse de serveur CSW. Je suppose donc qu'il y a une petite confusion ? Pour le lien, peut-être que je n'ai pas compris la remarque de Leslie demandant un urlopen avec le proxy géré par QGIS. Je n'ai pas vu de référence au VPN dans les dialogues précédents... Donc non je n'ai pas de piste pour shunter le VPN si celui-ci est actif sur le poste au moment de la requête....

WREATCHED commented 2 years ago

OK Alain, nous sommes d'accord

alhyss commented 2 years ago

Bonne année à tous les deux !

Merci @FERRATON pour les infos. Tu as peut-être fait plus de tests, @WREATCHED , mais QgsNetworkContentFetcher m'a l'air de correspondre à ce que j'avais en tête ?

from PyQt5.QtCore import QUrl
from qgis.core import QgsNetworkContentFetcher
from plume.iso.csw import getrecordbyid_request

request = getrecordbyid_request(
    'http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable',
    'fr-120066022-jdd-23d6b4cd-5a3b-4e10-83ae-d8fdad9b04ab'
    )
url = QUrl(request)
fetcher = QgsNetworkContentFetcher()
fetcher.fetchContent(url)
raw_xml = fetcher.contentAsString()

J'ai testé avec et sans VPN, ça semble fonctionner.

Si cette classe est bien la bonne, il y aura la question de la gestion des erreurs. Visiblement contentAsString renvoie une chaîne de caractères vide quand le serveur retourne une erreur, et si tu donnes ça en argument à mes fonctions, ça produira un formulaire vide, ce qui ne serait pas catastrophique. Sinon il doit être possible d'utiliser QgsNetworkContentFetcher.reply pour générer un message d'erreur... Comme tu préfères, @WREATCHED .

alhyss commented 2 years ago

J'ai continué à implémenter de mon côté, sinon.

Une fois que tu as récupéré le XML (le raw_xml de mon message précédent), il faudra le passer à la fonction plume.rdf.metagraph_from_iso pour récupérer le nouveau graphe de métadonnées.


from plume.rdf.metagraph import metagraph_from_iso

metagraph = metagraph_from_iso(raw_xml, old_metagraph)

Ça marche comme clean_metagraph. old_metagraph est le graphe de métadonnées - metagraph - actuel. Tu peux l'utiliser dès à présent. C'est dans mes nouveaux scripts, mais ça reste compatible avec la vieille fonction rdf_utils.build_dict.

Les paramètres - URL du serveur CSW et identifiant de la fiche - seront gérés via la propriété linked_record du graphe. NB. Remplace ce que j'avais appelé csw_parameters dans mon tout premier message de cette issue.

Pour récupérer les paramètres sauvegardés :


url_csw, file_identifier = metagraph.linked_record

Pour sauvegarder de nouveaux paramètres (si l'utilisateur ne décoche pas la case Enregistrer la configuration dans les métadonnées) :


metagraph.linked_record = url_csw, file_identifier

Cette propriété n'existe qu'avec la nouvelle classe Metagraph, donc tu ne peux pas encore l'appeler dans tes scripts, mais si tu veux voir ce que ça donne :


>>> from plume.rdf.metagraph import Metagraph
>>> metagraph = Metagraph()
>>> metagraph.datasetid = None
>>> metagraph.linked_record = 'http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable', 'fr-120066022-jdd-d3d794eb-76ba-450a-9f03-6eb84662f297'
>>> metagraph.linked_record
('http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable', 'fr-120066022-jdd-d3d794eb-76ba-450a-9f03-6eb84662f297')

Je vais mettre tout ça dans la doc, évidemment.

WREATCHED commented 2 years ago

Super, mais Bon, moi ça n'a pas fonctionné en l'état. J'ai du rajouté les évènements de "finsihed" Donc pour tests dans la console (sans les appels) si vous voulez essayez, l'ensemble des codes LL et DL

from urllib.parse import urlencode, urljoin
from urllib import request
from PyQt5.QtCore import QUrl
from qgis.core import QgsNetworkContentFetcher

#==================================================
"""Utilitaires pour le dialogue avec les services CSW des catalogues.
"""
def getrecordbyid_request(url_csw, file_identifier):
    """Crée une requête GetRecordById pour envoi en HTTP GET.

    Parameters
    ----------
    url_csw : str
        L'URL de base du service CSW du catalogue, sans aucun paramètre.
    file_identifier : str
        L'identifiant de la fiche de métadonnées sur le catalogue.
        Correspond à la valeur de la balise ``gmd:fileIdentifier``
        des fiches ISO 19139.

    Returns
    -------
    str

    Examples
    --------
    >>> r = getrecordbyid_request(
    ...     'http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable', 
    ...     'fr-120066022-jdd-d3d794eb-76ba-450a-9f03-6eb84662f297'
    ...     )
    >>> from urllib.request import urlopen
    >>> with urlopen(r) as src:
    ...     xml = src.read()

    """
    url_csw = url_csw.rstrip('?/')
    config = {
        'service' : 'CSW',
        'REQUEST': 'GetRecordById',
        'version': '2.0.2',
        'namespace': 'xmlns:csw=http://www.opengis.net/cat/csw',
        'outputFormat': 'application/xml',
        'outputSchema': 'http://www.isotc211.org/2005/gmd',
        'ElementSetName': 'full',
        'Id': file_identifier
        }
    data = urlencode(config)
    return '{}?{}'.format(url_csw, data)

#==================================================
def mainGeoideID_OK(urlCatalogue, nameCatalogue, idSet) :
    print("\n====== {} ======".format(str(nameCatalogue)))

    resultQueryId = getrecordbyid_request(urlCatalogue, idSet)
    url = QUrl(resultQueryId)
    fetcher = QgsNetworkContentFetcher()
    fetcher.fetchContent(url)
    #-
    evloop = QEventLoop()
    fetcher.finished.connect(evloop.quit)
    evloop.exec_(QEventLoop.ExcludeUserInputEvents)
    fetcher.finished.disconnect(evloop.quit)
    #-
    raw_xml = fetcher.contentAsString()
    print("raw_xml {} ".format(str(raw_xml)))
    #-
    ret = fetcher.reply()
    print("fetcher.reply() {} ".format(str(ret)))

#==================================================
urlGEOIDE,       nameGEOIDE       = "http://ogc.geo-ide.developpement-durable.gouv.fr/csw/dataset-harvestable",     "Catalogage GEOIDE"
mainGeoideID_OK(urlGEOIDE, nameGEOIDE, "fr-120066022-jdd-23d6b4cd-5a3b-4e10-83ae-d8fdad9b04ab")

#==================================================

Par contre, cela fonctionne de chez moi avec ou sans vpn, c'est cool J'attaque le code demain

WREATCHED commented 2 years ago

https://user-images.githubusercontent.com/66324136/149093506-36473b13-5a43-4df4-82d1-f46f638304be.mp4

alhyss commented 2 years ago

Merci @WREATCHED ! ça nous fait une très bonne base.

Quelques remarques en vrac :

WREATCHED commented 2 years ago

Ok, bien reçu J'ai donc du travail ........

WREATCHED commented 2 years ago

image

WREATCHED commented 2 years ago

https://user-images.githubusercontent.com/66324136/149514802-9c33d95a-cfeb-4fb7-970e-37a2cd46c00f.mp4

alhyss commented 2 years ago

C'est super !

Merci d'avoir là aussi rendu paramétrable la couleur des cadres, ça va nous épargner de grands débats ^^.

Quelques micro-remarques :

WREATCHED commented 2 years ago

https://user-images.githubusercontent.com/66324136/149753987-cfa6ab05-4195-4d3a-a2fe-c067fcd95dcc.mp4

WREATCHED commented 2 years ago

image

alhyss commented 2 years ago

Merci @WREATCHED. C'est parfait ! Comme tu dis, c'est vraiment la cerise sur le gâteau de pouvoir réordonner les URL. Je n'osais pas en demander tant, mais c'est super que tu l'aies fait.

alhyss commented 2 years ago

Salut @WREATCHED,

Comme prévu, j'ai modifié plume.rdf.metagraph.metagraph_from_iso, pour qu'elle prenne un argument supplémentaire preserve, correspondant au mode de fusion des informations du catalogue distant et de la fiche locale dans laquelle on réalise l'import.

Pour la correspondance avec les cases à cocher :

WREATCHED commented 2 years ago

Code

options d'imports

mOptions = "if blank" if self.option1.isChecked() : # Mettre à jour avec les métadonnées distantes : valeur 'if blank'.

mOptions = "if blank"

elif self.option1.isChecked() : # Compléter avec les métadonnées distantes : valeur 'always' (défaut).

mOptions = "always"

elif self.option1.isChecked() : # Remplacer par les métadonnées distantes : valeur 'never'.

mOptions = "never"

Faut-il mettre à l'ouverture de la boite de dialogue l'option 2 activée puisque c'est l'option par défaut ?

WREATCHED commented 2 years ago
      #options d'imports
      mOptions = "always"
      if self.option1.isChecked() :    # Compléter avec les métadonnées distantes : valeur 'always' (défaut).
         mOptions = "always"
      elif self.option2.isChecked() :  # Mettre à jour avec les métadonnées distantes : valeur 'if blank'.
         mOptions = "if blank"
      elif self.option3.isChecked() :  # Remplacer par les métadonnées distantes : valeur 'never'.
         mOptions = "never"

image

alhyss commented 2 years ago

C'est bien ça !