Append existing XML file using Element Tree - issue with IronPython

Hi!

I’m trying to append an existing XML file with new sub-elements. I’ve managed to get it to work in Spyder using Python 3.7, but when I copy-paste the code and add IN[0…] inputs, the code doesn’t work anymore.

Does anyone have an idea what could be wrong? :slight_smile:

I’m attaching an image of the script.

Move your code outside of the if __name__ == '__main__'. Also, there is no console in Dynamo’s Python node, so printing will do nothing unless you change your stdout. You can just assign a raw string to your OUT variable rather than using a print function or statement.

Edit: To elaborate, if you set __name__ to your OUT variable, you will see that it is actually <module>. Therefore, __name__ == '__main__' is false and the code within never executes.

Thanks for your help!

I removed the if name == ‘main’ and the print method. It still does not seem to append the XML. Perhaps, there is something else I am doing wrong?

For the file paths, what would be the correct input? A string, a file path directory, or a “file from path” node?

 # Enable Python support and load DesignScript library
import clr
import sys
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
sys.path.append("C:\Program Files (x86)\IronPython 2.7\Lib")

import xml.etree.ElementTree as ET

# The inputs to this node will be stored as a list in the IN variables.
newfilepath = IN[0]
object_name = IN[1]
basefrequency = IN[2]
baseunitprice = IN[3]
basevid = IN[4]
units = IN[5]
xpath = IN[6]
filepath = IN[7]
inputamount = IN[8]
inputvid = IN[9]

# Place your code below this line
def get_new_element():
    #create element and set attributes
    row = ET.Element('row')
    row.attrib['name'] = object_name
    row.attrib['include'] = 'true'
    row.attrib['units'] = units
    #create sub-element and set attributes
    basevalues = ET.SubElement(row, 'basevalues')

    #create sub-sub-element and set attributes
    values = ET.SubElement(basevalues, 'values')
    values.attrib['frequency'] = basefrequency
    values.attrib['unitprice'] = baseunitprice
    values.attrib['vid'] = basevid
    #create sub-element and set attributes
    inputvalues = ET.SubElement(row, 'inputvalues')
    
    #create sub-sub-element and set attributes
    values = ET.SubElement(inputvalues, 'values')
    values.attrib['amount'] = inputamount
    values.attrib['vid'] = inputvid

    return row


#if __name__ == '__main__':
	#find out how ot change .xml to variable.
xml_tree = ET.parse(filepath)
    # if this doesnt work find a way to split this ot something like:
    # "./costdatabreakdown/accountplan/maingroup/subgroup[@  name='Staircases']"
subgroup_element = xml_tree.find(xpath)

new_element = get_new_element()
subgroup_element.append(new_element)

new_xml_tree_string = ET.tostring(xml_tree.getroot())
	# if this doesnt work find a way to split this ot something like:
	#'F:/OneDrive - Aalborg Universitet/01_Thesis/01_Thesis_02_XML/Test samples/test1.xml'
with open(newfilepath, "wb") as f:
        f.write(new_xml_tree_string)
# Assign your output to the OUT variable.
OUT = "file appended in " + newfilepath + " ."

What’s going wrong exactly? I noticed that you’re already parsing the old XML file outside of the AppendXML node, but then using ET.parse() on the already-parsed ElementTree. There’s nothing wrong with how you are providing your filepaths otherwise.

Thank you so much for taking a look into it!

Basically, I’m trying to append an existing XML file with new data at different levels of the element tree.

I need something that will recursively input data from Revit objects and create those objects within the XML file, so that it can run in external software that takes the XML as its input.

Each room corresponds to the subgroup, and objects within the room correspond to rows (children of the subgroup). Then each row (object) has input values that correspond to attribute values of the children of rows.

I need some kind of a loop that will take each room instance and object instances within the room, and append instance parameter values to the schema.

Problem is, my python experience is very limited ;/.

The problem above is just a test module to see if the file can be appended. And as mentioned earlier, the Spyder python 3.7 appends the new XML file just as needed, and the iron python in Dynamo does not update the file.

Do you think I’m taking the right approach, or I need a different one?

I will be working on this tomorrow and will keep you posted if you were interested.

/Adam

