Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions 3 pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta:__legacy__"
5 changes: 4 additions & 1 deletion 5 tableaudocumentapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from .base import BaseObject
from .field import Field
from .connection import Connection
from .relation import Relation
from .connection import Connection, NamedConnection, RelationParser
from .datasource import Datasource, ConnectionParser
from .workbook import Workbook


__version__ = '0.0.1'
__VERSION__ = __version__
33 changes: 33 additions & 0 deletions 33 tableaudocumentapi/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@


class BaseObject:

def _to_dict(
self, base_attrs=[], to_dict_attrs=[],
to_dict_list_attrs=[], to_dict_of_dict_attrs=[]
):
base = {
k.replace('_', ''): getattr(self, k) for k in base_attrs
if getattr(self, k)
}
base.update(
{
k: getattr(self, k).to_dict() for k in to_dict_attrs
if getattr(self, k)
}
)
base.update(
{
k: [i.to_dict() for i in getattr(self, k)]
for k in to_dict_list_attrs
if getattr(self, k)
}
)
base.update(
{
i: {k:v.to_dict() for k, v in getattr(self, i).items()}
for i in to_dict_of_dict_attrs
if getattr(self, i)
}
)
return base
185 changes: 150 additions & 35 deletions 185 tableaudocumentapi/connection.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import xml.etree.ElementTree as ET
from tableaudocumentapi import BaseObject, Relation
from tableaudocumentapi.dbclass import is_valid_dbclass


class Connection(object):
"""A class representing connections inside Data Sources."""
class RelationParser(object):
"""Parser for detecting and extracting relations from Connection entities."""

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.
def __init__(self, connection_xml, version):
self._connxml = connection_xml
self._relversion = version

"""
def get_relations(self):
"""Find and return all relations."""
relations_xml = self._connxml.findall('./relation')
if relations_xml:
return list(map(Relation, relations_xml))
else:
return None


class BaseConnection(BaseObject):

def __init__(self, connxml, version=None):
self._connectionXML = connxml
self._class = connxml.get('class')
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)
self._port = connxml.get('port')
self._channel = connxml.get('channel')
self._dataserver_permissions = connxml.get('dataserver-permissions')
self._directory = connxml.get('directory')
self._server_oauth = connxml.get('server-oauth')
self._workgroup_auth_mode = connxml.get('workgroup-auth-mode')
self._query_band = connxml.get('query-band-spec')
self._initial_sql = connxml.get('one-time-sql')

def __repr__(self):
return "'<Connection server='{}' dbname='{}' @ {}>'".format(self._server, self._dbname, hex(id(self)))
Expand All @@ -39,9 +55,31 @@ def from_attributes(cls, server, dbname, username, dbclass, port=None, query_ban
xml.port = port
xml.query_band = query_band
xml.initial_sql = initial_sql

return xml

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

@class_.setter
def class_(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 dbname(self):
"""Database name for the connection. Not the table name."""
Expand Down Expand Up @@ -106,29 +144,6 @@ def username(self, value):
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."""
Expand Down Expand Up @@ -181,6 +196,26 @@ def query_band(self, value):
else:
self._connectionXML.set('query-band-spec', value)

@property
def channel(self):
return self._channel

@property
def dataserver_permissions(self):
return self._dataserver_permissions

@property
def directory(self):
return self._directory

@property
def server_oauth(self):
return self._server_oauth

@property
def workgroup_auth_mode(self):
return self._workgroup_auth_mode

@property
def initial_sql(self):
"""Initial SQL to be run."""
Expand All @@ -206,3 +241,83 @@ def initial_sql(self, value):
pass
else:
self._connectionXML.set('one-time-sql', value)

def base_dict(self):
base_attrs = [
'class_', 'dbname', 'server', 'username',
'authentication', 'port', 'channel', 'dataserver_permissions',
'directory', 'server_oauth', 'workgroup_auth_mode',
'query_band', 'initial_sql'
]
base = self._to_dict(
base_attrs=base_attrs
)
return base


class Connection(BaseConnection):

def __init__(self, connxml, version=None):
super().__init__(connxml, version=None)
self._named_connections = self._extract_named_connections()
self._relation_parser = RelationParser(
connxml, version=version
)
self._relations = self._relation_parser.get_relations()

