Skip to content

Commit

Permalink
Merge pull request #79 from bryantbhowell/5.1.0
Browse files Browse the repository at this point in the history
5.1.0
  • Loading branch information
Bryant Howell authored Jan 6, 2020
2 parents 875ddf1 + 3dfde0e commit 1bd2dfe
Show file tree
Hide file tree
Showing 20 changed files with 641 additions and 518 deletions.
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,26 @@ If you want to log something in your script into this log, you can call

where l is a string. You do not need to add a "\n", it will be added automatically.

The Logger class by default only logs Requests but not Responses. If you need to see the full responses, use the following method:
The Logger class, starting in tableau_tools 5.1, has multiple options to show different levels of response.

`Logger.enable_debug_level()`b
By default, the Logger will only show the HTTP requests with URI, along the chain of nested methods used to perform the actions.

`Logger.enable_request_logging()`

### 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.
will display the string version of the XML requests sent along with the HTTP requests.

It should never be necessary to use TableauBase by itself.
`Logger.enable_response_logging()`

will display the string version of all XML responses in the logs. This is far more verbose, so is only suggested when you are encountering errors based on expectations of what should be in the response.

`Logger.enable_debug_level()`

makes the Logger indent the lines of the log, so that you can see the nesting of the actions that happen more easily. This is what the logs looked like in previous version of tableau_tools, but now it must be turned on if you want that mode.

### 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.

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
Expand Down Expand Up @@ -912,12 +923,18 @@ For Projects, since the standard `query_project()` method returns the Project ob

Projects have additional commands that the other classes do not:

`Project.lock_permissions()`
`Project.lock_permissions() -> Project`

`Project.unlock_permission()`
`Project.unlock_permission() -> Project`

`Project.are_permissions_locked()`

If you are locking or unlocking permissions, you should replace the project object you used with the response that comes back:

proj = t.projects.query_project('My Project')
proj = proj.lock_permissions() # You want to updated object returned here to use from here on out
...

You access the default permissions objects with the following, which reference the objects of the correct type that have already been built within the Project object:

`Project.workbook_defaults`
Expand Down
24 changes: 12 additions & 12 deletions examples/create_site_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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, ])

Expand Down
1 change: 0 additions & 1 deletion examples/test_suite_all_querying_tableau_server_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions examples/user_sync_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 47 additions & 17 deletions logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import xml.etree.ElementTree as ET
from typing import Union, Any, Optional, List, Dict, Tuple

# Logger has several modes
# Default just shows REST URL requests
# If you "enable_request_xml_logging", then it will show the full XML of the request
# If you "enable_debugging mode", then the log will indent to show which calls are wrapped within another
# "enabled_response_logging" will log the response
class Logger(object):
def __init__(self, filename):
self._log_level = 'standard'
Expand All @@ -13,9 +18,17 @@ def __init__(self, filename):
except IOError:
print("Error: File '{}' cannot be opened to write for logging".format(filename))
raise
self._log_modes = {'debug': False, 'response': False, 'request': False}

def enable_debug_level(self):
self._log_level = 'debug'
self._log_modes['debug'] = True

def enable_request_logging(self):
self._log_modes['request'] = True

def enable_response_logging(self):
self._log_modes['response'] = True

def log(self, l: str):
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
Expand All @@ -29,7 +42,7 @@ def log(self, l: str):
self.__log_handle.write(log_line)

def log_debug(self, l: str):
if self._log_level == 'debug':
if self._log_modes['debug'] is True:
self.log(l)

def start_log_block(self):
Expand All @@ -38,10 +51,17 @@ def start_log_block(self):
class_path = c.split('.')
short_class = class_path[len(class_path)-1]
short_class = short_class[:-2]
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()).encode('utf-8')
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())


# Only move the log depth in debug mode
if self._log_modes['debug'] is True:
self.log_depth += 2
log_line = '{}vv-- {} : {} {} started --------vv\n'.format(" " * self.log_depth, str(cur_time), short_class,
caller_function_name)
else:
log_line = '{} : {} {} started --------vv\n'.format(str(cur_time), short_class, caller_function_name)

log_line = '{}vv-- {} : {} {} started --------vv\n'.format(" "*self.log_depth, cur_time, short_class, caller_function_name)
self.log_depth += 2
self.__log_handle.write(log_line.encode('utf-8'))

def end_log_block(self):
Expand All @@ -50,23 +70,33 @@ def end_log_block(self):
class_path = c.split('.')
short_class = class_path[len(class_path)-1]
short_class = short_class[:-2]
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()).encode('utf-8')
self.log_depth -= 2
log_line = '{}^^-- {} : {} {} ended --------^^\n'.format(" "*self.log_depth, cur_time, short_class, caller_function_name)
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
if self._log_modes['debug'] is True:
self.log_depth -= 2
log_line = '{}^^-- {} : {} {} ended --------^^\n'.format(" "*self.log_depth, str(cur_time), short_class, caller_function_name)
else:
log_line = '{} : {} {} ended --------^^\n'.format(str(cur_time), short_class, caller_function_name)

self.__log_handle.write(log_line.encode('utf-8'))