Everything looks correct in your code, although the order of the inputs within the Python node doesn’t match the order in the AppendXML custom node. If you can provide the source files I’ll take a look.

Hi @cgartland,

Here is the script file:
XPathV2test.dyn (27.8 KB)

And here is the xml. (converted to .txt)
190930_ENG.txt (333.4 KB)

As of now, I still can’t get it to work, and this time I’m getting a library error:

so the error it gives me only occurs if I exclude this part from the code. If I leave it be, then the code executes, but the file does not update for some reason.

Line 91 of your XML file has a UTF-8 character which is causing an issue with the XML parser. The parser in Python 3 most likely handles this properly, but the IronPython implementation does not. If you remove the character it’s able to parse it. Also, the element you’re trying to find has invalid syntax.

Change it from this:
"/costdatabreakdown/accountplan/maingroup/subgroup[@ name='Staircases']

To this:
/costdatabreakdown/accountplan/maingroup/subgroup[@name='Staircases']

Here’s a modified version of your code as well:

import clr
import sys
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
sys.path.append('C:\Program Files (x86)\IronPython 2.7\Lib')
import xml.etree.ElementTree as ET

room_name = IN[0]
units = IN[1]
basefrequency = IN[2]
baseunitprice = IN[3]
basevid = IN[4]
quantity = IN[5]
existingfilepath = IN[6]
xpath = IN[7]
updatedfile = IN[8]

def get_new_element():
	row = ET.Element('row')
	row.attrib['name'] = room_name
	row.attrib['include'] = 'true'
	row.attrib['units'] = units
	#create sub-element and set attributes
	basevalues = ET.SubElement(row, 'basevalues')
	
	#create sub-sub-element and set attributes
	values = ET.SubElement(basevalues, 'values')
	values.attrib['frequency'] = basefrequency
	values.attrib['unitprice'] = baseunitprice
	values.attrib['vid'] = basevid
	
	inputvalues = ET.SubElement(row, 'inputvalues')
	
	values = ET.SubElement(inputvalues, 'values')
	values.attrib['amount'] = quantity
	values.attrib['vid'] = basevid
	
	return row

xml_tree = ET.parse(existingfilepath)
subgroup_element = xml_tree.find(xpath)

new_element = get_new_element()
subgroup_element.append(new_element)

new_xml_tree_string = ET.tostring(xml_tree.getroot())

with open(updatedfile, 'wb') as f:
	f.write(new_xml_tree_string)
2 Likes

Thank you Christian!

Coincidentally, I managed to fix it right when you posted this:

import clr
import sys
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
sys.path.append("C:\Program Files (x86)\IronPython 2.7\Lib")

from string import *

room_name = IN[0]
units = IN[1]
basefrequency = IN[2]
baseunitprice = IN[3]
basevid = IN[4]
quantity = IN[5]
existingfile = IN[6]
xpath = IN[7]
updatedfile = IN[8]

import xml.etree.ElementTree as ET

# The inputs to this node will be stored as a list in the IN variables.


# Place your code below this line
def get_new_element():
    row = ET.Element('row')
    row.attrib['name'] = room_name
    row.attrib['include'] = 'true'
    row.attrib['units'] = units
    #create sub-element and set attributes
    basevalues = ET.SubElement(row, 'basevalues')

    #create sub-sub-element and set attributes
    values = ET.SubElement(basevalues, 'values')
    values.attrib['frequency'] = basefrequency
    values.attrib['unitprice'] = baseunitprice
    values.attrib['vid'] = basevid
 
    inputvalues = ET.SubElement(row, 'inputvalues')
    
    values = ET.SubElement(inputvalues, 'values')
    values.attrib['amount'] = quantity
    values.attrib['vid'] = basevid

    return row
# Assign your output to the OUT variable.

uniStr = unicode(open(existingfile, 'r').read())
#fixed = uniStr.encode('utf-8', 'replace')
fixed = uniStr.encode('ascii', 'replace')
fixed.decode('utf-8', 'replace')
treee = ET.ElementTree(ET.fromstring(fixed))

try:
	root = treee.getroot()
except:
	root = treee

subgroup_element = root.find(xpath)

