Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
19fd25f
adding permissions support for workbook, datasource, project
DepthDeluxe May 23, 2018
2239612
first rename
t8y8 Aug 30, 2018
28a66d0
checkpoint
t8y8 Aug 31, 2018
751901b
fix merge conflict
May 7, 2019
0670e42
checkpoint 1 of refactor, woo!
May 7, 2019
9641298
A new checkpoint, working update, delete, and populate
t8y8 May 8, 2019
c2e7ac9
linter fixes
t8y8 May 8, 2019
bb84698
Small refactor using properities correctly
t8y8 May 8, 2019
6cc5567
Keep Grantee classes internal
t8y8 Jun 4, 2019
ae27f28
checkpoint for feedback
t8y8 Jun 5, 2019
b1f629e
fix merge
t8y8 Jun 5, 2019
0ca12d9
defaults checkpoint 2
t8y8 Jun 5, 2019
f729296
small cleanup
t8y8 Jun 5, 2019
cbccf3f
checkpoint 3, with cleanup
t8y8 Jun 5, 2019
a4abfb1
Remove ExplicitPermissions
t8y8 Jun 5, 2019
326f224
checkpoint 4
t8y8 Jun 5, 2019
ac59af7
fix breakage
t8y8 Jun 6, 2019
38a2700
Updates based on code review
t8y8 Jun 11, 2019
79b611e
Fix up the constants
t8y8 Jun 11, 2019
e751068
Checkpoint 1 for external content support
t8y8 Jun 13, 2019
9f10736
small fix
t8y8 Jun 14, 2019
062fa85
style fixes
t8y8 Jun 15, 2019
a8b1878
fix logging
t8y8 Jun 20, 2019
d1d0b62
Fix some small bugs, and disable an endpoint that is not yet implemented
t8y8 Jun 26, 2019
08739ee
merge conflicts
t8y8 Jul 27, 2019
bfbd864
merge conflict 2
t8y8 Jul 27, 2019
69f13b3
Cleanup
t8y8 Aug 9, 2019
2fda34b
fix
t8y8 Aug 9, 2019
a1c68e5
Final fix
t8y8 Aug 9, 2019
81df01f
cleanup, constructors, tests
t8y8 Aug 21, 2019
333685d
test codestyle
t8y8 Aug 21, 2019
0e36d76
final mr feedback
t8y8 Aug 22, 2019
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
8 changes: 4 additions & 4 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
SubscriptionItem, Target, PermissionsRule, Permission
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem,\
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError,\
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem,\
SubscriptionItem, Target, PermissionsRule, Permission, DatabaseItem, TableItem, ColumnItem
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
from ._version import get_versions
Expand Down
3 changes: 3 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from .connection_credentials import ConnectionCredentials
from .connection_item import ConnectionItem
from .column_item import ColumnItem
from .datasource_item import DatasourceItem
from .database_item import DatabaseItem
from .exceptions import UnpopulatedPropertyError
from .group_item import GroupItem
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
Expand All @@ -13,6 +15,7 @@
from .tableau_auth import TableauAuth
from .personal_access_token_auth import PersonalAccessTokenAuth
from .target import Target
from .table_item import TableItem
from .task_item import TaskItem
from .user_item import UserItem
from .view_item import ViewItem
Expand Down
69 changes: 69 additions & 0 deletions tableauserverclient/models/column_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import xml.etree.ElementTree as ET

from .property_decorators import property_is_enum, property_not_empty
from .exceptions import UnpopulatedPropertyError


class ColumnItem(object):
def __init__(self, name, description=None):
self._id = None
self.description = description
self.name = name

@property
def id(self):
return self._id

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

@name.setter
@property_not_empty
def name(self, value):
self._name = value

@property
def description(self):
return self._description

@description.setter
def description(self, value):
self._description = value

@property
def remote_type(self):
return self._remote_type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should also be defined in the constructor to keep it consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point -- what is the pattern here?
That's not a user-editable attribute, and it's something we set purely from the XML response, sometimes we put these in constructors, sometimes we don't.

I can't add it here, just wondering.


def _set_values(self, id, name, description, remote_type):
if id is not None:
self._id = id
if name:
self._name = name
if description:
self.description = description
if remote_type:
self._remote_type = remote_type

@classmethod
def from_response(cls, resp, ns):
all_column_items = list()
parsed_response = ET.fromstring(resp)
all_column_xml = parsed_response.findall('.//t:column', namespaces=ns)

for column_xml in all_column_xml:
(id, name, description, remote_type) = cls._parse_element(column_xml, ns)
column_item = cls(name)
column_item._set_values(id, name, description, remote_type)
all_column_items.append(column_item)

return all_column_items

@staticmethod
def _parse_element(column_xml, ns):
id = column_xml.get('id', None)
name = column_xml.get('name', None)
description = column_xml.get('description', None)
remote_type = column_xml.get('remoteType', None)

return id, name, description, remote_type
260 changes: 260 additions & 0 deletions tableauserverclient/models/database_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import xml.etree.ElementTree as ET

from .permissions_item import Permission

from .property_decorators import property_is_enum, property_not_empty, property_is_boolean
from .exceptions import UnpopulatedPropertyError


class DatabaseItem(object):
class ContentPermissions:
LockedToProject = 'LockedToDatabase'
ManagedByOwner = 'ManagedByOwner'

def __init__(self, name, description=None, content_permissions=None):
self._id = None
self.name = name
self.description = description
self.content_permissions = content_permissions
self._certified = None
self._certification_note = None
self._contact_id = None