def log_uri(self, uri: str, verb: str):
self.log('Sending {} request via: \n{}'.format(verb, uri))
self.log('[{}] {}'.format(verb.upper(), uri))

def log_xml_request(self, xml: ET.Element, verb: str):
if isinstance(xml, str):
self.log('Sending {} request with XML: \n{}'.format(verb, xml))
def log_xml_request(self, xml: Union[ET.Element, str], verb: str, uri: str):
if self._log_modes['request'] is True:
if isinstance(xml, str):
self.log('[{}] \n{}'.format(verb.upper(), xml))
else:
self.log('[{}] \n{}'.format(verb.upper(), ET.tostring(xml)))
else:
self.log('Sending {} request with XML: \n{}'.format(verb, ET.tostring(xml)))
self.log('[{}] {}'.format(verb.upper(), uri))

def log_xml_response(self, xml: ET.Element):
if isinstance(xml, str):
self.log('Received response with XML: \n{}'.format(xml))
else:
self.log('Received response with XML: \n{}'.format(ET.tostring(xml)))
def log_xml_response(self, xml: Union[str, ET.Element]):
if self._log_modes['response'] is True:
if isinstance(xml, str):
self.log('[XML Response] \n{}'.format(xml))
else:
self.log('[XML Response] \n{}'.format(ET.tostring(xml)))

def log_error(self, error_text: str):
self.log('[ERROR] {}'.format(error_text))
18 changes: 13 additions & 5 deletions logging_methods.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional, List, Dict
from typing import Optional, List, Dict, Union
import xml.etree.ElementTree as ET

from tableau_tools.logger import Logger
from .logger import Logger

class LoggingMethods:
# Logging Methods
Expand All @@ -26,8 +26,16 @@ def end_log_block(self):

def log_uri(self, uri: str, verb: str):
if self.logger is not None:
self.logger.log_uri(verb, uri)
self.logger.log_uri(uri=uri, verb=verb)

def log_xml_request(self, xml: ET.Element, verb: str):
def log_xml_request(self, xml: ET.Element, verb: str, uri: str):
if self.logger is not None:
self.logger.log_xml_request(verb, xml)
self.logger.log_xml_request(xml=xml, verb=verb, uri=uri)

def log_xml_response(self, xml: Union[str, ET.Element]):
if self.logger is not None:
self.logger.log_xml_response(xml=xml)

def log_error(self, error_text: str):
if self.logger is not None:
self.logger.log_error(error_text=error_text)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
setup(
name='tableau_tools',
python_requires='>=3.6',
version='5.0.7',
version='5.1.0',
packages=['tableau_tools', 'tableau_tools.tableau_rest_api', 'tableau_tools.tableau_documents',
'tableau_tools.examples', 'tableau_tools.tableau_rest_api.methods'],
url='https://github.com/bryantbhowell/tableau_tools',
Expand Down
12 changes: 7 additions & 5 deletions tableau_documents/table_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import random
from xml.sax.saxutils import quoteattr, unescape
import copy
import datetime

from tableau_tools.tableau_exceptions import *
from ..tableau_exceptions import *
from ..tableau_rest_xml import TableauRestXml

# This represents the classic Tableau data connection window relations
# Allows for changes in JOINs, Stored Proc values, and Custom SQL
Expand All @@ -17,8 +19,8 @@ def __init__(self, relation_xml_obj: ET.Element):
self.main_table: ET.Element
self.table_relations: List[ET.Element]
self.join_relations = []
self.ns_map = {"user": 'http://www.tableausoftware.com/xml/user', 't': 'http://tableau.com/api'}
ET.register_namespace('t', self.ns_map['t'])
#self.ns_map = {"user": 'http://www.tableausoftware.com/xml/user', 't': 'http://tableau.com/api'}
#ET.register_namespace('t', self.ns_map['t'])
self._read_existing_relations()

def _read_existing_relations(self):
Expand All @@ -29,7 +31,7 @@ def _read_existing_relations(self):
self.table_relations = [self.relation_xml_obj, ]

else:
table_relations = self.relation_xml_obj.findall('.//relation', self.ns_map)
table_relations = self.relation_xml_obj.findall('.//relation', TableauRestXml.ns_map)
final_table_relations = []
# ElementTree doesn't implement the != operator, so have to find all then iterate through to exclude
# the JOINs to only get the tables, stored-procs and Custom SQLs
Expand Down Expand Up @@ -118,7 +120,7 @@ def set_stored_proc_parameter_value_by_name(self, parameter_name: str, parameter
if self._stored_proc_parameters_xml is None:
self._stored_proc_parameters_xml = ET.Element('actual-parameters')
# Find parameter with that name (if exists)
param = self._stored_proc_parameters_xml.find('.//column[@name="{}"]'.format(parameter_name), self.ns_map)
param = self._stored_proc_parameters_xml.find('.//column[@name="{}"]'.format(parameter_name), TableauRestXml.ns_map)

if param is None:
# create_stored... already converts to correct quoting
Expand Down
Loading

0 comments on commit 1bd2dfe

Please sign in to comment.