XmlElement - ein einfacher XML-Generator (und Parser)

Einleitung

Das Arbeiten mit der APIs wie der von enaio erfordert häufig das Erzeugen von XML-Inhalten im eigenen Code. Nicht selten sehe ich dabei, dass XMLs als String aufgebaut werden:

query_xml = '<DMSQuery><Archive name="Patient"><ObjectType name="Patient" type="FOLDER"><Fields field_schema="DEF"><Field name="Name" /><Field name="Vorname" /><Field name="PLZ" /><Field name="Ort">Example Value</Field></Fields></ObjectType></Archive></DMSQuery>'

Soweit so einfach. Sollen nun aber nicht drei Felder übergeben werden, sondern 20 oder ergeben sich diese Felder aus einem vorherigen Schritt dynamisch, wird es schon kniffeliger. Und was, wenn ein Feldinhalt kein reiner Text von A-z ist, sondern ein Zeichen wie < enthält, welches das XML zerstört?

Sinnvollerweise werden XMLs niemals „von Hand“ aus Strings zusammengebaut. Viel sicherer und flexibler lassen sich diese mit Generatorklassen und -bibliotheken erzeugen. Für Python wäre dies im Rahmen des Standardumfangs xml.etree.ElementTree und dazugehörige Objekte.

Schreibweise mit Wiedererkennungswert

Im Rahmen meiner Arbeiten für die enaio®-API-Aufrufe aus dem ecmind_blue_client wollte ich eine Generatorklasse nutzen, welche die Optik eines XMLs möglichst im Code sichtbar macht. Python bietet hier viele Möglichkeiten, noch hat mich aber keine 100%-ig überzeugt. In Ermangelung eines guten Fundes habe ich aktuell meine eigene Bibliothek geschaffen, welche als Open Source veröffentlicht ist: XmlElement

Hiermit kann ein XML in Python notiert werden und dabei dem Endprodukt ähnlich sehen:

from XmlElement import XmlElement as X
query_xml = X('DMSQuery', s=[
            X('Archive', {'name': 'Patient'}, [
                X('ObjectType', {'name': 'Patient', 'type': 'FOLDER'}, [
                    X('Fields', {'field_schema': 'DEF'}, [
                        X('Field', {'name': 'Name'}),
                        X('Field', {'name': 'Vorname'}),
                        X('Field', {'name': 'PLZ'}),
                        X('Field', {'name': 'Ort'}, t='Example Value')
                    ])
                ])
            ])
        ])
print(query_xml.to_string())

Vergleiche:

<DMSQuery>
    <Archive name="Patient">
        <ObjectType name="Patient" type="FOLDER">
            <Fields field_schema="DEF">
                <Field name="Name" />
                <Field name="Vorname" />
                <Field name="PLZ" />
                <Field name="Ort">Example Value</Field>
            </Fields>
        </ObjectType>
    </Archive>
</DMSQuery>

Weitere Möglichkeiten

Natürlich können auch XMLs ausgelesen werden, ergänzt, über Elemente iteriert etc. Bei Fragen hierzu ergänze ich gerne Beispiele.

1 „Gefällt mir“

Hier noch ein Anwendungsbeispiel der Bibliothek XMLElement zusammen mit der ecmind_blue_client. Dieses Beispiel sucht Dokumente in einer kombinierten Anfrage mit dem übergeordneten Register und Ordner.

# Verbindung per Nativer enaio API
client = TcpPoolClient("localhost:4000:1", "MyPythonAppName", "ecmind", "ecmind")

# Kombinierte Anfrage auf Dokument, Register und Dokument
query_xml = X('DMSQuery', {}, [
    X('Archive', {}, [
        X('ObjectType', {'internal_name': 'Document'}, [
            X('Fields', {'field_schema': 'ALL'}, []),
            X('Conditions', {}, [
                X('ConditionObject', {'internal_name': 'Document'}, [
                    X('FieldCondition', {'internal_name': 'Name', 'operator': '='}, [
                        X("Value", {}, t="<<DocumentName>>")])
                ]),
                X('ConditionObject', {'internal_name': 'Register'}, [
                    X('FieldCondition', {'internal_name': 'Name', 'operator': '='}, [
                        X("Value", {}, t="<<RegisterName>>")])
                ]),
                X('ConditionObject', {'internal_name': 'Ordner'}, [
                    X('FieldCondition', {'internal_name': 'Name', 'operator': '='}, [
                        X("Value", {}, t="<<OrdnerName>>")])
                ])
            ])
        ])
    ])
])

# Ausführen der Suche. In diesem Fall werden auf die Basisparameter und Dateiinformationen mit angefragt
job = Job('dms.GetResultList', Flags=0, Encoding='UTF-8', RequestType='HOL',
            RegisterContext=0, Baseparams=1, FileInfo=1, XML=query_xml)
result = client.execute(job)

# prüfen ob der Aufruf erfolgreich war
if result.return_code != 0:
    raise RuntimeError("Suche fehlgeschlagen. Error Message: " + (result.error_message or ''))

# Auslesen des Ergebnisses
resultXml = X.from_string(result.values["XML"])
objects = resultXml["Archive"][0]['ObjectType'][0]['ObjectList'][0]['Object']

# Durch alle Objekte iterieren
for obj in objects:
    print(obj.attributes["id"])

requirements.txt

ecmind-blue-client>=0.2.5

XMLElement ist eine Abhängigkeit der enaio-blue-client Bibliothek

1 „Gefällt mir“

Seit Version 0.2.1 (XmlElement: Release Notes - #2 von rk) kann das XmlElement ja aus dicts erzeugt werden und umgekehrt. Bei der Erstellung von dict-Objekten aus XMLs gibt es das Problem, dass nicht erkannt werden kann, wenn etwas eine Liste sein sollte aber einfach nur ein Element hat. Die Gegenrichtung ist unproblematisch. Mein Vorschlag wäre, vor dem Parsen entsprechend den ersten solchen Knoten zu markieren, was meint Ihr?

from XmlElement import XmlElement as X

xml = X.from_object('data', {
    'users': { 'user': [ 'rk', 'uw', 'ag', 'dm' ] },
    'offices': { 'office': [ 'Frauenfeld' ] },
    '': 'Wow'
})

print(xml) # --> <data>Wow<users><user>rk</user><user>uw</user><user>ag</user><user>dm</user></users>
           #     <offices><office>Frauenfeld</office></offices></data>

d1 = xml.to_dict()
print(d1['offices']['office'][0]) # --> F

xml['offices'][0]['office'][0].flag_as_list = True

d2 = xml.to_dict()
print(d2['offices']['office'][0]) # --> Frauenfeld

Das habe ich jetzt mal so umgesetzt, ist mit Version 0.2.2 auf PyPi.org.