self._connector_url = None
self._connection_type = None
self._embedded = None
self._file_extension = None
self._file_id = None
self._file_path = None
self._host_name = None
self._metadata_type = None
self._mime_type = None
self._port = None
self._provider = None
self._request_url = None

self._permissions = None
self._default_table_permissions = None

self._tables = None # Not implemented yet

@property
def content_permissions(self):
return self._content_permissions

@property
def permissions(self):
if self._permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._permissions()

@property
def default_table_permissions(self):
if self._default_table_permissions is None:
error = "Project item must be populated with permissions first."
raise UnpopulatedPropertyError(error)
return self._default_table_permissions()

@content_permissions.setter
@property_is_enum(ContentPermissions)
def content_permissions(self, value):
self._content_permissions = value

@property
def id(self):
return self._id

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

@name.setter
@property_not_empty
def name(self, value):
self._name = value

@property
def description(self):
return self._description

@description.setter
def description(self, value):
self._description = value

@property
def embedded(self):
return self._embedded

@property
def certified(self):
return self._certified

@certified.setter
@property_is_boolean
def certified(self, value):
self._certified = value

@property
def certification_note(self):
return self._certification_note

@certification_note.setter
def certification_note(self, value):
self._certification_note = value

@property
def metadata_type(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one and a few below are not defined in the constructor. Also for port, should it be an integer property?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of these properties will exist for all types (though metadata_type should).

Right now it lazily adds them based on whatever we pass to _set_values.
We could enumerate them all, but I thought it was a lot of copy/paste work to add them there and to _set_values.

Happy to do it if you think we should. It raises the question again about what makes it into constructor vs what doesn't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, I think it's good practice to have all properties defined in the constructor. That way we can easily see all the properties that the class can have, and avoids the possibility of an error from accessing it before assigned from the response.
Looks like we've been keeping that pattern for the other models as well.

return self._metadata_type

@property
def host_name(self):
return self._host_name

@property
def port(self):
return self._port

@property
def file_path(self):
return self._file_path

@property
def provider(self):
return self._provider

@property
def mime_type(self):
return self._mime_type

@property
def connector_url(self):
return self._connector_url

@property
def connection_type(self):
return self._connection_type

@property
def request_url(self):
return self._request_url

@property
def file_extension(self):
return self._file_extension

@property
def file_id(self):
return self._file_id

@property
def contact_id(self):
return self._contact_id

@contact_id.setter
def contact_id(self, value):
self._contact_id = value

@property
def tables(self):
if self._tables is None:
error = "Database must be populated with tables first."
raise UnpopulatedPropertyError(error)
# Each call to `.tables` should create a new pager, this just runs the callable
return self._tables()

def _set_values(self, database_values):
# ID & Settable
if 'id' in database_values:
self._id = database_values['id']

if 'contact' in database_values:
self._contact_id = database_values['contact']['id']

if 'name' in database_values:
self._name = database_values['name']

if 'description' in database_values:
self._description = database_values['description']

if 'isCertified' in database_values:
self._certified = string_to_bool(database_values['isCertified'])

if 'certificationNote' in database_values:
self._certification_note = database_values['certificationNote']

# Not settable, alphabetical

if 'connectionType' in database_values:
self._connection_type = database_values['connectionType']

if 'connectorUrl' in database_values:
self._connector_url = database_values['connectorUrl']

if 'contentPermissions' in database_values:
self._content_permissions = database_values['contentPermissions']

if 'embedded' in database_values:
self._embedded = string_to_bool(database_values['embedded'])

if 'fileExtension' in database_values:
self._file_extension = database_values['fileExtension']

if 'fileId' in database_values:
self._file_id = database_values['fileId']

if 'filePath' in database_values:
self._file_path = database_values['filePath']

if 'hostName' in database_values:
self._host_name = database_values['hostName']

if 'mimeType' in database_values:
self._mime_type = database_values['mimeType']

if 'port' in database_values:
self._port = int(database_values['port'])

if 'provider' in database_values:
self._provider = database_values['provider']

if 'requestUrl' in database_values:
self._request_url = database_values['requestUrl']

if 'type' in database_values:
self._metadata_type = database_values['type']

def _set_permissions(self, permissions):
self._permissions = permissions

def _set_tables(self, tables):
self._tables = tables

def _set_default_permissions(self, permissions, content_type):
setattr(self, "_default_{content}_permissions".format(content=content_type), permissions)

@classmethod
def from_response(cls, resp, ns):
all_database_items = list()
parsed_response = ET.fromstring(resp)
all_database_xml = parsed_response.findall('.//t:database', namespaces=ns)

for database_xml in all_database_xml:
parsed_database = cls._parse_element(database_xml, ns)
database_item = cls(parsed_database['name'])
database_item._set_values(parsed_database)
all_database_items.append(database_item)
return all_database_items

@staticmethod
def _parse_element(database_xml, ns):
database_values = database_xml.attrib.copy()
contact = database_xml.find('.//t:contact', namespaces=ns)
if contact is not None:
database_values['contact'] = contact.attrib.copy()
return database_values


# Used to convert string represented boolean to a boolean type
def string_to_bool(s):
return s.lower() == 'true'
2 changes: 2 additions & 0 deletions tableauserverclient/models/permissions_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Resource:
Workbook = 'workbook'
Datasource = 'datasource'
Flow = 'flow'
Table = 'table'
Database = 'database'


class PermissionsRule(object):
Expand Down
Loading