diff --git a/README.md b/README.md index ac00c41..1ca0f52 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,10 @@ The Logger class by default only logs Requests but not Responses. If you need to `Logger.enable_debug_level()`b -### 0.3 TableauBase class -Many classes within the tableau_tools package inherit from the TableauBase class. TableauBase implements the `enable_logging(Logger)` method, along with other a `.log()` method that calls to `Logger.log()`. It also has many static methods, mapping dicts, and helper classes related to Tableau in general. +### 0.3 TableauRestXml class +There is a class called TableauRestXml which holds static methods and properties that are useful on any Tableau REST XML request or response. -It should never be necessary to use TableauBase by itself. +TableauServerRest and TableauRestApiConnection both inherit from this class so you can call any of the methods from one of those objects rather than calling it directly. ### 0.4 tableau_exceptions The tableau_exceptions file defines a variety of Exceptions that are specific to Tableau, particularly the REST API. They are not very complex, and most simply include a msg property that will clarify the problem if logged diff --git a/examples/test_suite_all_querying_tableau_server_rest.py b/examples/test_suite_all_querying_tableau_server_rest.py index 409d21b..36ad4fe 100644 --- a/examples/test_suite_all_querying_tableau_server_rest.py +++ b/examples/test_suite_all_querying_tableau_server_rest.py @@ -32,7 +32,6 @@ def run_tests(server_url: str, username: str, password: str, site_content_url: s site_content_url=site_content_url) t.signin() t.enable_logging(rest_request_log_obj) - # Server info and methods server_info = t.query_server_info() server_version = t.query_server_version() diff --git a/tableau_documents/tableau_datasource.py b/tableau_documents/tableau_datasource.py index ea41684..bf71134 100644 --- a/tableau_documents/tableau_datasource.py +++ b/tableau_documents/tableau_datasource.py @@ -98,23 +98,11 @@ def __init__(self, datasource_xml: Optional[ET.Element] = None, logger_obj: Opti "Hyper": 'hyper', } - # Create from new or from existing object + # Create from new. Only doing 10.5 style for now. if datasource_xml is None: - if ds_version is None: - raise InvalidOptionException('When creating Datasource from scratch, must declare a ds_version') - self._ds_version = ds_version - version_split = self._ds_version.split('.') - if version_split[0] == '10': - if int(version_split[1]) < 5: - self.ds_version_type = '10' - else: - self.ds_version_type = '10.5' - elif version_split[0] == '9': - self.ds_version_type = '9' - else: - raise InvalidOptionException('Datasource being created with wrong version type') + self.ds_version_type = '10.5' self.xml = self.create_new_datasource_xml(ds_version) - + # Read existing data source else: self.xml = datasource_xml if self.xml.get("caption"): @@ -173,15 +161,6 @@ def __init__(self, datasource_xml: Optional[ET.Element] = None, logger_obj: Opti else: self.log('Found a Parameters datasource') - - #self.repository_location = None - - #if self.xml.find(u'repository-location') is not None: - # if len(self.xml.find(u'repository-location')) == 0: - # self._published = True - # repository_location_xml = self.xml.find(u'repository-location') - # self.repository_location = repository_location_xml - # Grab the extract filename if there is an extract section if self.xml.find('extract') is not None: e = self.xml.find('extract') @@ -282,16 +261,21 @@ def published_ds_content_url(self, new_content_url: str): # It seems some databases like Oracle and Teradata need this as well to swap a database def update_tables_with_new_database_or_schema(self, original_db_or_schema: str, new_db_or_schema: str): - for relation in self._tables_relations: + for relation in self.tables.table_relations: if relation.get('type') == "table": relation.set('table', relation.get('table').replace("[{}]".format(original_db_or_schema), "[{}]".format(new_db_or_schema))) + # Start of data sources creation methods (from scratch) + # Need considerable review and testing + @staticmethod def create_new_datasource_xml(version: str) -> ET.Element: # nsmap = {u"user": u'http://www.tableausoftware.com/xml/user'} + # The most basic component is just a datasource element with a version. ds_xml = ET.Element("datasource") ds_xml.set('version', version) + # Unclear if this is even necessary. May only appear in TWBXs ds_xml.set('inline', "true") return ds_xml @@ -300,9 +284,7 @@ def create_new_connection_xml(ds_version: str, ds_type: str, server: str, db_nam authentication: Optional[str] = None, initial_sql: Optional[str] = None) -> ET.Element: connection = ET.Element("connection") - if ds_version == '9': - c = connection - elif ds_version in ['10', '10.5']: + if ds_version in ['10', '10.5']: nc = ET.Element('named-connection') nc.set('caption', 'Connection') # Connection has a random number of 20 digits appended @@ -427,23 +409,6 @@ def translate_columns(self, translation_dict: Dict): self.columns.translate_captions(translation_dict=translation_dict) self.end_log_block() - def add_extract(self, new_extract_filename: str): - self.log('add_extract called, checking if extract exists already') - # Test to see if extract exists already - e = self.xml.find('extract') - if e is not None: - self.log("Existing extract found, no need to add") - raise AlreadyExistsException("An extract already exists, can't add a new one", "") - else: - self.log('Extract doesnt exist') - new_extract_filename_start = new_extract_filename.split(".")[0] - if self.ds_version_type == '10.5': - final_extract_filename = "{}.hyper".format(new_extract_filename_start) - else: - final_extract_filename = "{}.tde".format(new_extract_filename_start) - self._extract_filename = final_extract_filename - self.log('Adding extract to the data source') - def generate_extract_section(self) -> Union[ET.Element, bool]: # Short circuit if no extract had been set if self._extract_filename is None: diff --git a/tableau_rest_api/methods/rest_api_base.py b/tableau_rest_api/methods/rest_api_base.py index df6bcf6..72e3355 100644 --- a/tableau_rest_api/methods/rest_api_base.py +++ b/tableau_rest_api/methods/rest_api_base.py @@ -287,20 +287,24 @@ def signin(self, user_luid_to_impersonate: Optional[str] = None): self._request_obj.xml_request = tsr self._request_obj.http_verb = 'post' self.log('Login payload is\n {}'.format(ET.tostring(tsr))) + try: + self._request_obj.request_from_api(0) + # self.log(api.get_raw_response()) + xml = self._request_obj.get_response() - self._request_obj.request_from_api(0) - # self.log(api.get_raw_response()) - xml = self._request_obj.get_response() - - credentials_element = xml.findall('.//t:credentials', self.ns_map) - self.token = credentials_element[0].get("token") - self.log("Token is " + self.token) - self._request_obj.token = self.token - self.site_luid = credentials_element[0].findall(".//t:site", self.ns_map)[0].get("id") - self.user_luid = credentials_element[0].findall(".//t:user", self.ns_map)[0].get("id") - self.log("Site ID is " + self.site_luid) - self._request_obj.url = None - self._request_obj.xml_request = None + credentials_element = xml.findall('.//t:credentials', self.ns_map) + self.token = credentials_element[0].get("token") + self.log("Token is " + self.token) + self._request_obj.token = self.token + self.site_luid = credentials_element[0].findall(".//t:site", self.ns_map)[0].get("id") + self.user_luid = credentials_element[0].findall(".//t:user", self.ns_map)[0].get("id") + self.log("Site ID is " + self.site_luid) + self._request_obj.url = None + self._request_obj.xml_request = None + except RecoverableHTTPException as e: + if e.tableau_error_code == '401001': + self.end_log_block() + raise NotSignedInException('Sign-in failed due to wrong credentials') self.end_log_block() def swap_token(self, site_luid: str, user_luid: str, token: str): @@ -374,6 +378,8 @@ def query_resource(self, url_ending: str, server_level:bool = False, filters: Op sorts: Optional[List[Sort]] = None, additional_url_ending: Optional[str] = None, fields: Optional[List[str]] = None) -> ET.Element: self.start_log_block() + if self.token == "": + raise NotSignedInException('Must use .signin() to create REST API session first') url_endings = [] if filters is not None: if len(filters) > 0: @@ -525,6 +531,8 @@ def query_resource_json(self, url_ending: str, server_level: bool = False, sorts: Optional[List[Sort]] = None, additional_url_ending: str = None, fields: Optional[List[str]] = None, page_number: Optional[int] = None) -> Dict: self.start_log_block() + if self.token == "": + raise NotSignedInException('Must use .signin() to create REST API session first') url_endings = [] if filters is not None: if len(filters) > 0: diff --git a/tableau_rest_api/permissions.py b/tableau_rest_api/permissions.py index 0730980..68f8a3a 100644 --- a/tableau_rest_api/permissions.py +++ b/tableau_rest_api/permissions.py @@ -304,7 +304,7 @@ def __init__(self, group_or_user: str, luid: str, content_type: Optional[str] = '3.6': server_content_roles_3_5 } - self.__server_to_rest_capability_map = { + self.server_to_rest_capability_map = { 'Add Comment': 'AddComment', 'Move': 'ChangeHierarchy', 'Set Permissions': 'ChangePermissions', @@ -365,15 +365,15 @@ def group_or_user(self, group_or_user): # Just use the direct "to_allow" and "to_deny" methods def set_capability(self, capability_name: str, mode: str): - if capability_name not in list(self.__server_to_rest_capability_map.values()): + if capability_name not in list(self.server_to_rest_capability_map.values()): # If it's the Tableau UI naming, translate it over - if capability_name in self.__server_to_rest_capability_map: + if capability_name in self.server_to_rest_capability_map: # InheritedProjectLeader (2.8+) is Read-Only if capability_name == 'InheritedProjectLeader': self.log('InheritedProjectLeader permission is read-only, skipping') return if capability_name != 'all': - capability_name = self.__server_to_rest_capability_map[capability_name] + capability_name = self.server_to_rest_capability_map[capability_name] else: raise InvalidOptionException('"{}" is not a capability in REST API or Server'.format(capability_name)) self.capabilities[capability_name] = mode @@ -387,23 +387,23 @@ def set_capability_to_deny(self, capability_name: str): def set_capability_to_unspecified(self, capability_name: str): if capability_name not in self.capabilities: # If it's the Tableau UI naming, translate it over - if capability_name in self.__server_to_rest_capability_map: + if capability_name in self.server_to_rest_capability_map: if capability_name == 'InheritedProjectLeader': self.log('InheritedProjectLeader permission is read-only, skipping') return if capability_name != 'all': - capability_name = self.__server_to_rest_capability_map[capability_name] + capability_name = self.server_to_rest_capability_map[capability_name] else: raise InvalidOptionException('"{}" is not a capability in REST API or Server'.format(capability_name)) self.capabilities[capability_name] = None # This exists specifically to allow the setting of read-only permissions def _set_capability_from_published_content(self, capability_name: str, mode: str): - if capability_name not in list(self.__server_to_rest_capability_map.values()): + if capability_name not in list(self.server_to_rest_capability_map.values()): # If it's the Tableau UI naming, translate it over - if capability_name in self.__server_to_rest_capability_map: + if capability_name in self.server_to_rest_capability_map: if capability_name != 'all': - capability_name = self.__server_to_rest_capability_map[capability_name] + capability_name = self.server_to_rest_capability_map[capability_name] else: raise InvalidOptionException('"{}" is not a capability in REST API or Server'.format(capability_name)) self.capabilities[capability_name] = mode diff --git a/tableau_rest_api/published_content.py b/tableau_rest_api/published_content.py index 11d6340..a7ab699 100644 --- a/tableau_rest_api/published_content.py +++ b/tableau_rest_api/published_content.py @@ -950,9 +950,9 @@ def are_permissions_locked(self) -> bool: def lock_permissions(self): self.start_log_block() if self.permissions_locked is False: - if(isinstance(self.t_rest_api, TableauRestApiConnection)): + if(type(self.t_rest_api).__name__.contains('TableauRestApiConnection')): self.t_rest_api.update_project(self.luid, locked_permissions=True) - if(isinstance(self.t_rest_api, TableauServerRest)): + if(type(self.t_rest_api).__name__.contains('TableauServerRest')): self.t_rest_api.projects.update_project(self.luid, locked_permissions=True) self.end_log_block()