diff --git a/examples/create_site_sample.py b/examples/create_site_sample.py index c42e521..570ab46 100644 --- a/examples/create_site_sample.py +++ b/examples/create_site_sample.py @@ -46,23 +46,23 @@ def tableau_rest_api_connection_version(): # Add in any default permissions you'd like at this point - admin_perms = default_proj.create_project_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.get_permissions_obj(group_name_or_luid='Administrators', role='Project Leader') - default_proj.set_permissions_by_permissions_obj_list([admin_perms, ]) + default_proj.set_permissions([admin_perms, ]) - admin_perms = default_proj.create_workbook_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.workbook_defaults.get_permissions_obj(group_name_or_luid='Administrators', role='Editor') admin_perms.set_capability(capability_name='Download Full Data', mode='Deny') - default_proj.workbook_defaults.set_permissions_by_permissions_obj_list([admin_perms, ]) + default_proj.workbook_defaults.set_permissions([admin_perms, ]) - admin_perms = default_proj.create_datasource_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.datasource_defaults.get_permissions_obj(group_name_or_luid='Administrators', role='Editor') - default_proj.datasource_defaults.set_permissions_by_permissions_obj_list([admin_perms, ]) + default_proj.datasource_defaults.set_permissions([admin_perms, ]) # Change one of these - new_perms = default_proj.create_project_permissions_object_for_group(group_name_or_luid='Administrators', + new_perms = default_proj.get_permissions_obj(group_name_or_luid='Administrators', role='Publisher') - default_proj.set_permissions_by_permissions_obj_list([new_perms, ]) + default_proj.set_permissions([new_perms, ]) # Create Additional Projects projects_to_create = ['Sandbox', 'Data Source Definitions', 'UAT', 'Finance', 'Real Financials'] @@ -117,21 +117,21 @@ def tableau_server_rest_version(): default_proj.clear_all_permissions() # This clears all, including the defaults # Add in any default permissions you'd like at this point - admin_perms = default_proj.create_project_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.get_permissions_obj(group_name_or_luid='Administrators', role='Project Leader') default_proj.set_permissions_by_permissions_obj_list([admin_perms, ]) - admin_perms = default_proj.create_workbook_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.workbook_defaults.get_permissions_obj(group_name_or_luid='Administrators', role='Editor') admin_perms.set_capability(capability_name='Download Full Data', mode='Deny') default_proj.workbook_defaults.set_permissions_by_permissions_obj_list([admin_perms, ]) - admin_perms = default_proj.create_datasource_permissions_object_for_group(group_name_or_luid='Administrators', + admin_perms = default_proj.datasource_defaults.get_permissions_obj(group_name_or_luid='Administrators', role='Editor') default_proj.datasource_defaults.set_permissions_by_permissions_obj_list([admin_perms, ]) # Change one of these - new_perms = default_proj.create_project_permissions_object_for_group(group_name_or_luid='Administrators', + new_perms = default_proj.get_permissions_obj(group_name_or_luid='Administrators', role='Publisher') default_proj.set_permissions_by_permissions_obj_list([new_perms, ]) diff --git a/examples/user_sync_sample.py b/examples/user_sync_sample.py index cd61c43..8733f07 100644 --- a/examples/user_sync_sample.py +++ b/examples/user_sync_sample.py @@ -66,8 +66,8 @@ for user in users_dict: proj_obj = t.projects.create_project("My Saved Reports - {}".format(user)) user_luid = users_dict[user] - perms_obj = proj_obj.create_project_permissions_object_for_user(username_or_luid=user_luid, role='Publisher') - proj_obj.set_permissions_by_permissions_obj_list([perms_obj, ]) + perms_obj = proj_obj.get_permissions_obj(username_or_luid=user_luid, role='Publisher') + proj_obj.set_permissions([perms_obj, ]) # Reset back to beginning to reuse query diff --git a/tableau_documents/tableau_datasource.py b/tableau_documents/tableau_datasource.py index 2de475f..ea41684 100644 --- a/tableau_documents/tableau_datasource.py +++ b/tableau_documents/tableau_datasource.py @@ -282,7 +282,7 @@ 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.table_relations: + for relation in self._tables_relations: if relation.get('type') == "table": relation.set('table', relation.get('table').replace("[{}]".format(original_db_or_schema), "[{}]".format(new_db_or_schema))) diff --git a/tableau_documents/tableau_file.py b/tableau_documents/tableau_file.py index 2bec8dc..7e96bb1 100644 --- a/tableau_documents/tableau_file.py +++ b/tableau_documents/tableau_file.py @@ -578,6 +578,8 @@ def save_new_file(self, new_filename_no_extension: str): return save_filename +# TFL files are actually JSON rather than XML. I don't think there are actually any XML calls except +# possibly when creating the TableauDocument file. class TFL(TableauXmlFile): @property diff --git a/tableau_rest_api/methods/workbook.py b/tableau_rest_api/methods/workbook.py index 1091c96..2bd87bd 100644 --- a/tableau_rest_api/methods/workbook.py +++ b/tableau_rest_api/methods/workbook.py @@ -12,7 +12,7 @@ def __getattr__(self, attr): def get_published_workbook_object(self, workbook_name_or_luid: str, project_name_or_luid: Optional[str] = None) -> Workbook: luid = self.query_workbook_luid(workbook_name_or_luid, project_name_or_luid) - wb_obj = Workbook(luid=luid, tableau_rest_api_obj=self, + wb_obj = Workbook(luid=luid, tableau_rest_api_obj=self.rest_api_base, default=False, logger_obj=self.logger) return wb_obj diff --git a/tableau_rest_api/published_content.py b/tableau_rest_api/published_content.py index 7bbf4b7..782d020 100644 --- a/tableau_rest_api/published_content.py +++ b/tableau_rest_api/published_content.py @@ -3,6 +3,8 @@ import copy from typing import Union, Any, Optional, List, Dict, TYPE_CHECKING +from ..tableau_rest_xml import TableauRestXml + if TYPE_CHECKING: from tableau_tools.logging_methods import LoggingMethods from tableau_tools.logger import Logger @@ -86,7 +88,7 @@ def get_permissions_obj(self, group_name_or_luid: Optional[str] = None, username # Copy Permissions for users or group def _copy_permissions_obj(self, perms_obj, user_or_group, name_or_luid): self.start_log_block() - if self.is_luid(name_or_luid): + if TableauRestXml.is_luid(name_or_luid): luid = name_or_luid else: if user_or_group == 'group': @@ -339,12 +341,7 @@ def are_capabilities_obj_dicts_identical(new_obj_dict: Dict, dest_obj_dict: Dict return False # Dict { capability_name : mode } into XML with checks for validity. Set type to 'workbook' or 'datasource' - def build_capabilities_xml_from_dict(self, capabilities_dict, obj_type): - """ - :type capabilities_dict: dict - :type obj_type: unicode - :return: ET.Element - """ + def build_capabilities_xml_from_dict(self, capabilities_dict: Dict, obj_type: str) -> ET.Element: if obj_type not in self.permissionable_objects: error_text = 'objtype can only be "project", "workbook" or "datasource", was given {}' raise InvalidOptionException(error_text.format('obj_type')) @@ -760,6 +757,7 @@ def get_permissions_obj(self, group_name_or_luid: Optional[str] = None, username role=role) + class Project(PublishedContent): def __init__(self, luid, tableau_rest_api_obj, logger_obj=None, content_xml_obj=None): @@ -782,7 +780,7 @@ def luid(self): @luid.setter def luid(self, name_or_luid): - if self.is_luid(name_or_luid): + if TableauRestXml.is_luid(name_or_luid): luid = name_or_luid else: luid = self.t_rest_api.query_project_luid(name_or_luid) @@ -925,7 +923,6 @@ def create_datasource_permissions_object_for_user(self, username_or_luid: str, return self._get_permissions_object(username_or_luid=username_or_luid, role=role, permissions_class_override=DatasourcePermissions) - @property def workbook_defaults(self) -> Workbook: return self._workbook_defaults diff --git a/tableau_rest_xml.py b/tableau_rest_xml.py new file mode 100644 index 0000000..975922b --- /dev/null +++ b/tableau_rest_xml.py @@ -0,0 +1,93 @@ +# This is intended to be full of static helper methods +import xml.etree.ElementTree as ET +from typing import Union, Optional, List, Dict, Tuple +import re + +class TableauRestXml: + tableau_namespace = 'http://tableau.com/api' + ns_map = {'t': 'http://tableau.com/api'} + ns_prefix = '{' + ns_map['t'] + '}' + # ET.register_namespace('t', ns_map['t']) + # Generic method for XML lists for the "query" actions to name -> id dict + @staticmethod + def convert_xml_list_to_name_id_dict(xml_obj: ET.Element) -> Dict: + d = {} + for element in xml_obj: + e_id = element.get("id") + # If list is collection, have to run one deeper + if e_id is None: + for list_element in element: + e_id = list_element.get("id") + name = list_element.get("name") + d[name] = e_id + else: + name = element.get("name") + d[name] = e_id + return d + + # Repeat of above method with shorter name + @staticmethod + def xml_list_to_dict(xml_obj: ET.Element) -> Dict: + d = {} + for element in xml_obj: + e_id = element.get("id") + # If list is collection, have to run one deeper + if e_id is None: + for list_element in element: + e_id = list_element.get("id") + name = list_element.get("name") + d[name] = e_id + else: + name = element.get("name") + d[name] = e_id + return d + + @staticmethod + def luid_name_dict_from_xml(xml_obj: ET.Element) -> Dict: + d = {} + for element in xml_obj: + e_id = element.get("id") + # If list is collection, have to run one deeper + if e_id is None: + for list_element in element: + e_id = list_element.get("id") + name = list_element.get("name") + d[e_id] = name + else: + name = element.get("name") + d[e_id] = name + return d + + @staticmethod + def luid_content_url_dict_from_xml(xml_obj: ET.Element) -> Dict: + d = {} + for element in xml_obj: + e_id = element.get("id") + # If list is collection, have to run one deeper + if e_id is None: + for list_element in element: + e_id = list_element.get("id") + name = list_element.get("contentUrl") + d[e_id] = name + else: + name = element.get("contentUrl") + d[e_id] = name + return d + + # This corrects for the first element in any response by the plural collection tag, which leads to differences + # with the XPath search currently + @staticmethod + def make_xml_list_iterable(xml_obj: ET.Element) -> List[ET.Element]: + pass + + # 32 hex characters with 4 dashes + @staticmethod + def is_luid(val: str) -> bool: + luid_pattern = r"[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*" + if len(val) == 36: + if re.match(luid_pattern, val) is not None: + return True + else: + return False + else: + return False \ No newline at end of file