Skip to content

Commit

Permalink
Lots of README material updated
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryant Howell committed Nov 13, 2019
1 parent dea1dac commit 57ebed6
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 36 deletions.
54 changes: 50 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion tableau_rest_api/methods/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
62 changes: 31 additions & 31 deletions tableau_rest_api/methods/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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(
Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit 57ebed6

Please sign in to comment.