tableau / document-api-python

Create and modify Tableau workbook and datasource files
https://tableau.github.io/document-api-python/
MIT License
333 stars 178 forks source link

Updating Hortonworks (Hadoop) connection attributes #85

Open akatona1 opened 8 years ago

akatona1 commented 8 years ago

Hi,

I’m interested in using the Document API to update the connection type for .twbx files. I would like to update the following fields for a Hortonworks Hadoop Hive connection type: • Server Name • Authentication • Realm • Host FQDN • Service Name

If this is not possible with the current version, I would like to submit this as a feature request. These are only the fields that I need, but all field options on the Hortonworks connector would be helpful as well.

It would be great if I could do this not only locally, but also with a workbook that is published on Tableau Server. The Tableau Server UI doesn’t allow me to edit the authentication type for a data source and I haven’t seen a way to do this with tabcmd.

Also, I tried to access the Examples link at the bottom of the Github page (https://github.com/tableau/document-api-python) and it seems like the link is dead.

Thanks for your help!

Alex

t8y8 commented 8 years ago

Hello @akatona1,

Thank you for the issue report! We renamed the examples folder and the docs didn't get updated too, great catch.

The samples can be found at https://github.com/tableau/document-api-python/tree/master/samples.

Version 0.3 should have the ability to update a connection inside a workbook, though not all the attributes are exposed, and I think Hadoop's specifically are not yet.

Can you post a clean TWB file that has a hadoop connection, and an example of what you would want to update it to?

akatona1 commented 8 years ago

Hi @t8y8,

Thanks for the quick response. Unfortunately, I will not be able to upload anything. I can only provide the information that I gave above.

Alex

t8y8 commented 8 years ago

Here's an example of the connections element:

<connection authentication-type='2' class='hortonworkshadoophive' connection-type='2' 
dbname='' odbc-connect-string-extras='' one-time-sql='' port='15000' 
server='hadoophorton.test.tsi.lan' server-oauth='' username='' workgroup-auth-mode='as-is'>

We'll need to figure out what all the auth-types are and the connection-types so we can validate that they make sense.

akatona1 commented 8 years ago

@t8y8,

This seems to be the design of the auth-types and connection-types for the Hortonworks connection, where the auth-types are increasing from 0 to 8 and the connection type is 2 (for HiveServer2):

Auth-Type Auth-Type XML code Connection-Type XML code
No Authentication 0 2
Kerberos 1 2
User Name 2 2
User Name and Password 3 2
User Name and Password (SSL) 4 2
Microsoft Azure HDInsight Emulator 5 2
Microsoft Azure HDInsight Service 6 2
HTTP 7 2
HTTPS 8 2
icb410 commented 7 years ago

I was playing around with this and modified connection.py to read the fields I needed. Copying here in case it's useful. `import xml.etree.ElementTree as ET from tableaudocumentapi.dbclass import is_valid_dbclass

class Connection(object): """A class representing connections inside Data Sources."""

def __init__(self, connxml):
    """Connection is usually instantiated by passing in connection elements
    in a Data Source. If creating a connection from scratch you can call
    `from_attributes` passing in the connection attributes.

    """
    self._connectionXML = connxml
    self._dbname = connxml.get('dbname')
    self._server = connxml.get('server')
    self._username = connxml.get('username')
    self._authentication = connxml.get('authentication')
    self._class = connxml.get('class')
    self._port = connxml.get('port', None)
    self._query_band = connxml.get('query-band-spec', None)
    self._initial_sql = connxml.get('one-time-sql', None)
    #added for Hortonworks Hive connection
    self._schema = connxml.get('schema', None)
    self._http_path = connxml.get('http-path',None)
    self._connection_type = connxml.get('connection-type',None)
    self._sslcert = connxml.get('sslcert',None)
    self._sslmode = connxml.get('sslmode',None)

def __repr__(self):
    return "'<Connection server='{}' dbname='{}' @ {}>'".format(self._server, self._dbname, hex(id(self)))

@classmethod
def from_attributes(cls, server, dbname, username, dbclass, port=None, query_band=None,
                    initial_sql=None, authentication=''):
    """Creates a new connection that can be added into a Data Source.
    defaults to `''` which will be treated as 'prompt' by Tableau."""

    root = ET.Element('connection', authentication=authentication)
    xml = cls(root)
    xml.server = server
    xml.dbname = dbname
    xml.username = username
    xml.dbclass = dbclass
    xml.port = port
    xml.query_band = query_band
    xml.initial_sql = initial_sql

    return xml

@property
def dbname(self):
    """Database name for the connection. Not the table name."""
    return self._dbname

@dbname.setter
def dbname(self, value):
    """
    Set the connection's database name property.

    Args:
        value:  New name of the database. String.

    Returns:
        Nothing.

    """
    self._dbname = value
    self._connectionXML.set('dbname', value)

@property
def server(self):
    """Hostname or IP address of the database server. May also be a URL in some connection types."""
    return self._server

@server.setter
def server(self, value):
    """
    Set the connection's server property.

    Args:
        value:  New server. String.

    Returns:
        Nothing.

    """
    self._server = value
    self._connectionXML.set('server', value)

@property
def username(self):
    """Username used to authenticate to the database."""
    return self._username

@username.setter
def username(self, value):
    """
    Set the connection's username property.

    Args:
        value:  New username value. String.

    Returns:
        Nothing.

    """
    self._username = value
    self._connectionXML.set('username', value)

@property
def authentication(self):
    return self._authentication

@property
def dbclass(self):
    """The type of connection (e.g. 'MySQL', 'Postgresql'). A complete list
    can be found in dbclass.py"""
    return self._class

@dbclass.setter
def dbclass(self, value):
    """Set the connection's dbclass property.

    Args:
        value:  New dbclass value. String.

    Returns:
        Nothing.
    """

    if not is_valid_dbclass(value):
        raise AttributeError("'{}' is not a valid database type".format(value))

    self._class = value
    self._connectionXML.set('class', value)

@property
def port(self):
    """Port used to connect to the database."""
    return self._port

@port.setter
def port(self, value):
    """Set the connection's port property.

    Args:
        value:  New port value. String.

    Returns:
        Nothing.
    """

    self._port = value
    # If port is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['port']
        except KeyError:
            pass
    else:
        self._connectionXML.set('port', value)

@property
def query_band(self):
    """Query band passed on connection to database."""
    return self._query_band

@query_band.setter
def query_band(self, value):
    """Set the connection's query_band property.

    Args:
        value:  New query_band value. String.

    Returns:
        Nothing.
    """

    self._query_band = value
    # If query band is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['query-band-spec']
        except KeyError:
            pass
    else:
        self._connectionXML.set('query-band-spec', value)

@property
def initial_sql(self):
    """Initial SQL to be run."""
    return self._initial_sql

@initial_sql.setter
def initial_sql(self, value):
    """Set the connection's initial_sql property.

    Args:
        value:  New initial_sql value. String.

    Returns:
        Nothing.
    """

    self._initial_sql = value
    # If initial_sql is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['one-time-sql']
        except KeyError:
            pass
    else:
        self._connectionXML.set('one-time-sql', value)
@property
def schema(self):
    """Initial SQL to be run."""
    return self._schema

@schema.setter
def schema(self, value):
    """Set the connection's initial_sql property.

    Args:
        value:  New initial_sql value. String.

    Returns:
        Nothing.
    """

    self._schema = value
    # If schema is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['schema']
        except KeyError:
            pass
    else:
        self._connectionXML.set('schema', value)
@property
def http_path(self):
    """http_path."""
    return self._http_path

@http_path.setter
def http_path(self, value):
    """Set the connection's http_path property.

    Args:
        value:  New http_path value. String.

    Returns:
        Nothing.
    """

    self._http_path = value
    # If http_path is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['http-path']
        except KeyError:
            pass
    else:
        self._connectionXML.set('http-path', value)
@property
def connection_type(self):
    """Initial SQL to be run."""
    return self._connection_type

@connection_type.setter
def connection_type(self, value):
    """Set the connection's connection_type property.

    Args:
        value:  New connection_type value. Integer.

    Returns:
        Nothing.
    """

    self._connection_type = value
    # If connection_type is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['connection-type']
        except KeyError:
            pass
    else:
        self._connectionXML.set('connection-type', value)
@property
def sslcert(self):
    """sslcert."""
    return self._sslcert

@sslcert.setter
def sslcert(self, value):
    """Set the connection's sslcert property.

    Args:
        value:  New sslcert value. String.

    Returns:
        Nothing.
    """

    self._sslcert = value
    # If sslcert is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['sslcert']
        except KeyError:
            pass
    else:
        self._connectionXML.set('sslcert', value)
@property
def sslmode(self):
    """sslmode."""
    return self._sslmode

@sslmode.setter
def sslmode(self, value):
    """Set the connection's sslcert property.

    Args:
        value:  New sslcert value. String.

    Returns:
        Nothing.
    """

    self._sslmode = value
    # If sslmode is None we remove the element and don't write it to XML
    if value is None:
        try:
            del self._connectionXML.attrib['sslmode']
        except KeyError:
            pass
    else:
        self._connectionXML.set('sslmode', value)`