new_element = get_new_element()
subgroup_element.append(new_element)

new_xml_tree_string = ET.tostring(treee.getroot())

with open(updatedfile, "wb") as f:
	f.write(new_xml_tree_string)
        
OUT = treee

Thank you so much for your involvement!

2 Likes

Hi Christian,

I’ve got one more question, perhaps you know how to fix!

I am now using the same code, but want to pass a list instead of a single value. Here is my way about it:

And here is what I’m trying to do with the code. Basically, unzip it so that the elements from each lists correspond to the same index, and populate the new_elements one by one into the XML file.

# -*- coding: utf-8 -*-
import clr
import sys
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
sys.path.append("C:\Program Files (x86)\IronPython 2.7\Lib")

from string import *

object_name_list = IN[0]
units_list = IN[1]
basefrequency_list = IN[2]
baseunitprice_list = IN[3]
basevid_list = IN[4]
quantity_list = IN[5]
existingfile = IN[6]
xpath = IN[7]
updatedfile = IN[8]

import xml.etree.ElementTree as ET

# The inputs to this node will be stored as a list in the IN variables.

#for object_name, units, basefrequency, baseunitprice, basevid, quantity in zip(object_name_list, units_list, basefrequency_list, baseunitprice_list, basevid_list, quantity_list):

for index, object_name in enumerate(object_name_list):
    units = units_list[index]
    basefrequency = basefrequency_list[index]
    baseunitprice = baseunitprice_list[index]
    basevid = basevid_list[index]
    quantity = quantity_list[index]
    
    
# Populate to XML
    def get_new_element():
        row = ET.Element('row')
        row.attrib['name'] = {object_name}
        row.attrib['include'] = 'true'
        row.attrib['units'] = {units}
        #create sub-element and set attributes
        basevalues = ET.SubElement(row, 'basevalues')
    
        #create sub-sub-element and set attributes
        values = ET.SubElement(basevalues, 'values')
        values.attrib['frequency'] = {basefrequency}
        values.attrib['unitprice'] = {baseunitprice}
        values.attrib['vid'] = {basevid}
     
        inputvalues = ET.SubElement(row, 'inputvalues')
        
        values = ET.SubElement(inputvalues, 'values')
        values.attrib['amount'] = {quantity}
        values.attrib['vid'] = {basevid}

        return row
# Assign your output to the OUT variable.

uniStr = unicode(open(existingfile, 'r').read())
#fixed = uniStr.encode('utf-8', 'replace')
fixed = uniStr.encode('ascii', 'replace')
fixed.decode('utf-8', 'replace')
treee = ET.ElementTree(ET.fromstring(fixed))

try:
	root = treee.getroot()
except:
	root = treee
#Find XPATH
subgroup_element = root.find(xpath)

new_element = get_new_element()
#Create a list of new elements
new_elements = []
for new_element in new_elements:
    
    subgroup_element.append(new_elements)

new_xml_tree_string = ET.tostring(treee.getroot())

with open(updatedfile, "wb") as f:
	f.write(new_xml_tree_string)
        
OUT = treee

Do you think this is the right way to do this?
XPathV4test.dyn (31.6 KB)
LCCbygAppendXMLv4.dyf (21.3 KB)
Kind regards,
Adam

