Tester les développements XML en Python


Présentation du problème

Ces derniers temps je travail beaucoup sur la génération de fichiers XML à partir de données éparses suivants des contraintes externes qui sont impossibles à vérifier manuellement. Pour ce travail je ne peux m’accrocher qu’au schema XML que nous a transmis le fournisseur du service avec une documentation lacunaire. Chaque fichier généré fait entre 700 et 1000 lignes et la schéma pèse plus de 5Mo.

Le problème est que la plupart des outils python permettant d’écrire des fichiers XML de manière aisée ne comporte pas de validateur et ceux qui en contiennent un sont un vrai casse-tête pour l’écriture des fichiers. Après quelques tests j’ai décidé de ne pas utiliser les mêmes modules pour les tests et pour l’écriture : lxml pour le premier et elementree pour le second.

Mise en place des tests

Pour cette extension j’ai utilisé des doctests pour documenter le module qui est un élément à part dans l’application que nous mettons en place. Cela me permettait d’être moins dépendant de l’environnement qui évoluait énormément autour du module.

Je me suis appuyé sur une très bonne documentation de lxml publiée chez codespeak pour bâtir mes tests : Validation with lxml

Ce qu est intéressant dans cette bibliothèque est qu’elle supporte un grand nombre de type de schéma XML pour la validation :

  • DTD
  • RelaxNG
  • XMLSchema
  • Schematron

Lorsqu’il n’y a qu’un fichier à traiter il est possible de le tester directement au chargement ce qui est assez coûteux en mémoire. Lorsqu’il y a plusieurs fichiers à tester il faut d’abord charger le schéma puis tester les fichiers un par un. Là encore il est possible de ne tester que la validité globale du fichier ou d’avoir une sortie plus verbeuse.

    >>> from lxml import etree
    >>> from messagefactory import Message

Création d'un message

    >>> xml_message1 = Message()
    >>> xml_message1.initialize()
    >>> xml_message1.fillMessage()
    >>> xml_message1.finish()

Chargement du schéma
Tout chargment avec etree.parse() doit être fait avec un descripteur
de fichier ouvert ou un tampon StringIO.

    >>> f = open('../tests/output_schema.xsd')
    >>> xmlschema_file = etree.parse(f)
    >>> xmlschema = etree.XMLSchema(xmlschema_file)
    >>> f.close()

Les messages peuvent être ensuite testés à la volée.
Nos messages ne comportent pas de saut de ligne à la fin de chaque 
balise ce qui rend la lecture humaine difficile et la diagnostique 
verbeux inutilisable.

    >>> xml_message1 = StringIO(test_message1.getRawMessage().replace'><', '>\\n<'))
    >>> xml_file1 = etree.parse(xml_message1)

La méthode assertValid(xml_data) retourne une sortie verbeuse
si les données ne sont pas conformes.

    >>> xmlschema.assertValid(xml_file1)

La méthode validate(xml_data) se contente de renvoyer True ou False.

    >>> xmlschema.validate(xml_file1)
    True

Écriture des fichier XML

Le module elementtree possède une petite API extensible pour écrire des fichier XML. Elle était largement suffisante pour mes besoins qui consistaient à mettre bouts à bouts des données éparses suivant un modèle connu.

Le module elementtree.SimpleXMLWriter

L’objet XMLWriter possède les mêmes contraintes que celles vu dans les tests avec lxml : il ne travaille qu’avec un tampon (StringIO) ou un descripteur de fichier. Cela impose de différencier 2 mode de rendu : le mode RAW avec le texte brute et le le mode natif qui dans mon cas renvoi un StringIO. Il travaille comme en mode ligne : une fois qu’il a écrit un élément il ne peut pas revenir en arrière. Il n’est pas possible de faire une modification après coup du texte produit.
L’API permet de créer des bloc de 2 manière la méthode element et le couple start/end.
La méthode element permet de créer une balise et son contenu directement. elle prend comme argument :

  1. tag : le nom du tag à créer (sensible à la casse)
  2. text : une chaîne de caractère qui va être utilisée comme contenu de la balise
  3. attrib : un dictionnaire d’attribut à rajouter dans la balise

C’est pratique pour écrire une feuille ou un bloc venant d’une source externe.

Le couple de méthodes start/end ne font que créer des balises sans contenu et sans vérifier la cohérence des ouvertures/fermetures des balises (d’où l’importance d’une validation). La méthode start possède les même arguments que la méthode element en dehors de l’argument text. La méthode end n’accepte que l’argument tag.

Code du module messagefactory

Le code a été volontairement simplifié et ne met pas en exergue la complexité de création d’une arborescence complète de nœuds avec des répétitions. Je qualifierai cette implémentation de naïve.

from StringIO import StringIO
from elementtree.SimpleXMLWriter import XMLWriter

class Message(object):
    _file = None
    _writer = None

    #----------------------------------------------------------------------
    def __init__(self, encoding='ISO-8859-1'):
         """"""
         self._file = StringIO()
         self._writer = XMLWriter(self._file, encoding)

    #----------------------------------------------------------------------
    def getRawMessage(self):
         """Return message as a string"""
         self._file.flush()
         return self._file.getvalue()

    #----------------------------------------------------------------------
    def getMessage(self):
         """Return message as a StringIO object"""
         self._file.flush()
         return self._file

    #----------------------------------------------------------------------
    def initialize(self):
        """Create the enveloppe"""
        self._writer.declaration()

        self._writer.start('Envelope',
                            attrib={'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
                                    'xsi:noNamespaceSchemaLocation': 'output_schema.xsd',
                             },
        )

        self._writer.flush()

    #----------------------------------------------------------------------
    def finish(self):
        """Close the enveloppe"""
        self._writer.end('Envelope')
        self._writer.flush()
        self._writer.close()

    #----------------------------------------------------------------------
    def fillMessage(self):
        """ Create message content
        """
        self._writer.start('Message')

        # Delivery block
        self._writer.start('delivery')

        self._writer.start('to')
        self._writer.element('address', text=self.agencyCode)
        self._writer.end('to')

        self._writer.start('from')
        self._writer.element('address', text=self.clientCode)
        self._writer.end('from')

        self._writer.end('delivery')

        # Properties block
        self._writer.start('properties')

        self._writer.element('identity', text='1')
        self._writer.element('sentAt', text=self.creation_date.strftime('%y-%m-%dT%H:%M+1'))
        self._writer.element('expiresAt')
        self._writer.element('topic')

        self._writer.end('properties')

        # Manifest block
        self._writer.start('manifest')

        self._writer.start('reference', attrib={'uri': '#Reply-to'})
        self._writer.element('description')
        self._writer.end('reference')

        self._writer.end('manifest')

        # Process block
        self._writer.start('process')

        self._writer.element('type')
        self._writer.element('instance')
        self._writer.element('handle')

        self._writer.end('process')

        self._writer.end('Message')

        self._writer.flush()

.

About these ads

Les commentaires sont fermés.

Suivre

Recevez les nouvelles publications par mail.

Joignez-vous à 276 followers

%d bloggers like this: