From 57ebed6f0c1ca0916fc4484ad53b6a3a199e8e5c Mon Sep 17 00:00:00 2001 From: Bryant Howell Date: Wed, 13 Nov 2019 14:10:32 -0600 Subject: [PATCH] Lots of README material updated --- README.md | 54 ++++++++++++++++++++-- tableau_rest_api/methods/datasource.py | 2 +- tableau_rest_api/methods/schedule.py | 62 +++++++++++++------------- 3 files changed, 82 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f48487c..edc0734 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The TableauDatasource class uses the `TDEFileGenerator` and/or the `HyperFileGen * [0.2 Logger class](#02-logger-class) * [0.3 TableauBase class](#03-tableaubase-class) * [0.4 tableau_exceptions](#04-tableau-exceptions) + * [0.5 ElementTree.Element for XML handling](#05-elementtree) - [1. tableau_rest_api sub-package](#1-tableau-rest-api-sub-package) * [1.1 Connecting](#11-connecting) + [1.1.2 Enabling logging for TableauRestApiConnection classes](#112-enabling-logging-for-tableaurestapiconnection-classes) @@ -194,7 +195,7 @@ where l is a string. You do not need to add a "\n", it will be added automatical The Logger class by default only logs Requests but not Responses. If you need to see the full responses, use the following method: -`Logger.enable_debug_level()` +`Logger.enable_debug_level()`b ### 0.3 TableauBase class @@ -205,8 +206,8 @@ It should never be necessary to use TableauBase by itself. ### 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 -### 0.5 ElementTree elements for XML -All XML in tableau_tools is handled through ElementTree. It is aliased +### 0.5 ElementTree.Element for XML handling +All XML in tableau_tools is handled through ElementTree. It is aliased as ET per the standard Python documentation (https://docs.python.org/3/library/xml.etree.elementtree.html) . If you see a return type of ET.Element, that means you are dealing with an ElementTree.Element object -- basically the raw response from the Tableau REST API, or some kind of slice of one. ## 1. tableau_rest_api sub-package @@ -358,7 +359,32 @@ You can get all of the values from the previous object, if it has been signed in Now any actions you take with t2 will be the same session to the Tableau Server as those taken with t1. This can also be very useful for multi-threaded programming, where you may want to clone instances of an object so that they don't interact with one another while doing things on different threads, but still use the same REST API session. ### 1.2 Basics and Querying +#### 1.2.0 ElementTree.Element XML Responses +As mentioned in the 0 section, tableau_tools returns ElementTree.Element responses for any XML request. To know exactly what is available in any given object coming back from the Tableau REST API, you'll need to either consult the REST API Reference or convert to it something you can write to the console or a text file. + t = TableauServerRest("http://127.0.0.1", "admin", "adminsp@ssw0rd", site_content_url="site1") + t.signin() + groups = t.groups.query_groups() + print(ET.tostring(groups)) + +If it is a collection of elements, you can iterate through using the standard Python for.. in loop. If you know there is an attribute you want (per the Reference or the printed response), you can access it via the `.get(attribute_name)` method of the Element object: + + for group in groups: + print(ET.tostring(group)) + group_luid = group.get('id') + group_name = group.get('name') + + +Some XML responses are more complex, with element nodes within other elements. An example of this would be workbooks, which have a project tag inside the workbook tag. The Pythonic way is to iterate through the workbook object to get to the sub-objects. You must watch out, though, because the Tableau REST API uses XML Namespaces, so you can't simply match the tag names directly using the `==` operator. Instead, you are better off using `.find()` string method to find a match with the tag name you are looking for: + + wbs = t.workbooks.query_workbooks() + for wb in wbs: + print(ET.tostring(wb)) + for elem in wb: + # Only want the project element inside + if elem.tag.find('project') != -1: + proj_luid = elem.get('id') + #### 1.2.1 LUIDs - Locally Unique IDentifiers The Tableau REST API represents each object on the server (project, workbook, user, group, etc.) with a Locally Unique IDentifier (LUID). Every command other than the sign-in to a particular site (which uses the `site_content_url`) requires a LUID. LUIDs are returned when you create an object on the server, or they can be retrieved by the Query methods and then searched to find the matching LUID. In the XML or JSON, they are labeled with the `id` value, but tableau_tools specifically refers to them as LUID throughout, because there are other Tableau IDs in the Tableau Server repoistory. @@ -550,7 +576,7 @@ Most methods follow this pattern: Yo'll notice that `query_workbook` and `query_datasource` include parameters for the project (and the username for workbooks). This is because workbook and datasource names are only unique within a Project of a Site, not within a Site. If you search without the project specified, the method will return a workbook if only one is found, but if multiple are found, it will throw a `MultipleMatchesFoundException` . -Unlike almost every other singular method, `query_project` returns a `Project` object, which is necessary when setting Permissions, rather than an `etree.Element` . This does take some amount of time, because all of the underlying permissions and default permissions on the project are requested when creating the Project object +Unlike almost every other singular method, `query_project` returns a `Project` object, which is necessary when setting Permissions, rather than an `ET.Element` . This does take some amount of time, because all of the underlying permissions and default permissions on the project are requested when creating the Project object `TableauRestApiConnection.query_project(project_name_or_luid) : returns Project` @@ -613,6 +639,26 @@ Ex. # or if using TableauServerRest new_luid = t.groups.create_group('Awesome People') +##### 1.3.2.1 direct_xml_request arguments on ADD / CREATE methods for duplicating site information +Some, if not all, of the create / add methods implement an optional `direct_xml_request` parameter, which allows you to submit your own ET.Element, starting with the tsRequest tag. When there is any value sent as an argument for this parameter, that method will ignore any values from the other arguments and just submit the Element you send in. + +The original purpose of this parameter is for allowing easy duplication of content from one site/server to another in conjunction with + +`build_request_from_response(request: ET.Element)` + +This method automatically converts any single Tableau REST API XML tsResponse object into a tsRequest -- in particular, it removes any IDs, so that the tsRequest can be submitted to create a new element with the new settings. + + t = TableauServerRest("http://127.0.0.1", "admin", "adminsp@ssw0rd", site_content_url="site1") + t.signin() + + t2 = TableauServerRest("http://127.0.0.1", "admin", "adminsp@ssw0rd", site_content_url="duplicate_site1") + t2.signin() + + o_groups = t.groups.query_groups() + for group in groups: + new_group_request = t.build_request_from_response(group) + new_group_luid = t2.groups.create_group(direct_xml_request=new_group_request) + #### 1.3.3 Adding users to a Group Once users have been created, they can be added into a group via the following method, which can take either a single string or a list/tuple set. Anywhere you see the `"or_luid_s"` pattern in a parameter, it means you can pass a string or a list of strings to make the action happen to all of those in the list. diff --git a/tableau_rest_api/methods/datasource.py b/tableau_rest_api/methods/datasource.py index d1ee22d..cf28b92 100644 --- a/tableau_rest_api/methods/datasource.py +++ b/tableau_rest_api/methods/datasource.py @@ -208,7 +208,7 @@ def __init__(self, rest_api_base: TableauRestApiBase27): def update_datasource(self, datasource_name_or_luid: str, datasource_project_name_or_luid: Optional[str] = None, new_datasource_name: Optional[str] = None, new_project_luid: Optional[str] = None, - new_owner_luid: Optional[str] = None, certification_status: Optional[str] = None, + new_owner_luid: Optional[str] = None, certification_status: Optional[bool] = None, certification_note: Optional[str] = None) -> ET.Element: self.start_log_block() if certification_status not in [None, False, True]: diff --git a/tableau_rest_api/methods/schedule.py b/tableau_rest_api/methods/schedule.py index 861876c..08d351c 100644 --- a/tableau_rest_api/methods/schedule.py +++ b/tableau_rest_api/methods/schedule.py @@ -6,7 +6,7 @@ def __init__(self, rest_api_base: TableauRestApiBase): def __getattr__(self, attr): return getattr(self.rest_api_base, attr) - def query_schedules(self) -> etree.Element: + def query_schedules(self) -> ET.Element: self.start_log_block() schedules = self.query_resource("schedules", server_level=True) self.end_log_block() @@ -18,14 +18,14 @@ def query_schedules_json(self, page_number: Optional[int] = None)-> str: self.end_log_block() return schedules - def query_extract_schedules(self) -> etree.Element: + def query_extract_schedules(self) -> ET.Element: self.start_log_block() schedules = self.query_schedules() extract_schedules = schedules.findall('.//t:schedule[@type="Extract"]', self.ns_map) self.end_log_block() return extract_schedules - def query_subscription_schedules(self) -> etree.Element: + def query_subscription_schedules(self) -> ET.Element: self.start_log_block() schedules = self.query_schedules() subscription_schedules = schedules.findall('.//t:schedule[@type="Subscription"]', self.ns_map) @@ -34,13 +34,13 @@ def query_subscription_schedules(self) -> etree.Element: - def query_schedule(self, schedule_name_or_luid: str) -> etree.Element: + def query_schedule(self, schedule_name_or_luid: str) -> ET.Element: self.start_log_block() schedule = self.query_single_element_from_endpoint('schedule', schedule_name_or_luid, server_level=True) self.end_log_block() return schedule - def query_extract_refresh_tasks_by_schedule(self, schedule_name_or_luid: str) -> etree.Element: + def query_extract_refresh_tasks_by_schedule(self, schedule_name_or_luid: str) -> ET.Element: self.start_log_block() luid = self.query_schedule_luid(schedule_name_or_luid) tasks = self.query_resource("schedules/{}/extracts".format(luid)) @@ -52,7 +52,7 @@ def create_schedule(self, name: Optional[str] = None, extract_or_subscription: O priority: Optional[int] = None, start_time: Optional[str] = None, end_time: Optional[str] = None, interval_value_s: Optional[Union[List[str], str]] = None, interval_hours_minutes: Optional[int] = None, - direct_xml_request: Optional[etree.Element] = None) -> str: + direct_xml_request: Optional[ET.Element] = None) -> str: self.start_log_block() if direct_xml_request is not None: tsr = direct_xml_request @@ -65,25 +65,25 @@ def create_schedule(self, name: Optional[str] = None, extract_or_subscription: O raise InvalidOptionException("parallel_or_serial must be 'Parallel' or 'Serial'") if frequency not in ['Hourly', 'Daily', 'Weekly', 'Monthly']: raise InvalidOptionException("frequency must be 'Hourly', 'Daily', 'Weekly' or 'Monthly'") - tsr = etree.Element('tsRequest') - s = etree.Element('schedule') + tsr = ET.Element('tsRequest') + s = ET.Element('schedule') s.set('name', name) s.set('priority', str(priority)) s.set('type', extract_or_subscription) s.set('frequency', frequency) s.set('executionOrder', parallel_or_serial) - fd = etree.Element('frequencyDetails') + fd = ET.Element('frequencyDetails') fd.set('start', start_time) if end_time is not None: fd.set('end', end_time) - intervals = etree.Element('intervals') + intervals = ET.Element('intervals') # Daily does not need an interval value if interval_value_s is not None: ivs = self.to_list(interval_value_s) for i in ivs: - interval = etree.Element('interval') + interval = ET.Element('interval') if frequency == 'Hourly': if interval_hours_minutes is None: raise InvalidOptionException( @@ -115,7 +115,7 @@ def update_schedule(self, schedule_name_or_luid: str, new_name: Optional[str] = priority: Optional[int] = None, start_time: Optional[str] = None, end_time: Optional[str] = None, interval_value_s: Optional[Union[List[str], str]] = None, interval_hours_minutes: Optional[int] = None, - direct_xml_request: Optional[etree.Element] = None) -> etree.Element: + direct_xml_request: Optional[ET.Element] = None) -> ET.Element: self.start_log_block() if self.is_luid(schedule_name_or_luid): luid = schedule_name_or_luid @@ -124,8 +124,8 @@ def update_schedule(self, schedule_name_or_luid: str, new_name: Optional[str] = if direct_xml_request is not None: tsr = direct_xml_request else: - tsr = etree.Element('tsRequest') - s = etree.Element('schedule') + tsr = ET.Element('tsRequest') + s = ET.Element('schedule') if new_name is not None: s.set('name', new_name) if priority is not None: @@ -141,18 +141,18 @@ def update_schedule(self, schedule_name_or_luid: str, new_name: Optional[str] = if frequency is not None: if frequency not in ['Hourly', 'Daily', 'Weekly', 'Monthly']: raise InvalidOptionException("frequency must be 'Hourly', 'Daily', 'Weekly' or 'Monthly'") - fd = etree.Element('frequencyDetails') + fd = ET.Element('frequencyDetails') fd.set('start', start_time) if end_time is not None: fd.set('end', end_time) - intervals = etree.Element('intervals') + intervals = ET.Element('intervals') # Daily does not need an interval value if interval_value_s is not None: ivs = self.to_list(interval_value_s) for i in ivs: - interval = etree.Element('interval') + interval = ET.Element('interval') if frequency == 'Hourly': if interval_hours_minutes is None: raise InvalidOptionException( @@ -178,8 +178,8 @@ def disable_schedule(self, schedule_name_or_luid: str): self.start_log_block() luid = self.query_schedule_luid(schedule_name_or_luid) - tsr = etree.Element('tsRequest') - s = etree.Element('schedule') + tsr = ET.Element('tsRequest') + s = ET.Element('schedule') s.set('state', 'Suspended') tsr.append(s) @@ -191,8 +191,8 @@ def enable_schedule(self, schedule_name_or_luid: str): self.start_log_block() luid = self.query_schedule_luid(schedule_name_or_luid) - tsr = etree.Element('tsRequest') - s = etree.Element('schedule') + tsr = ET.Element('tsRequest') + s = ET.Element('schedule') s.set('state', 'Active') tsr.append(s) @@ -372,15 +372,15 @@ def __init__(self, rest_api_base: TableauRestApiBase28): self.rest_api_base = rest_api_base def add_workbook_to_schedule(self, wb_name_or_luid: str, schedule_name_or_luid: str, - proj_name_or_luid: Optional[str] = None) -> etree.Element: + proj_name_or_luid: Optional[str] = None) -> ET.Element: self.start_log_block() wb_luid = self.query_workbook_luid(wb_name_or_luid, proj_name_or_luid) schedule_luid = self.query_schedule_luid(schedule_name_or_luid) - tsr = etree.Element('tsRequest') - t = etree.Element('task') - er = etree.Element('extractRefresh') - w = etree.Element('workbook') + tsr = ET.Element('tsRequest') + t = ET.Element('task') + er = ET.Element('extractRefresh') + w = ET.Element('workbook') w.set('id', wb_luid) er.append(w) t.append(er) @@ -393,16 +393,16 @@ def add_workbook_to_schedule(self, wb_name_or_luid: str, schedule_name_or_luid: return response def add_datasource_to_schedule(self, ds_name_or_luid: str, schedule_name_or_luid: str, - proj_name_or_luid: Optional[str] = None) -> etree.Element: + proj_name_or_luid: Optional[str] = None) -> ET.Element: self.start_log_block() ds_luid = self.query_workbook_luid(ds_name_or_luid, proj_name_or_luid) schedule_luid = self.query_schedule_luid(schedule_name_or_luid) - tsr = etree.Element('tsRequest') - t = etree.Element('task') - er = etree.Element('extractRefresh') - d = etree.Element('datasource') + tsr = ET.Element('tsRequest') + t = ET.Element('task') + er = ET.Element('extractRefresh') + d = ET.Element('datasource') d.set('id', ds_luid) er.append(d) t.append(er)