I will look into this on Monday, but a few things stand out to me here.

  1. You are defining get_new_element within your for loop, which is unnecessary–each time the loop iterates, it redefines the function, but it only needs to be defined once.

  2. The inputs do not look like they match up to what data is being provided (e.g. “units” is a file path and ”xpath" is a unit of measure)

I haven’t made any custom nodes with python, so I don’t know what the behavior is with lacing and levels. Given that, I don’t know how to modify your code exactly to work with the custom node as well. However, a starting point would be to simply use a for loop along with a zip function. For example, if you have some single functions like this:

def foo(bar):
    return bar + 1

item0 = IN[0]
item1 = IN[1]

OUT = [foo(item0), foo(item1)]

You can modify it like this:

def foo(bar):
    return bar + 1

list0 = IN[0]
list1 = IN[1]

outlist = []
for item0, item1 in zip(list0, list1):
    outlist.append([foo(item0), foo(item1)])

OUT = outlist

The zip function actually creates a list of new tuples containing each respective item of each of your iterables. So, zipping lists ['a', 'b'] and [1, 2] would give us [('a', 1), ('b', 2)].

1 Like

Thanks Christian,

Regarding your points:

  1. Indeed looping def is unnecessary, although something will need to iterate through each index and populate the get_new_element list.

  2. I am attaching a fixed list of inputs file, you were right, the spaghetti was incorrectly plugged. I also removed the .dyf custom node to simplify it.
    XPathV5test.dyn (36.3 KB)

  3. I don’t have to plug it to the custom Xpath node - that was just to check if the output came through at the end of the code. My sole purpose is to export data to the XML file.

I still don’t know how to put this all together, but I added some #comments to the script to show you my thought process. I’m mostly confused with plugging the indexed list values to the def get_new_element part, which appends the “row” object.

To put it in words, I imagine the zip function creates a dictionary index, which maps all 6 lists, and then some loop will use the def get_new_element to input 6 indexed (i+1 loop) attribute values into each new_element.

Then each “def get_new_element” will be appended to the “new_elements_list”.
And then, the list will be appended to the XPATH subgroup in the XML.

Is that the right way to look at this? Here is how I think this could work, but in Dynamo, not Python. Python could hopefully remove the “getitemAtIndex” and replace it with conditional loops.


import clr
import sys
clr.AddReference(‘ProtoGeometry’)
from Autodesk.DesignScript.Geometry import *
sys.path.append(“C:\Program Files (x86)\IronPython 2.7\Lib”)
from string import *
import xml.etree.ElementTree as ET
import itertools as IT

#List of input lists:
object_name_list = IN[0]
units_list = IN[1]
basefrequency_list = IN[2]
baseunitprice_list = IN[3]
basevid_list = IN[4]
quantity_list = IN[5]
#List of input directories and XPATH
existingfile = IN[6]
xpath = IN[7]
updatedfile = IN[8]


# The input lists are mapped to the same index value using zip.
new_objects_list = []
for object_name, units, basefrequency, baseunitprice, basevid, quantity in IT.izip_longest(object_name_list, units_list, basefrequency_list, baseunitprice_list, basevid_list, quantity_list):

    new_objects_list.append([index(object_name), index(units), index(basefrequency), index(baseunitprice), index(basevid), index(quantity)])

OUT = new_objects_list

i = index()

for i in new_element_list:
	i =+1
	
# define get_new_element() - Mapping of each index of lists to ET.
    #I'm not sure about the [i], but how to pass each index, execute the def get_new_element and proceed to next index?
def get_new_element():
    row = ET.Element('row')
    row.attrib['name'] = room_name[i]
    row.attrib['include'] = 'true'
    row.attrib['units'] = units[i]
    #create sub-element and set attributes
    basevalues = ET.SubElement(row, 'basevalues')
    #create sub-sub-element and set attributes
    values = ET.SubElement(basevalues, 'values')
    values.attrib['frequency'] = basefrequency[i]
    values.attrib['unitprice'] = baseunitprice[i]
    values.attrib['vid'] = basevid[i]
 	#create sub-element and set attributes
    inputvalues = ET.SubElement(row, 'inputvalues')
    #create sub-sub-element and set attributes
    values = ET.SubElement(inputvalues, 'values')
    values.attrib['amount'] = quantity[i]
    values.attrib['vid'] = basevid[i]
	#return mapped object = row
    return row
# For each index of outlist, populate get_new_element


   #read existing file
uniStr = unicode(open(existingfile, 'r').read())
#fixed = uniStr.encode('utf-8', 'replace')
fixed = uniStr.encode('ascii', 'replace')
fixed.decode('utf-8', 'replace')
treee = ET.ElementTree(ET.fromstring(fixed))

try:
	root = treee.getroot()
except:
	root = treee

subgroup_element = root.find(xpath)
# figure a way to append the Xpath with each new element.
new_element = get_new_element()
subgroup_element.append(new_element)

new_xml_tree_string = ET.tostring(treee.getroot())

with open(updatedfile, "wb") as f:
	f.write(new_xml_tree_string)
        
OUT = treee

Kind regards,
Adam

1 Like