def _extract_named_connections(self):
named_connections = [
conn for conn in self._connectionXML.findall('./named-connections/named-connection')
]
return {nc.name: nc for nc in list(map(NamedConnection, named_connections))}

@property
def named_connections(self):
return self._named_connections

@property
def relations(self):
return self._relations

def to_dict(self):
base = super().base_dict()
to_dict_list_attrs = ['relations']
to_dict_of_dict_attrs = ['named_connections']
base.update(
self._to_dict(
to_dict_list_attrs=to_dict_list_attrs,
to_dict_of_dict_attrs=to_dict_of_dict_attrs
)
)
return base


class NamedConnection(BaseConnection):
"""A class representing connections inside Data Sources."""

def __init__(self, connxml, version=None):
"""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.

"""
assert connxml.tag == 'named-connection', "Must be of type named-connection"
super().__init__(connxml.find('./connection'), version=version)
self._name = connxml.get('name')
self._caption = connxml.get('caption')

@property
def name(self):
return self._name

@property
def caption(self):
return self._caption

def to_dict(self):
base = super().base_dict()
base_attrs = ['name', 'caption']
base.update(
self._to_dict(base_attrs=base_attrs)
)
return base
48 changes: 26 additions & 22 deletions 48 tableaudocumentapi/datasource.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import collections
import itertools
import xml.etree.ElementTree as ET
import xml.sax.saxutils as sax
import functools
import collections
from uuid import uuid4
import xml.sax.saxutils as sax
import xml.etree.ElementTree as ET

from tableaudocumentapi import Connection, xfile
from tableaudocumentapi import Field
from tableaudocumentapi import BaseObject, Connection, Field, xfile
from tableaudocumentapi.multilookup_dict import MultiLookupDict
from tableaudocumentapi.xfile import xml_open

Expand Down Expand Up @@ -33,6 +33,14 @@ def _is_used_by_worksheet(names, field):
return any(y for y in names if y in field.worksheets)


def partialclass(cls, *args, **kwds):

class NewCls(cls):
__init__ = functools.partialmethod(cls.__init__, *args, **kwds)

return NewCls


class FieldDictionary(MultiLookupDict):

def used_by_sheet(self, name):
Expand Down Expand Up @@ -94,28 +102,14 @@ class ConnectionParser(object):
def __init__(self, datasource_xml, version):
self._dsxml = datasource_xml
self._dsversion = version

def _extract_federated_connections(self):
connections = list(map(Connection, self._dsxml.findall('.//named-connections/named-connection/*')))
# 'sqlproxy' connections (Tableau Server Connections) are not embedded into named-connection elements
# extract them manually for now
connections.extend(map(Connection, self._dsxml.findall("./connection[@class='sqlproxy']")))
return connections

def _extract_legacy_connection(self):
return list(map(Connection, self._dsxml.findall('connection')))
self.Connection = partialclass(Connection, version=version)

def get_connections(self):
"""Find and return all connections based on file format version."""

if float(self._dsversion) < 10:
connections = self._extract_legacy_connection()
else:
connections = self._extract_federated_connections()
return connections
return list(map(self.Connection, self._dsxml.findall('./connection')))


class Datasource(object):
class Datasource(BaseObject):
"""A class representing Tableau Data Sources, embedded in workbook files or
in TDS files.

Expand All @@ -133,6 +127,7 @@ def __init__(self, dsxml, filename=None):
'formatted-name') # TDS files don't have a name attribute
self._version = self._datasourceXML.get('version')
self._caption = self._datasourceXML.get('caption', '')
self._inline = True if self._datasourceXML.get('inline', '') == 'true' else False
self._connection_parser = ConnectionParser(
self._datasourceXML, version=self._version)
self._connections = self._connection_parser.get_connections()
Expand Down Expand Up @@ -245,3 +240,12 @@ def _get_metadata_objects(self):
def _get_column_objects(self):
return [_column_object_from_column_xml(self._datasourceTree, xml)
for xml in self._datasourceTree.findall('.//column')]

def to_dict(self):
base_attrs = ['name', 'version', 'caption']
to_dict_list_attrs = ['connections']
base = self._to_dict(
base_attrs=base_attrs,
to_dict_list_attrs=to_dict_list_attrs
)
return base
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.