bryanthowell-tableau / tableau_tools

Package containing Tableau REST API, XML modification, tabcmd and repository tools
Other
215 stars 86 forks source link

None Element Error When Creating Data Source from Scratch #43

Closed tableaukun closed 6 years ago

tableaukun commented 6 years ago

tableau_tools Version = 4.6.0 Python Version = 2.7.10 OS Version = macOS High Sierra 10.13.3 Tableau Server Version = 10.5

I followed the docs to create a data source from scratch, but keep running into errors. When trying to create one with a Custom SQL Query, I get an Element error. Below is my code:

new_file = TableauFile(filename='test.tds',create_new=True,ds_version='10.5')
new_doc = new_file.tableau_document

new_doc.add_new_connection(ds_type='postgres'
                           ,server='localhost'
                           ,db_or_schema_name='db_name'
                           ,authentication='username-password'
                          )

ds = new_doc.datasources[0]
conn = ds.connections[0]

conn.port = '5432'
conn.username = 'postgres'

ds.set_first_custom_sql('select * from schema.table limit 5','my_query',conn.connection_name)

new_doc.save_file(u'New TDS')
TypeError                                 Traceback (most recent call last)
<ipython-input-33-c2c2382e0ae5> in <module>()
----> 1 new_doc.save_file(u'New TDS')

/Users/tableaukun/virtualenvs/venv/lib/python2.7/site-packages/tableau_tools/tableau_documents/tableau_datasource.py in save_file(self, filename_no_extension, save_to_directory)
    386             lh.write(u"<?xml version='1.0' encoding='utf-8' ?>\n\n")
    387             # Write the datasource XML itself
--> 388             lh.write(self.get_datasource_xml())
    389             lh.close()
    390 

/Users/tableaukun/virtualenvs/venv/lib/python2.7/site-packages/tableau_tools/tableau_documents/tableau_datasource.py in get_datasource_xml(self)
    319             new_rel_xml = self.generate_relation_section()
    320             connection_root = new_xml.find(u'.//connection', self.ns_map)
--> 321             connection_root.append(new_rel_xml)
    322             cas = self.generate_aliases_column_section()
    323             # If there is no existing aliases tag, gotta add one. Unlikely but safety first

TypeError: must be Element, not None

There seems to be an issue with generate_relation_section(). I manually ran the code for the function and received the expected value:

import xml.etree.ElementTree as ET

rel_xml_obj = ET.Element(u"relation")
# There's only a single main relation with only one table

if len(new_doc.join_relations) == 0:
    for item in new_doc.main_table_relation.items():
        rel_xml_obj.set(item[0], item[1])
    if new_doc.main_table_relation.text is not None:
        rel_xml_obj.text = new_doc.main_table_relation.text
tableaukun commented 6 years ago

Looks like return rel_xml_obj needs to be called out twice in the code.

def generate_relation_section(self):
        # Because of the strange way that the interior definition is the last on, you need to work inside out
        # "Middle-out" as Silicon Valley suggests.
        # Generate the actual JOINs
        #if self.relation_xml_obj is not None:
        #    self.relation_xml_obj.clear()
        #else:
        rel_xml_obj = etree.Element(u"relation")
        # There's only a single main relation with only one table

        if len(self.join_relations) == 0:
            for item in self.main_table_relation.items():
                rel_xml_obj.set(item[0], item[1])
            if self.main_table_relation.text is not None:
                rel_xml_obj.text = self.main_table_relation.text
            return rel_xml_obj ###THIS WAS MISSING
        else:
            prev_relation = rel_xml_obj

            # We go through each relation, build the whole thing, then append it to the previous relation, then make
            # that the new prev_relationship. Something like recursion
            for join_desc in self.join_relations:
                r = etree.Element(u"relation")
                r.set(u"join", join_desc[u"join_type"])
                r.set(u"type", u"join")
                if len(join_desc[u"on_clauses"]) == 0:
                    raise InvalidOptionException("Join clause must have at least one ON clause describing relation")
                else:
                    and_expression = None
                    if len(join_desc[u"on_clauses"]) > 1:
                        and_expression = etree.Element(u"expression")
                        and_expression.set(u"op", u'AND')
                    for on_clause in join_desc[u"on_clauses"]:
                        c = etree.Element(u"clause")
                        c.set(u"type", u"join")
                        e = etree.Element(u"expression")
                        e.set(u"op", on_clause[u"operator"])

                        e_field1 = etree.Element(u"expression")
                        e_field1_name = u'[{}].[{}]'.format(on_clause[u"left_table_alias"],
                                                            on_clause[u"left_field"])
                        e_field1.set(u"op", e_field1_name)
                        e.append(e_field1)

                        e_field2 = etree.Element(u"expression")
                        e_field2_name = u'[{}].[{}]'.format(on_clause[u"right_table_alias"],
                                                            on_clause[u"right_field"])
                        e_field2.set(u"op", e_field2_name)
                        e.append(e_field2)
                        if and_expression is not None:
                            and_expression.append(e)
                        else:
                            and_expression = e

                c.append(and_expression)
                r.append(c)
                r.append(prev_relation)

                if join_desc[u"custom_sql"] is None:
                    new_table_rel = self.create_table_relation(join_desc[u"db_table_name"],
                                                               join_desc[u"table_alias"])
                elif join_desc[u"custom_sql"] is not None:
                    new_table_rel = self.create_custom_sql_relation(join_desc[u'custom_sql'],
                                                                    join_desc[u'table_alias'])
                r.append(new_table_rel)
                prev_relation = r

                rel_xml_obj.append(prev_relation)

                return rel_xml_obj
bryanthowell-tableau commented 6 years ago

I've fixed this by moving the return out of the else loop so that it will return under either condition. Thank you for finding and determining a fix. I've checked into the code for 4.6.2 and will close this when it is fully released

bryanthowell-tableau commented 6 years ago

Version 4.6.2 is now released with a fix for this issue. Please reopen if the issue persists.