Skip to content

Commit

Permalink
Significant update to logger
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryant Howell committed Dec 12, 2019
1 parent 34b35cc commit 2621913
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 147 deletions.
49 changes: 36 additions & 13 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 @@ -41,7 +54,9 @@ def start_log_block(self):
cur_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()).encode('utf-8')

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

def end_log_block(self):
Expand All @@ -51,22 +66,30 @@ def end_log_block(self):
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
if self._log_modes['debug'] is True:
self.log_depth -= 2
log_line = '{}^^-- {} : {} {} ended --------^^\n'.format(" "*self.log_depth, 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)
20 changes: 11 additions & 9 deletions tableau_rest_api/methods/_lookups.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#from tableau_rest_api.methods.rest_api_base import *
from typing import Union, Optional
from ...tableau_rest_xml import TableauRestXml
from ...tableau_exceptions import *
# These find LUIDs from real names or other aspects. They get added to the RestApiBase class because methods on
# almost any different object might need a LUID from any of the others
class LookupMethods():
Expand Down Expand Up @@ -35,7 +37,7 @@ def query_datasource_luid(self, datasource_name: str, project_name_or_luid: Opti
# Search for ContentUrl which should be unique, return
if content_url is not None:
datasources_with_content_url = datasources_with_name.findall(
'.//t:datasource[@contentUrl="{}"]'.format(content_url), self.ns_map)
'.//t:datasource[@contentUrl="{}"]'.format(content_url), TableauRestXml.ns_map)
self.end_log_block()
if len(datasources_with_name) == 1:
return datasources_with_content_url[0].get("id")
Expand All @@ -58,11 +60,11 @@ def query_datasource_luid(self, datasource_name: str, project_name_or_luid: Opti
else:
if self.is_luid(project_name_or_luid):
ds_in_proj = datasources_with_name.findall('.//t:project[@id="{}"]/..'.format(project_name_or_luid),
self.ns_map)
TableauRestXml.ns_map)
else:
ds_in_proj = datasources_with_name.findall(
'.//t:project[@name="{}"]/..'.format(project_name_or_luid),
self.ns_map)
TableauRestXml.ns_map)
if len(ds_in_proj) == 1:
self.end_log_block()
return ds_in_proj[0].get("id")
Expand Down Expand Up @@ -123,9 +125,9 @@ def query_workbook_view_luid(self, wb_name_or_luid: str, view_name: Optional[str
wb_luid = self.query_workbook_luid(wb_name_or_luid, proj_name_or_luid)
vws = self.query_resource("workbooks/{}/views?includeUsageStatistics={}".format(wb_luid, str(usage).lower()))
if view_content_url is not None:
views_with_name = vws.findall('.//t:view[@contentUrl="{}"]'.format(view_content_url), self.ns_map)
views_with_name = vws.findall('.//t:view[@contentUrl="{}"]'.format(view_content_url), TableauRestXml.ns_map)
else:
views_with_name = vws.findall('.//t:view[@name="{}"]'.format(view_name), self.ns_map)
views_with_name = vws.findall('.//t:view[@name="{}"]'.format(view_name), TableauRestXml.ns_map)
if len(views_with_name) == 0:
self.end_log_block()
raise NoMatchFoundException('No view found with name {} or content_url {} in workbook {}'.format(view_name, view_content_url, wb_name_or_luid))
Expand Down Expand Up @@ -153,11 +155,11 @@ def query_workbook_luid(self, wb_name: str, proj_name_or_luid: Optional[str] = N
elif len(workbooks_with_name) > 1 and proj_name_or_luid is not None:
if self.is_luid(proj_name_or_luid):
wb_in_proj = workbooks_with_name.findall('.//t:project[@id="{}"]/..'.format(proj_name_or_luid),
self.ns_map)
TableauRestXml.ns_map)
else:
wb_in_proj = workbooks_with_name.findall(
'.//t:project[@name="{}"]/..'.format(proj_name_or_luid),
self.ns_map)
TableauRestXml.ns_map)
if len(wb_in_proj) == 0:
self.end_log_block()
raise NoMatchFoundException('No workbook found with name {} in project {}'.format(wb_name, proj_name_or_luid))
Expand All @@ -174,7 +176,7 @@ def query_database_luid(self, database_name: str) -> str:
if self.is_luid(database_name):
return database_name
databases = self.query_resource("databases")
databases_with_name = databases.findall('.//t:database[@name="{}"]'.format(database_name), self.ns_map)
databases_with_name = databases.findall('.//t:database[@name="{}"]'.format(database_name), TableauRestXml.ns_map)
if len(databases_with_name) == 0:
self.end_log_block()
raise NoMatchFoundException(
Expand All @@ -194,7 +196,7 @@ def query_table_luid(self, table_name: str) -> str:
if self.is_luid(table_name):
return table_name
tables = self.query_resource("tables")
tables_with_name = tables.findall('.//t:table[@name="{}"]'.format(table_name), self.ns_map)
tables_with_name = tables.findall('.//t:table[@name="{}"]'.format(table_name), TableauRestXml.ns_map)
if len(tables_with_name) == 0:
self.end_log_block()
raise NoMatchFoundException(
Expand Down
5 changes: 3 additions & 2 deletions tableau_rest_api/methods/datasource.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .rest_api_base import *
from ..published_content import Datasource, Datasource28
from ...tableau_rest_xml import TableauRestXml

class DatasourceMethods():
def __init__(self, rest_api_base: TableauRestApiBase):
Expand Down Expand Up @@ -34,7 +35,7 @@ def query_datasources(self, project_name_or_luid: Optional[str] = None, all_fiel
# If there is a project filter
if project_name_or_luid is not None:
project_luid = self.query_project_luid(project_name_or_luid)
dses_in_project = datasources.findall('.//t:project[@id="{}"]/..'.format(project_luid), self.ns_map)
dses_in_project = datasources.findall('.//t:project[@id="{}"]/..'.format(project_luid), TableauRestXml.ns_map)
dses = ET.Element(self.ns_prefix + 'datasources')
for ds in dses_in_project:
dses.append(ds)
Expand Down Expand Up @@ -181,7 +182,7 @@ def publish_datasource(self, ds_filename: str, ds_name: str, project_obj: Projec
project_luid = project_obj.luid
xml = self._publish_content('datasource', ds_filename, ds_name, project_luid, {"overwrite": overwrite},
connection_username, connection_password, save_credentials, oauth_flag=oauth_flag)
datasource = xml.findall('.//t:datasource', self.ns_map)
datasource = xml.findall('.//t:datasource', TableauRestXml.ns_map)
return datasource[0].get('id')

#
Expand Down
104 changes: 2 additions & 102 deletions tableau_rest_api/methods/rest_api_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from tableau_tools.tableau_rest_api.published_content import Project, Project28, Project33, Workbook, Datasource, Flow33
from tableau_tools.tableau_rest_api.url_filter import *
from tableau_tools.tableau_rest_api.sort import *
from ...tableau_rest_xml import TableauRestXml


class TableauRestApiBase(LookupMethods, LoggingMethods):
class TableauRestApiBase(LookupMethods, LoggingMethods, TableauRestXml):
# Defines a class that represents a RESTful connection to Tableau Server. Use full URL (http:// or https://)
def __init__(self, server: str, username: str, password: str, site_content_url: Optional[str] = ""):
if server.find('http') == -1:
Expand Down Expand Up @@ -82,83 +82,7 @@ def __init__(self, server: str, username: str, password: str, site_content_url:
u'SiteAdministratorCreator'
)

# URI is different form actual URL you need to load a particular view in iframe
@staticmethod
def convert_view_content_url_to_embed_url(content_url: str) -> str:
split_url = content_url.split('/')
return 'views/{}/{}'.format(split_url[0], split_url[2])

# 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

def set_tableau_server_version(self, tableau_server_version: str) -> str:
if str(tableau_server_version)in ["10.3", "10.4", "10.5", '2018.1', '2018.2', '2018.3', '2019.1',
Expand Down Expand Up @@ -212,25 +136,7 @@ def read_file_in_chunks(file_object, chunk_size=(1024 * 1024 * 10)):
break
yield data

# You must generate a boundary string that is used both in the headers and the generated request that you post.
# This builds a simple 30 hex digit string
@staticmethod
def generate_boundary_string() -> str:
random_digits = [random.SystemRandom().choice('0123456789abcdef') for n in range(30)]
s = "".join(random_digits)
return s

# 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

@property
def token(self) -> str:
Expand Down Expand Up @@ -347,12 +253,6 @@ def __build_connection_update_xml(new_server_address: Optional[str] = None,
tsr.append(c)
return tsr

#
# Factory methods for PublishedContent and Permissions objects
#



#
# Sign-in and Sign-out
#
Expand Down
Loading

0 comments on commit 2621913

Please sign in to comment.