From 13a1963a378764ea4c155abd587c8896bc08c897 Mon Sep 17 00:00:00 2001 From: Bhuvnesh Singh Date: Wed, 4 Jan 2023 01:46:03 -0800 Subject: [PATCH 01/14] remove lowercase while parsing csv --- tabcmd/commands/user/user_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index cab01427..b9012078 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -206,7 +206,7 @@ def get_users_from_file(csv_file: io.TextIOWrapper, logger=None) -> List[TSC.Use def _parse_line(line: str) -> Optional[TSC.UserItem]: if line is None or line is False or line == "\n" or line == "": return None - line = line.strip().lower() + line = line.strip() line_parts: List[str] = line.split(",") data = Userdata() values: List[str] = list(map(str.strip, line_parts)) From 02edb282ca29fae62b58f847217a838b9b42be80 Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 6 Jan 2023 14:52:55 -0800 Subject: [PATCH 02/14] Jac/user agent (#219) --- tabcmd/commands/auth/session.py | 5 ++++- tabcmd/execution/parent_parser.py | 14 +------------- tabcmd/version.py | 9 +++++++++ tests/e2e/online_tests.py | 6 ------ 4 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 tabcmd/version.py diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 986b2c71..e6583f87 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -7,10 +7,13 @@ import urllib3 from urllib3.exceptions import InsecureRequestWarning +from tabcmd.version import version from tabcmd.commands.constants import Errors from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log +from typing import Dict, Any + class Session: """ @@ -152,7 +155,7 @@ def _set_connection_options(self) -> TSC.Server: # args still to be handled here: # proxy, --no-proxy, # cert - http_options = {} + http_options: Dict[str, Any] = {"headers": {"User-Agent": "Tabcmd/{}".format(version)}} if self.no_certcheck: http_options["verify"] = False urllib3.disable_warnings(category=InsecureRequestWarning) diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 0aa6ee75..21890728 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -1,21 +1,9 @@ import argparse -import logging from .localize import _ from .logger_config import log from .map_of_commands import CommandsMap - - -# when we drop python 3.8, this could be replaced with this lighter weight option -# from importlib.metadata import version, PackageNotFoundError -from pkg_resources import get_distribution, DistributionNotFound - -try: - version = get_distribution("tabcmd").version -except DistributionNotFound: - version = "2.x.unknown" - pass - +from tabcmd.version import version """ Note: output order is influenced first by grouping, then by order they are added in here diff --git a/tabcmd/version.py b/tabcmd/version.py new file mode 100644 index 00000000..e6e2d0f1 --- /dev/null +++ b/tabcmd/version.py @@ -0,0 +1,9 @@ +# when we drop python 3.8, this could be replaced with this lighter weight option +# from importlib.metadata import version, PackageNotFoundError +from pkg_resources import get_distribution, DistributionNotFound + +try: + version = get_distribution("tabcmd").version +except DistributionNotFound: + version = "2.0.0" + pass diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index b73d73b7..117b67a8 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -129,12 +129,6 @@ def _get_datasource(self, server_file): arguments = [command, server_file] _test_command(arguments) - def _get_datasource(self, server_file): - command = "get" - server_file = "/datasources/" + server_file - arguments = [command, server_file] - _test_command(arguments) - def _create_extract(self, wb_name): command = "createextracts" arguments = [command, "-w", wb_name, "--encrypt"] From 6e48d625f67d0a18bcca75a714188f6315de707e Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 1 Feb 2023 12:59:10 -0800 Subject: [PATCH 03/14] Jac/logging (#224) * handle exit with no logger * redact logging token, also clean up exit a little * add logging to check signin failure * remove duplicate log lines * refactor: extract strings to local dictionary to make it easier to extract to another file * add error handling around session file data * add flows and object detail to list output * some test coverage --- tabcmd/commands/auth/login_command.py | 2 +- tabcmd/commands/auth/session.py | 103 +++++++++++------- tabcmd/commands/constants.py | 43 ++++---- .../delete_command.py | 2 +- .../export_command.py | 2 +- .../get_url_command.py | 2 +- .../publish_command.py | 2 +- .../runschedule_command.py | 2 +- .../extracts/create_extracts_command.py | 2 +- .../extracts/decrypt_extracts_command.py | 2 +- .../extracts/delete_extracts_command.py | 2 +- .../extracts/encrypt_extracts_command.py | 2 +- .../extracts/reencrypt_extracts_command.py | 2 +- .../extracts/refresh_extracts_command.py | 2 +- tabcmd/commands/group/create_group_command.py | 2 +- tabcmd/commands/group/delete_group_command.py | 2 +- .../project/create_project_command.py | 2 +- .../project/delete_project_command.py | 2 +- .../project/publish_samples_command.py | 2 +- tabcmd/commands/site/create_site_command.py | 2 +- tabcmd/commands/site/delete_site_command.py | 2 +- tabcmd/commands/site/edit_site_command.py | 2 +- tabcmd/commands/site/list_command.py | 25 ++++- tabcmd/commands/site/list_sites_command.py | 2 +- tabcmd/commands/user/add_users_command.py | 2 +- tabcmd/commands/user/create_site_users.py | 2 +- tabcmd/commands/user/create_users_command.py | 2 +- .../user/delete_site_users_command.py | 2 +- tabcmd/commands/user/remove_users_command.py | 2 +- tabcmd/execution/tabcmd_controller.py | 18 ++- tests/commands/test_run_commands.py | 3 +- tests/commands/test_session.py | 41 +++++-- tests/e2e/tests_integration.py | 4 +- 33 files changed, 171 insertions(+), 118 deletions(-) diff --git a/tabcmd/commands/auth/login_command.py b/tabcmd/commands/auth/login_command.py index 647fee3f..d042f724 100644 --- a/tabcmd/commands/auth/login_command.py +++ b/tabcmd/commands/auth/login_command.py @@ -22,4 +22,4 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - session.create_session(args) + session.create_session(args, logger) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index e6583f87..2e7107ef 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -48,8 +48,8 @@ def __init__(self): self.timeout = None self.logging_level = "info" - self._read_from_json() self.logger = log(__name__, self.logging_level) # instantiate here mostly for tests + self._read_from_json() self.tableau_server = None # this one is an object that doesn't get persisted in the file # called before we connect to the server @@ -206,7 +206,7 @@ def _create_new_connection(self) -> TSC.Server: return self.tableau_server def _read_existing_state(self): - if self._check_json(): + if self._json_exists(): self._read_from_json() def _print_server_info(self): @@ -265,13 +265,13 @@ def _get_saved_credentials(self): return credentials # external entry point: - def create_session(self, args): + def create_session(self, args, logger): signed_in_object = None # pull out cached info from json, then overwrite with new args if available self._read_existing_state() self._update_session_data(args) self.logging_level = args.logging_level or self.logging_level - self.logger = self.logger or log(__class__.__name__, self.logging_level) + self.logger = logger or log(__class__.__name__, self.logging_level) credentials = None if args.password: @@ -344,51 +344,70 @@ def _clear_data(self): self.timeout = None # json file functions ---------------------------------------------------- + # These should be moved into a separate class def _get_file_path(self): home_path = os.path.expanduser("~") file_path = os.path.join(home_path, "tableau_auth.json") return file_path def _read_from_json(self): - if not self._check_json(): + if not self._json_exists(): return file_path = self._get_file_path() - data = {} - with open(str(file_path), "r") as file_contents: - data = json.load(file_contents) + content = None try: - for auth in data["tableau_auth"]: - self.auth_token = auth["auth_token"] - self.server_url = auth["server"] - self.site_name = auth["site_name"] - self.site_id = auth["site_id"] - self.username = auth["username"] - self.user_id = auth["user_id"] - self.token_name = auth["personal_access_token_name"] - self.token_value = auth["personal_access_token"] - self.last_login_using = auth["last_login_using"] - self.password_file = auth["password_file"] - self.no_prompt = auth["no_prompt"] - self.no_certcheck = auth["no_certcheck"] - self.certificate = auth["certificate"] - self.no_proxy = auth["no_proxy"] - self.proxy = auth["proxy"] - self.timeout = auth["timeout"] - except KeyError as e: - self.logger.debug(_("sessionoptions.errors.bad_password_file"), e) - self._remove_json() - except Exception as any_error: - self.logger.info(_("session.new_session")) - self._remove_json() + with open(str(file_path), "r") as file_contents: + data = json.load(file_contents) + content = data["tableau_auth"] + except json.JSONDecodeError as e: + self._wipe_bad_json(e, "Error reading data from session file") + except IOError as e: + self._wipe_bad_json(e, "Error reading session file") + except AttributeError as e: + self._wipe_bad_json(e, "Error parsing session details from file") + except Exception as e: + self._wipe_bad_json(e, "Unexpected error reading session details from file") + + try: + auth = content[0] + self.auth_token = auth["auth_token"] + self.server_url = auth["server"] + self.site_name = auth["site_name"] + self.site_id = auth["site_id"] + self.username = auth["username"] + self.user_id = auth["user_id"] + self.token_name = auth["personal_access_token_name"] + self.token_value = auth["personal_access_token"] + self.last_login_using = auth["last_login_using"] + self.password_file = auth["password_file"] + self.no_prompt = auth["no_prompt"] + self.no_certcheck = auth["no_certcheck"] + self.certificate = auth["certificate"] + self.no_proxy = auth["no_proxy"] + self.proxy = auth["proxy"] + self.timeout = auth["timeout"] + except AttributeError as e: + self._wipe_bad_json(e, "Unrecognized attribute in session file") + except Exception as e: + self._wipe_bad_json(e, "Failed to load session file") - def _check_json(self): + def _wipe_bad_json(self, e, message): + self.logger.debug(message + ": " + e.__str__()) + self.logger.info(_("session.new_session")) + self._remove_json() + + def _json_exists(self): + # todo: make this location configurable home_path = os.path.expanduser("~") file_path = os.path.join(home_path, "tableau_auth.json") return os.path.exists(file_path) def _save_session_to_json(self): - data = self._serialize_for_save() - self._save_file(data) + try: + data = self._serialize_for_save() + self._save_file(data) + except Exception as e: + self._wipe_bad_json(e, "Failed to save session file") def _save_file(self, data): file_path = self._get_file_path() @@ -420,7 +439,15 @@ def _serialize_for_save(self): return data def _remove_json(self): - file_path = self._get_file_path() - self._save_file({}) - if os.path.exists(file_path): - os.remove(file_path) + file_path = "" + try: + if not self._json_exists(): + return + file_path = self._get_file_path() + self._save_file({}) + if os.path.exists(file_path): + os.remove(file_path) + except Exception as e: + message = "Error clearing session data from {}: check and remove manually".format(file_path) + self.logger.error(message) + self.logger.error(e) diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index c37cb259..c491a135 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,8 +1,6 @@ import inspect import sys -from tableauserverclient import ServerResponseError - from tabcmd.execution.localize import _ @@ -25,20 +23,19 @@ def is_expired_session(error): @staticmethod def is_resource_conflict(error): if hasattr(error, "code"): - return error.code == Constants.source_already_exists + return error.code.startswith(Constants.resource_conflict_general) @staticmethod def is_login_error(error): if hasattr(error, "code"): return error.code == Constants.login_error - @staticmethod - def is_server_response_error(error): - return isinstance(error, ServerResponseError) - # https://gist.github.com/FredLoney/5454553 @staticmethod def log_stack(logger): + if not logger: + print("logger not available: cannot show stack") + return try: """The log header message formatter.""" HEADER_FMT = "Printing Call Stack at %s::%s" @@ -49,11 +46,10 @@ def log_stack(logger): file, line, func = here[1:4] start = 0 n_lines = 5 - logger.trace(HEADER_FMT % (file, func)) - - for frame in stack[start + 2 : n_lines]: + logger.debug(HEADER_FMT % (file, func)) + for frame in stack[start + 1 : n_lines]: file, line, func = frame[1:4] - logger.trace(STACK_FMT % (file, line, func)) + logger.debug(STACK_FMT % (file, line, func)) except Exception as e: logger.info("Error printing stack trace:", e) @@ -66,9 +62,9 @@ def exit_with_error(logger, message=None, exception=None): if exception: if message: logger.debug("Error message: " + message) - Errors.check_common_error_codes_and_explain(logger, exception) + Errors.check_common_error_codes_and_explain(logger, exception) except Exception as exc: - print(sys.stderr, "Error during log call from exception - {}".format(exc)) + print(sys.stderr, "Error during log call from exception - {} {}".format(exc.__class__, message)) try: logger.info("Exiting...") except Exception: @@ -77,16 +73,15 @@ def exit_with_error(logger, message=None, exception=None): @staticmethod def check_common_error_codes_and_explain(logger, exception): - if Errors.is_server_response_error(exception): - logger.error(_("publish.errors.unexpected_server_response").format(exception)) - if Errors.is_expired_session(exception): - logger.error(_("session.errors.session_expired")) - # TODO: add session as an argument to this method - # and add the full command line as a field in Session? - # "session.session_expired_login")) - # session.renew_session - return - if exception.code == Constants.source_not_found: - logger.error(_("publish.errors.server_resource_not_found"), exception) + # most errors contain as much info in the message as we can get from the code + # identify any that we can add useful detail for and include them here + if Errors.is_expired_session(exception): + # catch this one so we can attempt to refresh the session before telling them it failed + logger.error(_("session.errors.session_expired")) + # TODO: add session as an argument to this method + # and add the full command line as a field in Session? + # "session.session_expired_login")) + # session.renew_session() + return else: logger.error(exception) diff --git a/tabcmd/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py index e28f5f45..9ac1a9e3 100644 --- a/tabcmd/commands/datasources_and_workbooks/delete_command.py +++ b/tabcmd/commands/datasources_and_workbooks/delete_command.py @@ -32,7 +32,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) content_type: str = "" if args.workbook: content_type = "workbook" diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 6775181a..4dad864b 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -73,7 +73,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) view_content_url, wb_content_url = ExportCommand.parse_export_url_to_workbook_and_view(logger, args.url) logger.debug([view_content_url, wb_content_url]) if not view_content_url and not wb_content_url: diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 83fb158a..440f8c8a 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -41,7 +41,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) if " " in args.url: Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index c4866bbd..ce04fa31 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -36,7 +36,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) if args.project_name: try: diff --git a/tabcmd/commands/datasources_and_workbooks/runschedule_command.py b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py index 9661135b..9634c4e2 100644 --- a/tabcmd/commands/datasources_and_workbooks/runschedule_command.py +++ b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py @@ -23,7 +23,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) schedule = DatasourcesAndWorkbooks.get_items_by_name(logger, server.schedules, args.schedule)[0] logger.info(_("runschedule.status")) Errors.exit_with_error(logger, "Not yet implemented") diff --git a/tabcmd/commands/extracts/create_extracts_command.py b/tabcmd/commands/extracts/create_extracts_command.py index f23be293..4dcf2ba0 100644 --- a/tabcmd/commands/extracts/create_extracts_command.py +++ b/tabcmd/commands/extracts/create_extracts_command.py @@ -31,7 +31,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) creation_call = None try: logger.debug( diff --git a/tabcmd/commands/extracts/decrypt_extracts_command.py b/tabcmd/commands/extracts/decrypt_extracts_command.py index a43d8fa6..cb47d7fa 100644 --- a/tabcmd/commands/extracts/decrypt_extracts_command.py +++ b/tabcmd/commands/extracts/decrypt_extracts_command.py @@ -24,7 +24,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("decryptextracts.status").format(args.site_name)) diff --git a/tabcmd/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py index 62ba3bb0..20eaf5b8 100644 --- a/tabcmd/commands/extracts/delete_extracts_command.py +++ b/tabcmd/commands/extracts/delete_extracts_command.py @@ -31,7 +31,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) try: if args.datasource: logger.info(_("deleteextracts.for.datasource").format(args.datasource)) diff --git a/tabcmd/commands/extracts/encrypt_extracts_command.py b/tabcmd/commands/extracts/encrypt_extracts_command.py index 1b697fd7..0454d0ea 100644 --- a/tabcmd/commands/extracts/encrypt_extracts_command.py +++ b/tabcmd/commands/extracts/encrypt_extracts_command.py @@ -26,7 +26,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("encryptextracts.status").format(site_item.name)) diff --git a/tabcmd/commands/extracts/reencrypt_extracts_command.py b/tabcmd/commands/extracts/reencrypt_extracts_command.py index 4f7b4ef5..e8eb363a 100644 --- a/tabcmd/commands/extracts/reencrypt_extracts_command.py +++ b/tabcmd/commands/extracts/reencrypt_extracts_command.py @@ -26,7 +26,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("reencryptextracts.status").format(site_item.name)) diff --git a/tabcmd/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py index e18020a3..73a467ff 100644 --- a/tabcmd/commands/extracts/refresh_extracts_command.py +++ b/tabcmd/commands/extracts/refresh_extracts_command.py @@ -33,7 +33,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) refresh_action = "refresh" if args.addcalculations or args.removecalculations: diff --git a/tabcmd/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py index 8aeddb84..357fccb5 100644 --- a/tabcmd/commands/group/create_group_command.py +++ b/tabcmd/commands/group/create_group_command.py @@ -25,7 +25,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) try: logger.info(_("creategroup.status").format(args.name)) new_group = TSC.GroupItem(args.name) diff --git a/tabcmd/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py index accf62e4..71af42a5 100644 --- a/tabcmd/commands/group/delete_group_command.py +++ b/tabcmd/commands/group/delete_group_command.py @@ -25,7 +25,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) try: logger.info(_("tabcmd.find.group").format(args.name)) group_id = Server.find_group(logger, server, args.name).id diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index 03a7477d..85853edd 100644 --- a/tabcmd/commands/project/create_project_command.py +++ b/tabcmd/commands/project/create_project_command.py @@ -30,7 +30,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) parent_id = None readable_name = args.project_name if args.parent_project_path: diff --git a/tabcmd/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py index 74b72395..b107406c 100644 --- a/tabcmd/commands/project/delete_project_command.py +++ b/tabcmd/commands/project/delete_project_command.py @@ -27,7 +27,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) if args.parent_project_path: logger.debug("parent path: {}".format(args.parent_project_path)) diff --git a/tabcmd/commands/project/publish_samples_command.py b/tabcmd/commands/project/publish_samples_command.py index 6d8a54c4..046e4491 100644 --- a/tabcmd/commands/project/publish_samples_command.py +++ b/tabcmd/commands/project/publish_samples_command.py @@ -31,7 +31,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) try: project = PublishSamplesCommand.get_project_by_name_and_parent_path( logger, server, args.project_name, args.parent_project_path diff --git a/tabcmd/commands/site/create_site_command.py b/tabcmd/commands/site/create_site_command.py index 84208188..f684ba1f 100644 --- a/tabcmd/commands/site/create_site_command.py +++ b/tabcmd/commands/site/create_site_command.py @@ -27,7 +27,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) admin_mode = "ContentAndUsers" # default: allow site admins to manage users if not args.site_admin_user_management: admin_mode = "ContentOnly" diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 56256c8e..f1892730 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -25,7 +25,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) target_site: TSC.SiteItem = Server.get_site_by_name(logger, server, args.site_name_to_delete) target_site_id = target_site.id logger.debug(strings[3].format(target_site_id, server.site_id)) diff --git a/tabcmd/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py index 2d7e3db4..ab6451ed 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -30,7 +30,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) if args.url: diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index a77eab75..da6b9f76 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -3,7 +3,6 @@ from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors from tabcmd.commands.server import Server -from tabcmd.execution.global_options import * from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log @@ -13,34 +12,48 @@ class ListCommand(Server): Command to return a list of content the user can access """ + # strings to move to string files + local_strings = { + "tabcmd_content_listing": "===== Listing {0} content for user {1}...", + "tabcmd_listing_label_name": "NAME:", + "tabcmd_listing_label_id": "ID:", + } + name: str = "list" description: str = "List content items of a specified type" @staticmethod def define_args(list_parser): args_group = list_parser.add_argument_group(title=ListCommand.name) - args_group.add_argument("content", choices=["projects", "workbooks", "datasources"], help="View content") + args_group.add_argument( + "content", choices=["projects", "workbooks", "datasources", "flows"], help="View content" + ) + args_group.add_argument("-d", "--details", action="store_true", help="Show object details") @staticmethod def run_command(args): logger = log(__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) content_type = args.content try: + logger.info(ListCommand.local_strings.tabcmd_content_listing.format(content_type, session.username)) + if content_type == "projects": items = server.projects.all() elif content_type == "workbooks": items = server.workbooks.all() elif content_type == "datasources": items = server.datasources.all() + elif content_type == "flows": + items = server.flows.all() - logger.info("===== Listing {0} content for user {1}...".format(content_type, session.username)) for item in items: - logger.info("NAME:".rjust(10), item.name) - logger.info("ID:".rjust(10), item.id) + logger.info(ListCommand.local_strings.tabcmd_listing_label_name.rjust(10), item.name) + if args.details: + logger.info(item) except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index 66ad24c7..e077072e 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -26,7 +26,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) try: sites, pagination = server.sites.get() logger.info(_("listsites.status").format(session.username)) diff --git a/tabcmd/commands/user/add_users_command.py b/tabcmd/commands/user/add_users_command.py index 801f0253..1fc79b46 100644 --- a/tabcmd/commands/user/add_users_command.py +++ b/tabcmd/commands/user/add_users_command.py @@ -25,7 +25,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) logger.info(_("tabcmd.add.users.to_site").format(args.users.name, args.name)) diff --git a/tabcmd/commands/user/create_site_users.py b/tabcmd/commands/user/create_site_users.py index 7a01af21..ce515fec 100644 --- a/tabcmd/commands/user/create_site_users.py +++ b/tabcmd/commands/user/create_site_users.py @@ -31,7 +31,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) number_of_users_listed = 0 number_of_users_added = 0 number_of_errors = 0 diff --git a/tabcmd/commands/user/create_users_command.py b/tabcmd/commands/user/create_users_command.py index 71468f39..1c3ab4fa 100644 --- a/tabcmd/commands/user/create_users_command.py +++ b/tabcmd/commands/user/create_users_command.py @@ -31,7 +31,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) number_of_users_listed = 0 number_of_users_added = 0 number_of_errors = 0 diff --git a/tabcmd/commands/user/delete_site_users_command.py b/tabcmd/commands/user/delete_site_users_command.py index 42f088fd..731ed7bf 100644 --- a/tabcmd/commands/user/delete_site_users_command.py +++ b/tabcmd/commands/user/delete_site_users_command.py @@ -28,7 +28,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) logger.info(_("deleteusers.status").format(args.filename.name)) diff --git a/tabcmd/commands/user/remove_users_command.py b/tabcmd/commands/user/remove_users_command.py index 722ce3d5..38f0f720 100644 --- a/tabcmd/commands/user/remove_users_command.py +++ b/tabcmd/commands/user/remove_users_command.py @@ -25,7 +25,7 @@ def run_command(args): logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() - server = session.create_session(args) + server = session.create_session(args, logger) logger.info(_("tabcmd.delete.users.from_server").format(args.users.name, args.name)) diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 1f7f0fdc..3798b30c 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -23,29 +23,27 @@ def run(parser, user_input=None): sys.exit(0) user_input = user_input or sys.argv[1:] namespace = parser.parse_args(user_input) - if namespace.logging_level and namespace.logging_level != logging.INFO: + if hasattr("namespace", "logging_level") and namespace.logging_level != logging.INFO: print("logging:", namespace.logging_level) logger = log(__name__, namespace.logging_level or logging.INFO) - if namespace.password or namespace.token_value: - logger.trace(namespace.func) + if (hasattr("namespace", "password") or hasattr("namespace", "token_value")) and hasattr("namespace", "func"): + # don't print whole namespace because it has secrets + logger.debug(namespace.func) else: - logger.trace(namespace) + logger.debug(namespace) if namespace.language: set_client_locale(namespace.language, logger) - try: - command_name = namespace.func - except AttributeError as aer: - # if no command was given, argparse will just not create the attribute - parser.print_help() - sys.exit(2) try: # if a subcommand was identified, call the function assigned to it # this is the functional equivalent of the call by reflection in the previous structure # https://stackoverflow.com/questions/49038616/argparse-subparsers-with-functions namespace.func.run_command(namespace) except Exception as e: + # todo: use log_stack here for better presentation logger.exception(e) + # if no command was given, argparse will just not create the attribute + sys.exit(2) return namespace diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 774878b6..43004ba5 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -76,7 +76,8 @@ def _set_up_session(mock_session, mock_server): def test_login(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) login_command.LoginCommand.run_command(mock_args) - mock_session.assert_called_with(mock_args) + + mock_session.assert_called_with(mock_args, ANY) @patch("tabcmd.commands.auth.session.Session.end_session_and_clear_data") def test_logout(self, mock_end_session, mock_create_session, mock_server): diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index fba90fd8..5f4c8297 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -68,9 +68,9 @@ def _set_mocks_for_json_file_exists(mock_path, does_it_exist=True): return mock_path -def _set_mocks_for_creds_file(mock_file): - mock_file.readlines.return_value = "dummypassword" - return mock_file +def _set_mock_file_content(mock_load, expected_content): + mock_load.return_value = expected_content + return mock_load @mock.patch("json.dump") @@ -96,6 +96,25 @@ def test_save_session_to_json(self, mock_open, mock_path, mock_load, mock_dump): test_session._save_session_to_json() assert mock_dump.was_called() + def clear_session(self, mock_open, mock_path, mock_load, mock_dump): + _set_mocks_for_json_file_exists(mock_path) + test_session = Session() + test_session.username = "USN" + test_session.server = "SRVR" + test_session._clear_data() + assert test_session.username is None + assert test_session.server is None + + def test_json_not_present(self, mock_open, mock_path, mock_load, mock_dump): + _set_mocks_for_json_file_exists(mock_path, False) + assert mock_open.was_not_called() + + def test_json_invalid(self, mock_open, mock_path, mock_load, mock_dump): + _set_mocks_for_json_file_exists(mock_path) + _set_mock_file_content(mock_load, "just a string") + test_session = Session() + assert test_session.username is None + @mock.patch("getpass.getpass") class BuildCredentialsTests(unittest.TestCase): @@ -212,7 +231,7 @@ def test_create_session_first_time_no_args( mock_tsc().users.get_by_id.return_value = None new_session = Session() with self.assertRaises(SystemExit): - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) @mock.patch("tableauserverclient.Server") def test_create_session_first_time_with_token_arg( @@ -224,7 +243,7 @@ def test_create_session_first_time_with_token_arg( test_args.token_name = "tn" test_args.token_value = "foo" new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token assert auth.auth_token.name is not None, auth.auth_token @@ -241,7 +260,7 @@ def test_create_session_first_time_with_password_arg( test_args.password = "pppp" new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token assert new_session.username == "uuuu", new_session @@ -259,7 +278,7 @@ def test_create_session_first_time_with_password_file_as_password( test_args.password_file = "filename" with mock.patch("builtins.open", mock.mock_open(read_data="my_password")): new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token @@ -277,7 +296,7 @@ def test_create_session_first_time_with_password_file_as_token( test_args.password_file = "filename" with mock.patch("builtins.open", mock.mock_open(read_data="my_token")): new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token @@ -309,7 +328,7 @@ def test_create_session_with_active_session_saved( test_args.no_prompt = False new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token assert mock_tsc.has_been_called() @@ -326,7 +345,7 @@ def test_create_session_with_saved_expired_username_session( mock_pass.getpass.return_value = "success" test_args.password = "eqweqwe" new_session = Session() - auth = new_session.create_session(test_args) + auth = new_session.create_session(test_args, None) assert mock_pass.has_been_called() assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token @@ -379,7 +398,7 @@ def test_connection_times_out(self): test_args.server = "https://nothere.com" with self.assertRaises(SystemExit): - new_session.create_session(test_args) + new_session.create_session(test_args, None) # should test connection doesn't time out? diff --git a/tests/e2e/tests_integration.py b/tests/e2e/tests_integration.py index e18c38b6..f63588b1 100644 --- a/tests/e2e/tests_integration.py +++ b/tests/e2e/tests_integration.py @@ -65,7 +65,7 @@ def test_log_in(): no_cookie=False, ) test_session = Session() - server = test_session.create_session(args) + server = test_session.create_session(args, logger) assert test_session.auth_token is not None assert test_session.site_id is not None assert test_session.user_id is not None @@ -96,7 +96,7 @@ def test_reuse_session(self): no_cookie=False, ) test_session = Session() - test_session.create_session(args) + test_session.create_session(args, logger) assert test_session.auth_token is not None assert test_session.site_id is not None assert test_session.user_id is not None From c261d76e26cef78769df1166db0018d0fa37ba62 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 2 Feb 2023 11:58:22 -0800 Subject: [PATCH 04/14] Jac/filter spaces (#225) * encoding filter values to handle spaces and special chars in filters * added new --filter option to pass in un-encoded value for simpler input * refactoring order of args, method names * stopped sending filters for workbooks --- .../datasources_and_workbooks_command.py | 52 +++++++---- .../export_command.py | 40 ++++---- .../get_url_command.py | 6 +- .../test_datasources_and_workbooks_command.py | 93 +++++++++++++++++++ tests/commands/test_geturl_utils.py | 10 +- 5 files changed, 152 insertions(+), 49 deletions(-) create mode 100644 tests/commands/test_datasources_and_workbooks_command.py diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 1d345a12..1a2169f5 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -1,3 +1,5 @@ +import urllib + import tableauserverclient as TSC from tabcmd.commands.constants import Errors @@ -23,7 +25,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url)) - logger.trace(req_option.get_query_params()) + logger.debug(req_option.get_query_params()) matching_views, paging = server.views.get(req_option) except Exception as e: Errors.exit_with_error(logger, e) @@ -37,7 +39,7 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, workbook_content_url)) - logger.trace(req_option.get_query_params()) + logger.debug(req_option.get_query_params()) matching_workbooks, paging = server.workbooks.get(req_option) except Exception as e: Errors.exit_with_error(logger, e) @@ -51,7 +53,7 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) - logger.trace(req_option.get_query_params()) + logger.debug(req_option.get_query_params()) matching_datasources, paging = server.datasources.get(req_option) except Exception as e: Errors.exit_with_error(logger, e) @@ -60,39 +62,52 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou return matching_datasources[0] @staticmethod - def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger) -> None: - # should be able to replace this with request_options._append_view_filters(params) + def apply_values_from_url_params(logger, request_options: TSC.PDFRequestOptions, url) -> None: logger.debug(url) try: if "?" in url: query = url.split("?")[1] - logger.trace("Query parameters: {}".format(query)) + logger.debug("Query parameters: {}".format(query)) else: logger.debug("No query parameters present in url") return params = query.split("&") - logger.trace(params) + logger.debug(params) for value in params: if value.startswith(":"): - DatasourcesAndWorkbooks.apply_option_value(request_options, value, logger) + DatasourcesAndWorkbooks.apply_options_in_url(logger, request_options, value) else: # it must be a filter - DatasourcesAndWorkbooks.apply_filter_value(request_options, value, logger) + DatasourcesAndWorkbooks.apply_encoded_filter_value(logger, request_options, value) except Exception as e: logger.warn("Error building filter params", e) # ExportCommand.log_stack(logger) # type: ignore + # this is called from within from_url_params, for each view_filter value + @staticmethod + def apply_encoded_filter_value(logger, request_options, value): + # the REST API doesn't appear to have the option to disambiguate with "Parameters." + value = value.replace("Parameters.", "") + # the filter values received from the url are already url encoded. tsc will encode them again. + # so we run url.decode, which will be a no-op if they are not encoded. + decoded_value = urllib.parse.unquote(value) + logger.debug("url had `{0}`, saved as `{1}`".format(value, decoded_value)) + DatasourcesAndWorkbooks.apply_filter_value(logger, request_options, decoded_value) + + # this is called for each filter value, + # from apply_options, which expects an un-encoded input, + # or from apply_url_params via apply_encoded_filter_value which decodes the input @staticmethod - def apply_filter_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None: - # todo: do we need to strip Parameters.x -> x? - logger.trace("handling filter param {}".format(value)) + def apply_filter_value(logger, request_options: TSC.PDFRequestOptions, value: str) -> None: + logger.debug("handling filter param {}".format(value)) data_filter = value.split("=") request_options.vf(data_filter[0], data_filter[1]) + # this is called from within from_url_params, for each param value @staticmethod - def apply_option_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None: - logger.trace("handling url option {}".format(value)) + def apply_options_in_url(logger, request_options: TSC.PDFRequestOptions, value: str) -> None: + logger.debug("handling url option {}".format(value)) setting = value.split("=") if ":iid" == setting[0]: logger.debug(":iid value ignored in url") @@ -111,19 +126,18 @@ def is_truthy(value: str): return value.lower() in ["yes", "y", "1", "true"] @staticmethod - def apply_png_options(request_options: TSC.ImageRequestOptions, args, logger): + def apply_png_options(logger, request_options: TSC.ImageRequestOptions, args): if args.height or args.width: - # only applicable for png logger.warn("Height/width arguments not yet implemented in export") # Always request high-res images request_options.image_resolution = "high" @staticmethod - def apply_pdf_options(request_options: TSC.PDFRequestOptions, args, logger): - request_options.page_type = args.pagesize + def apply_pdf_options(logger, request_options: TSC.PDFRequestOptions, args): if args.pagelayout: - logger.debug("Setting page layout to: {}".format(args.pagelayout)) request_options.orientation = args.pagelayout + if args.pagesize: + request_options.page_type = args.pagesize @staticmethod def save_to_data_file(logger, output, filename): diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 4dad864b..970c4b9f 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -59,8 +59,8 @@ def define_args(export_parser): group.add_argument("--height", default=600, help=_("export.options.height")) group.add_argument( "--filter", - metavar="COLUMN:VALUE", - help="View filter to apply to the view", + metavar="COLUMN=VALUE", + help="Data filter to apply to the view", ) """ @@ -104,9 +104,10 @@ def run_command(args): default_filename = "{}.png".format(view_item.name) - except Exception as e: + except TSC.ServerResponseException as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(""), e) - + except Exception as e: + Errors.exit_with_error(logger, exception=e) try: save_name = args.filename or default_filename if args.csv: @@ -118,25 +119,19 @@ def run_command(args): Errors.exit_with_error(logger, "Error saving to file", e) @staticmethod - def apply_values_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: - logger.debug( - "Args: {}, {}, {}, {}, {}".format(args.pagelayout, args.pagesize, args.width, args.height, args.filter) - ) - if args.pagelayout: - request_options.orientation = args.pagelayout - if args.pagesize: - request_options.page_type = args.pagesize + def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: if args.filter: params = args.filter.split("&") for value in params: - ExportCommand.apply_filter_value(request_options, value, logger) + ExportCommand.apply_filter_value(logger, request_options, value) + # filtering isn't actually implemented for workbooks in REST @staticmethod def download_wb_pdf(server, workbook_item, args, logger): logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) - ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) - ExportCommand.apply_values_from_args(pdf_options, args, logger) + ExportCommand.apply_values_from_url_params(logger, pdf_options, args.url) + ExportCommand.apply_pdf_options(logger, pdf_options, args) logger.debug(pdf_options.get_query_params()) server.workbooks.populate_pdf(workbook_item, pdf_options) return workbook_item.pdf @@ -145,8 +140,9 @@ def download_wb_pdf(server, workbook_item, args, logger): def download_view_pdf(server, view_item, args, logger): logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) - ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) - ExportCommand.apply_values_from_args(pdf_options, args, logger) + ExportCommand.apply_values_from_url_params(logger, pdf_options, args.url) + ExportCommand.apply_filters_from_args(pdf_options, args, logger) + ExportCommand.apply_pdf_options(logger, pdf_options, args) logger.debug(pdf_options.get_query_params()) server.views.populate_pdf(view_item, pdf_options) return view_item.pdf @@ -155,8 +151,8 @@ def download_view_pdf(server, view_item, args, logger): def download_csv(server, view_item, args, logger): logger.debug(args.url) csv_options = TSC.CSVRequestOptions(maxage=1) - ExportCommand.apply_values_from_url_params(csv_options, args.url, logger) - ExportCommand.apply_values_from_args(csv_options, args, logger) + ExportCommand.apply_values_from_url_params(logger, csv_options, args.url) + ExportCommand.apply_filters_from_args(csv_options, args, logger) logger.debug(csv_options.get_query_params()) server.views.populate_csv(view_item, csv_options) return view_item.csv @@ -165,9 +161,9 @@ def download_csv(server, view_item, args, logger): def download_png(server, view_item, args, logger): logger.debug(args.url) image_options = TSC.ImageRequestOptions(maxage=1) - ExportCommand.apply_values_from_url_params(image_options, args.url, logger) - ExportCommand.apply_values_from_args(image_options, args, logger) - DatasourcesAndWorkbooks.apply_png_options(image_options, args, logger) + ExportCommand.apply_values_from_url_params(logger, image_options, args.url) + ExportCommand.apply_filters_from_args(image_options, args, logger) + DatasourcesAndWorkbooks.apply_png_options(logger, image_options, args) logger.debug(image_options.get_query_params()) server.views.populate_image(view_item, image_options) return view_item.image diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 440f8c8a..ae7d3a54 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -176,7 +176,7 @@ def generate_pdf(logger, server, args, view_url): view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) req_option_pdf = TSC.PDFRequestOptions(maxage=1) - DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_pdf, args.url, logger) + DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_pdf, args.url) server.views.populate_pdf(view_item, req_option_pdf) filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf") DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename) @@ -190,7 +190,7 @@ def generate_png(logger, server, args, view_url): view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) req_option_csv = TSC.ImageRequestOptions(maxage=1) - DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_csv, args.url, logger) + DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.url) server.views.populate_image(view_item, req_option_csv) filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename) @@ -204,7 +204,7 @@ def generate_csv(logger, server, args, view_url): view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) req_option_csv = TSC.CSVRequestOptions(maxage=1) - DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_csv, args.url, logger) + DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.url) server.views.populate_csv(view_item, req_option_csv) file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") DatasourcesAndWorkbooks.save_to_data_file(logger, view_item.csv, file_name_with_path) diff --git a/tests/commands/test_datasources_and_workbooks_command.py b/tests/commands/test_datasources_and_workbooks_command.py new file mode 100644 index 00000000..0cbf765b --- /dev/null +++ b/tests/commands/test_datasources_and_workbooks_command.py @@ -0,0 +1,93 @@ +import argparse +from unittest.mock import MagicMock + +from tabcmd.commands.datasources_and_workbooks.datasources_and_workbooks_command import DatasourcesAndWorkbooks +import tableauserverclient as tsc +import unittest +from unittest import mock + +mock_logger = mock.MagicMock() + +fake_item = mock.MagicMock() +fake_item.name = "fake-name" +fake_item.id = "fake-id" + +getter = MagicMock() +getter.get = MagicMock("get", return_value=([fake_item], 1)) + +mock_args = argparse.Namespace() + + +class ParameterTests(unittest.TestCase): + def test_get_view_url_from_names(self): + wb_name = "WB" + view_name = "VIEW" + out_value = DatasourcesAndWorkbooks.get_view_url_from_names(wb_name, view_name) + assert out_value == "{}/sheets/{}".format(wb_name, view_name) + + def test_apply_filters_from_url_params(self): + query_params = "?Product=widget" + expected = [("Product", "widget")] + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_values_from_url_params(mock_logger, request_options, query_params) + assert request_options.view_filters == expected + + def test_apply_encoded_filters_from_url_params(self): + query_params = "?Product%20type=Z%C3%BCrich" + expected = [("Product type", "Zürich")] + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_values_from_url_params(mock_logger, request_options, query_params) + assert request_options.view_filters == expected + + def test_apply_options_from_url_params(self): + query_params = "?:iid=5&:refresh=yes&:size=600,700" + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_values_from_url_params(mock_logger, request_options, query_params) + assert request_options.max_age == 0 + + def test_apply_png_options(self): + # these aren't implemented yet. the layout and orientation ones don't apply. + mock_args.width = 800 + mock_args.height = 76 + request_options = tsc.ImageRequestOptions() + DatasourcesAndWorkbooks.apply_png_options(mock_logger, request_options, mock_args) + assert request_options.image_resolution == "high" + + def test_apply_pdf_options(self): + expected_page = tsc.PDFRequestOptions.PageType.Folio.__str__() + expected_layout = tsc.PDFRequestOptions.Orientation.Portrait.__str__() + mock_args.pagelayout = expected_layout + mock_args.pagesize = expected_page + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_pdf_options(mock_logger, request_options, mock_args) + assert request_options.page_type == expected_page + assert request_options.orientation == expected_layout + + +@mock.patch("tableauserverclient.Server") +class MockedServerTests(unittest.TestCase): + def test_mock_getter(self, mock_server): + mock_server.fakes = getter + mock_server.fakes.get() + getter.get.assert_called() + + def test_get_ds_by_content_url(self, mock_server): + mock_server.datasources = getter + content_url = "blah" + DatasourcesAndWorkbooks.get_ds_by_content_url(mock_logger, mock_server, content_url) + getter.get.assert_called() + # should also assert the filter on content url + + def test_get_wb_by_content_url(self, mock_server): + mock_server.workbooks = getter + content_url = "blah" + DatasourcesAndWorkbooks.get_wb_by_content_url(mock_logger, mock_server, content_url) + getter.get.assert_called() + # should also assert the filter on content url + + def test_get_view_by_content_url(self, mock_server): + mock_server.views = getter + content_url = "blah" + DatasourcesAndWorkbooks.get_view_by_content_url(mock_logger, mock_server, content_url) + getter.get.assert_called() + # should also assert the filter on content url diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 117a9181..32497be6 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -149,7 +149,7 @@ def test_apply_filter(self): options = TSC.PDFRequestOptions() assert options.view_filters is not None assert len(options.view_filters) is 0 - ExportCommand.apply_filter_value(options, "param1=value1", mock_logger) + ExportCommand.apply_filter_value(mock_logger, options, "param1=value1") assert len(options.view_filters) == 1 assert options.view_filters[0] == ("param1", "value1") @@ -158,7 +158,7 @@ def test_extract_query_params(self): options = TSC.PDFRequestOptions() assert options.view_filters is not None assert len(options.view_filters) is 0 - ExportCommand.apply_values_from_url_params(options, url, mock_logger) + ExportCommand.apply_values_from_url_params(mock_logger, options, url) assert len(options.view_filters) == 1 assert options.view_filters[0] == ("param1", "value1") @@ -166,21 +166,21 @@ def test_refresh_true(self): url = "wb-name/view-name?:refresh=TRUE" options = TSC.PDFRequestOptions() assert options.max_age == -1 - ExportCommand.apply_values_from_url_params(options, url, mock_logger) + ExportCommand.apply_values_from_url_params(mock_logger, options, url) assert options.max_age == 0 def test_refresh_yes(self): url = "wb-name/view-name?:refresh=yes" options = TSC.PDFRequestOptions() assert options.max_age == -1 - ExportCommand.apply_values_from_url_params(options, url, mock_logger) + ExportCommand.apply_values_from_url_params(mock_logger, options, url) assert options.max_age == 0 def test_refresh_y(self): url = "wb-name/view-name?:refresh=y" options = TSC.PDFRequestOptions() assert options.max_age == -1 - ExportCommand.apply_values_from_url_params(options, url, mock_logger) + ExportCommand.apply_values_from_url_params(mock_logger, options, url) assert options.max_age == 0 def test_save_to_binary_file(self): From 9d1d40d61855b148609ec1a318ec7f1a5e19935b Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 17 Feb 2023 12:57:44 -0800 Subject: [PATCH 05/14] Jac/delete extract (#227) * add --url, --include-all/--embedded-datasources for create/refresh/delete extract commands * replace polling code with library call * add views to workbook listing --- WorldIndicators.tdsx | Bin 0 -> 160687 bytes pyproject.toml | 5 +- .../export_command.py | 4 +- .../get_url_command.py | 2 +- .../extracts/create_extracts_command.py | 33 +++--- .../extracts/delete_extracts_command.py | 22 ++-- tabcmd/commands/extracts/extracts.py | 30 ++++++ .../extracts/refresh_extracts_command.py | 69 +++--------- tabcmd/commands/group/create_group_command.py | 2 +- tabcmd/commands/server.py | 2 +- tabcmd/commands/site/list_command.py | 15 +-- tabcmd/execution/global_options.py | 11 +- tests/commands/test_run_commands.py | 19 +++- tests/e2e/online_tests.py | 98 ++++++++---------- 14 files changed, 151 insertions(+), 161 deletions(-) create mode 100644 WorldIndicators.tdsx create mode 100644 tabcmd/commands/extracts/extracts.py diff --git a/WorldIndicators.tdsx b/WorldIndicators.tdsx new file mode 100644 index 0000000000000000000000000000000000000000..2df960ccda93870af5bbb4f8f8f7cd6ba0d13e65 GIT binary patch literal 160687 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̮O}L5p&S*HLi)O>Bx1iaVMB5!j7B+d8vaK=X+7jD_ zC1_z75;i_rBpa>fHtew--o&~9OW^g6uxxLi&FoF6$EyfJmsiZ7h&i~lO^Ov8^0UR< zXR!Y&=#*+_6-g|L1sKBmaBj%7H9CM|_^G>%w<(H#E3g(-&4t|rB2baALpKBlF#)aw z)VPH@!&t^%V7BHgwn$`Yth&4iq{z-_{KH%*TkC->?#Wahi*=hIis|S>YO*5*$kMZ6 z5qmc}@=2hW5t&MmgIZ-O)^!j}=b~7BsnC&z!2eaUop$!YcETDguRbU5WR}?(9YH4) zbt}yWnW2zkIXzlHI)TwN+SyMY#G+jXXlMi4!bF-J!4iw(4lf92G-w}gC19r<@+^tE z=YplyMqk++5^aoiB)^J^9ipp-Wg$qv9HbnMPOWNYQS!A}a-#{!Uvu^#*Impq@e#vI zT10Q$nN}DA1<|5AoBIMKtB6|GPbg~1*TMEqTZY%j8qifNCE)o1yT*c*Mx&t;K)I95TMNy)ar0%aV2 zy)Ac+Dx=wjVb>eIU|>t?(A8@t7$lODQh;s@rAP+`4|=QE*Hbc-2)Skmdq*00Lo};^ z22(ghWTSvx(Xo`mqPN_wy9`MwJJ0~6MI_)83xw6u`*<&wug~sNbtK;jMpAqBfi+tU z!yJVwt-u;yTN?d`4K$s|cj)j@&v)DfA`U@U&;o*Li_st;L|EF_)FI|hu0~{C)VZR+ zaJON%93|_lpl}hs$gq^+h0g|9{X&E+?ptNLSFhG<$poMr~Rq&De4!efc(Zq&B zT2&$5)>TxS14^V0DncCDrK;Ilbu^?-SV!!HxK3zHo$+4Y;DHFi-X2Tws);p0u&L}# z>5hvo``6~Mia4w*{)mn)0{whlY?eLG59?`GQ8uMzo4jD+!)VbOm6jS*&af|`cupZv z_!VQcic0m(QtcIRJfnbK(}rQZoDS0z%*ya9^&;Mu4GF@YLqgcEz2)jS62_d_ciWK2t*Q=s0zV`h<7rrG z+YGydVrT2LQ4QQ7xoz1xO4A>?WkaGXP`)k%b{)HgE}&r_H8KL6y>>|;JXBOH4qt&! z6Z~M@Jb{{SmaoB(l$?aTv3nLQ4BO_=T)PW2%|cG9NRN zZukavF?!?&{-GRQ4nTtPr-X~?C(I8<9Oa3=80Wm8j$s(vD$#C?Kj5OEI0uv2&RAGt zG06S~Rk)#8W?^-%0E=vZ{=Xe&s4;%jFNYLji>oaXg`p^(~IrMrlxVH~vhV2(>8>h)`xHl+l zIlAZcp#eK`TAD?x$js8-W;|Gmd2%8~xls_@pZJoxB`i*}H=wRj-0nf~SDDR0T24!6 zH#Z8AV~wQ|TuY~|LumGK)G?XaLYZb{EQXcJW)^|JD~{{#EGocq3`^L-WQ~l@VcA>5 z95fT7{2>~UscZ|tJl+eoKA6;!=;{10c3*(}yrAvWWad66B++86{RO zLU*86t|eHEBwNUSuqxr_qV{|=77O-i4D6A@HC?TZOS#ZY9?=%kUz$TE%QkYNJhO?R z(f7dPykK%RJUD2ehGBxbyIF#dmw@OjQ}&6-Rxplnp2pnv7%JPKP<7MH$fn>oxz5jN zX|zT`AL$;W6F$iIjt@p3xEyWb07^X=^YUMta%&xkrppH&DLIzwLWKK|w*puhjfFz8 zSPR9))=ss1{Go&2GZ|GKfu1jndFJA{wq=bA-Eapj($dk5^Np&t9y*niU)4cVVB4E% za5CF&$4y=kOK(!zQAhxlVjq-P8zL6aye%bHqvtOM+Q369Kbd(uCjYbp>O(UDHN6AYe`vdd7y7hgoS@4~3GOuJuV>&EL!e9GdJ61~!NFFj3Wi4o*KT101VAGZIgZ}` zAh(JzAtZnN!6o_xECoD^c-%^FfbuQ$JP_7K89+Hve+>Np*n1B!DT=P|ySjTxoAb_w z$uqk%8(H!a8(1=uK|#qmCqXhp&RKF=az>J(s4#$mAc9#G70il(V8-zMPu1Dy;k_@P z@44^$ectcAuJ4+H+Fzejp{q`H_jFfRS78_8CKwBT3(^(k2N3_o@5qN1}(4xTw;XjI7MGr!v7hLortr^%G zq_=kT77oX@e6dyx(hIW*;8?IGNH5@;fHT3?;3VyqZ3s9DGQGtc34RE(&|Ar?s8%Pi zGfG+v`r)(l=#5iin?W#9ExG-lFOUA6t@$f_*y}>%Tya3%AOEUmFCP~b6%kW5Jh4Jd zc!DiFJ|-nTF(x`TBG#1@9+m8{hsVW)N2DbREh;`TF)}L79a*+~yd%SE$xLxOvQw?u zmQ;_`lI3<*X472yeYzOb$Qd$?H*r}EzN0jyWCE|Te%i-)&Nht(=F-N>{;d}K7$CkgdQv_-_nCZj$vu@z$7$>GsdpV*l2 z$h0J(N5@4bMMTHBqso>;eQc?j7OIaeJ2lN~P4&557E79{PYUYe5x(>+uhZ@GWLf=L zKA+E#okjKW*|OaJE8=dsY?sU9%l6yTeW*&d+v|0sDw$XCI7HR3U%_Ml72$IDu84Y8 zhZViqaamWy9W#DGU8JSiT&7F#4BEyfWr?^VA z>ozi--;wKs>1^2}^pP1j68vuTh9!7WC`7C1@p-O@l5Cqh6Q%R`ldp&(j~l8pXmy*- zX-iFVIuQ4DG`SN^p5<|6IU=yn2;4g+CZISy6OBU0 z#d%^Q$|s`G8Cg~oIvs_!QlUNGwA74@jBHn$6V{HyA=tz3_1XP6zBcFjMYi4Mw?ZkB z5PDbhd%Rf|kz?~@X27V~uu>bh$LUMQ5m?6QLphxu8`k56t(;!`Lp#Lbz$x@OV8&Rq z=q+~y%9@mfvc|~<^4iac(IrCNPx zeNSduT80&6O?RfHUJ;LF_^}f|j>3jHc3TdbHXBxTT5{a3tZaw_ZB^V}AM82No1NmY zI&s+A-B(1|tt16o!P<%n<46*@(!mxEyd-`bY{KP=hFFuS09ypbO0>Pb){!#xve>#5)J2QiNlj^ zal&+7PkflukAsqqkbvFcvZtrKy;W2kP1N`MJR!J+5ZpbuO>o!X?lQQ$JHZL=%;3RY zg1cK7++8!cI}C7m-t%6bbuPY(@1obLo9eFGd;fpctGjCNmW0$~4Cz@1Y#zfjNt$*R~i4@$u4pL;kCPqI6e0G+2;IrS{p>sg;MA$o8jv)IIIc&{kpA_C=cuCvoe@ zlax=!xXrk;WebODl^e^X+b{Q*YP*vw&6fVedp)QW`D%awh7PD zsdzJccXFsVJ(J3XL@{MHy5F&AI6XNNRjm(jD$u2#7>!E#=Yw9F)#V&otuD+cXE8( zb=Af1t0Hl^)@aVpq@nGrmDS_Rwilj0(_aB(!d!=C4xYGwz9Y`Yj)ZE%IEp1`CH1oA za%Oi_^Hn^eH!gi)UU$JinxrEgp{Oz)paCSTzn{SWBb(wQ)nl z*%t)&2}?2dShiV;;y!To1J#9_&*n=YjdLkG>A(3_uXNZ3u?RzNsk^NpFWg^aw{cq@ zzEWX)WF`}V_rsqFwyH!wr~rlx?99cq1KK+W#2 zEp)hk&GdeGds&I@_=E>#?}(m$qgx zN$XQI&HFEy)c7eVq^EpKgbh)mCZ`l_p;hxx)4LXRPtpi8f%KT?R`v1k`Ss2}>@iy*}Y1+5b@)}Fs2W3yJCH(aCJ`JGc9VMrCBam!&D z(;*mz&t!=?z_(8>Mbf#8ERrUr>+4rG-`4{n^qPFRAW7K1p9x0YtuPe3K-A;yB-BJt zzF^#6{EGA`o%TzCiq9CEn?mJ7yo!(OV7E}VcJou9aJIJXX`mfnU1$h!LFqx5J9SAo zZv44h5UhO^C{Mf4y8Sj}X7YBr)RFrJuJe97_A`0YGrcxUy^b2K^OmzRWas6o;U8zb zbBrJFN1JAajhS8-zoeU9vyCrF@s2OG23k06lylfPky;Qv$Gu;`At;TJA1V4DMh-M% zYNJrYK5vSM?P)aAzw>MzsQBS@5rrR7If)tB+cJ|*+O|+# z`%$mB<|bLi`jV28q9Pm}rn{CL4bJtlGX!jeXV?3=VU+y|S}&1Fci+lbTmB=OM?d$Y z=x>}J7)wn}_gQ}rY&Nui?_MT~v-_Baf%v=kf8>}fRi^H8P2#f_qvdwHo|otI9w+}M z7UblJ!Y&}WbFnt*Sl1%Rtz5*<&dSyu*IX;m`HLl#s9_##m7-T9O{P+3f-~Khd3p;N z_td!DM=v0)L6|>^x(v#?+t_PxI}-Sstg=jysj{9F)V62ll{Vz5Zpl_^;ca z&~4^2niWjrg+CZ`G=nxu(gqx}v-S&oUgUP*lyJR;)#IZ=ZDMD*XbwT^4Jl43vwafg z>ece{il2C%!-h3g-UgQaW`Q2-#U`g6>jfUEH`(oeA=-`J=RxxY;ald9K6O^cZz?YX zOUq+)_L{3|;zd0oj=)2t(@)OZqAVCCfJ9Q$OlcC*H!Gg?MKU&d7fKgr#U=4&U_ z_@x3JM`lu9u37hZFWpLu$Rx(pA zBkQcKt#un=iP`>~J;eg^+V#?z{R@6D z57#&ffk@bw?|+FC0=qP+#3Goi#dK76?WqemG&mP7Gwdc(Sk)KlxXjc~VOdJHrt7M9 zELg}`>qNTP>U5e#b2><5GdYIW9QH}OUgbDyt|8-GJ{mlB$un&n+1QMZ4w7%)%H1ih zwAa6N;H(a`F)NYkMMq_Q;U9>S3@JgwO5SHdJ<;WmgOIe^4ClDJFWP;FxZX6ZWKv6O zpPDn9{N}DnQ}a3&%t%VtG_##+R~Ua!j>D9}po^!aXqAGP?@~FtpoN81$6KEu#svp>q`5bW^;cOK)Q~OpaB+tMW+-!Zy!FJAr zv6Z`H4NsY@1euv+b_<1a8d|1-ag3X}){g5_^TdcI%^oJRGo3;H?TC;-zlf#UzXO-< zrDOCRz21J)Q-|V%Dc*7==Z0L;5TZtl;f>qd1xNW2X#+cjWGapm^9xOu`aF!I86$aF zL!ZPd=kvQpgnX994?96o-w@()=F1nv4AykOJ>=WnC; zv?%#eNr~m0v;r5cccnt|tcxAN*MJwZXrjS;%az}HOeE>)Z>qD(shEZT$WEdT`5s zXAj_ZkpEhH|Jwo}hoRIQQ50FGlX}z|+B~|H0=uE~-4)6{Lc54MS(%kQ?#utRE7@q{ zF2R}0$XMb1w^G+zV@RdO*SU(;1XdX(esQA@G7+cit8$6^L*Tr?Fp6QDLBqeE8~z8t zRGYtBWQxELa=f@+blGqFTyMwAoE|ed***0TloM9Hb0Xj=s+d+_U}|{`2vqExFnQh) zyv8^q>F_!wRr&qx0YWyprvKi<@0~LS~fW=Ao4uf)DN63eu%o;Xqv;7 z!I>eEib8s*0JCg+8)eZ57mO^oIPSN-sKoPeO0@T7P|-#m%^yb<3q`P2cxwPpVdY+_ zQ~H3)d3kw|0S^N=J-Vd5qmrlkP^%$GjD$Z(>Z@=YXsYhAzuZqRoSCXo^!FgPaG_?? zWX__^Whe}$6}5UZ?uoO%*N*DqKXIo(Y(MMIdrOMdj(2A0W_3PQM06Kbr3dsX-kYX4 zFc)OfoFrrSECsc=E#JV6Sw``ia-9pb6!i{`mr9c9xiHT4mFW#MoMfOYN8e#H!7mV2 z5Baw*9PeZC-dg;`NVSd4VX~PkrOS9YeXYcuhqtm!YkU?Fj41;0=%tI1WojL1CG9HhEQMHy*qq zP-4ntv4~%9JD$2jAZi-WXt-~rYm#ALwdD|OcBs?yBF&Kab_qCJEC>M34P-I$X_Rq# zl$Wa1cUhe@Kl(G5TP{{;n_i`$$)^~Uq=E!W{-`%ZQ1KG%yE|&7HhAgL+}!OK<`eSh z`0{dQ6cyqcVs*2~{Px0rnjabUz-cgGD>CV=uo$KY&?*p^quAW{sST@AlPd0ZV7(kz z6O1QY6?VD3doj6>yPk9)7xh~ryhepPsh%9{|nO2irhs>FV|`>+zWWFujuRDfPnjiid=i-{nMe<+p-9RKN{mw^=#ZSDQ7ja5AAMt9Bs!SL{Pu z7@PFis!RsQZb^yHT))wapriPmv0!SG{kC{Iy2Fx&p6I#5whnIFF(X6TSp}M1*O27Z zFTdEZ`M}4Opfg&M83<`Wz_qUDYQBV3>jXfnW6OBQbXAt|s-mc6c=XJjg3Ys8m5`$i zDL4c`-(`wchqi$JdKN%@u5w)&@J9+=cL8FpTm}W)5Jo<^ul_uAtzea6aJGuXr1jo!*gH$F(R7{)#IfvgNfoKAkjxutb3#_^ z^{6AIY-XEzdfdU%m~QjateroNmNR)RPq%zgI@eK}LT&dW*k0dXy7$;_o82(foUN54 zkL<}kttw3pfdVOVo=j@&Bud8w7tG`Va6-{Ylf7!Jax6JZ)1A~=2O7P_4)w#=G74JdhlW zQM@=NlzINOAF1l8-}-jJa#NwwbCh5y8rS!VO%aI3i)8pug(w;NON|ll1zZAOJY_GY z=uwe#@QrRJU*^$|^I$Wm3ESuA&7XZ~A`bs}3;5Ky^v08`Y0ZB5MAg1`d=}qBM=^Ws zfM^wTBFTI3?H+mLU_04BrKCqM!P?^9d}}OHI<9NL%{b5u7GIb9GWGV4H|_UMJ^=Tt z?uFn;AO*~|mSj)=V61OQ&!}76Y6O}<7TU$|@Nq_sl^_6bfF%@>qDImmBUmm$i0r8@ z@}8In)!I@t@Qb-2d^f@6p;zHqpN0|DfG^s!KkD!B7VGayz@HuPO&`Wggkygx1$U5G zANKG3jXV#6Azc-0!{1nY5buDe)Erlt2W-(CL|>y59no$S$m7Fu!pS;*xRoSMQirPT z1NqF35Q_{e%S&A`2TF2Eqn6GYx70&fMTLae;GexeUY=3bsB&XIJ0iXiTu@&9qStt$ zv}Y+5eM4#L77@x$?BcizBz`nK_yvj)s1ET=P$Ae8Ch;NMaDp4qR@g?g&gP(!)7o-irK;*OrT ziTa3y9|vQL!>KswsZJ^>#@hw}Mf+><~? z0i0V7GXW)J#F@7szc{MRedXf)?ZrHpUXOcZOAcjy1*$*EfMd)9c0vI872(HCv~9XG z#%4V0CJIA^2c!c`Di{*X`+@hnC(7aRCSIW6O%3HaJb4xNCE!XR-j*X72eMxXwW^bu zjC`O#4Q2ZDTjc+4<1HC?M9KmXAtKb3*}l2$%71tP&UP#6eW+CA^JAeS5h_WpzESI& zQQA!?as#+i@;)i%5c`uU#dRUl;T4cudK8ZHq#d?fy4;Wmw@SeCD*alnHR&Z%W(S8%< zeKO3^4+<_FDLii()lf~irkb+ANvq(fV7Cr{bNTjV5B6_^%%*cvMG^TvU7kWl`VZpq z4>EC3J#x!G-AC;TD|S6D;uh@nSoQ4n$o2H~1ogc2@bxsw#iw-dXMYuIl`;XWaIH-! zE?U0hEu<~zXvdXA;<}V*q&I&h9p{sGs}(6c1>_R@pDR^oe!vY-u12h7D6o*f?L6y1-JtSNCa4{h7^)8yfcil{Kq;Xn z(9cj+CZ*P>JCMR4nvec zxg5f#0TZy`N1AI!=prN&;t1O0=r#?Q>=^7A?-+vxJyKoMK_?+VP&Y@Q>4xd+1T6Ux zPtD3E1o}d5$!6V(>tYTv2LsO0TdyD}5G2}hIq|qOPHMdI7~>(1UC<@uqXi}BPZ>s< zBst?580Fp!2(bBymvMH&jV)WAd$zw&Cv4RGJ%ogcb7iha62^?7oTwliaF~3CaIn6) zg$C`yWO6lMeWXsZPP9&@PK3<0dP2o;p||qLH#07i83F3O3^r@Sk1pe2W)R>UwG|Nr zc!O_kf_OmgAQ$MD24N?N0WK&|xH5VEd7%hBs8-WLxfGt6+Y(TTF0Y9*Jj(5-_!!^gXJCq+%0l5dA zbJnm6n;BYe7zOmfQXYA)QK9LO8ZbBbf4w)}(GLSZ(p|F_7c)K^Q3f8expujkg870eH62m68$!P1~%Fdc{v zTmt$ZgR#Gx?Ej5@+-v)HV|jwLzzshCwqk-7!Q3D+PGRH!4R_jWe&{_!4?+g+W(zcb z?S&=({5e^~*v~W>V==^$492A$&y|dWW>ZREXC{uB;<8n+z1!g30^i=F{d21il*@VKab7-k53dp06L1+h4;vhdmqg@xB#>Z(%#T&qxp|C zwBu^u&)BZ<-|<0DJFXyoMw|>4qQ1vXhN4p4V_kDX@8t_(P~t5t*sj5;AV36ls||z@ z;scR_SU@l!XOInfU5SV_9w_xY>WJTQ_Dw#+H(~8RaEZQ8MhA)tafMt%w7`y>o8~jB zd3zg{0psuZ;H($Z!I|v8;dBMHLdyT^qirs-S#ng+D%TdcYGOp-J$K!>qNTcW$ah5Y#g?m;LN-pmMd6%y+hxLS>3qXs5 zH-j^gc-;AW^?S!GYH3%NB%)s8;lftd(do6^JqvbAPU{J2mrRf^SQHBXPT|#e-1>yd zk`2XJ(z0KJzJaPh*`Q9)3y2DY3W^4gfigjQ@3F7M2{baa=o)*cH}f^uH3lbx>U{qw z0DKD`2UmefK-u7WQ2qNzBEXXzxu%By8D#>N@yMwbulhI2Xa%eQ!encIN1rqYnYp4# zwW))JgYlV((70dFBkncfH3BEEZt7Uy4V(Xa6yUxC6$^3+k$DGTE9CtY_4{IW3#o#f zLb4$SkQWFee!;f0E`X}0aJnFIrcg6I5FAd**A>>hU6nPp5-ixIs73G5K)LT zg!la>k`R*j&WqqhFpzU>pzNg${2yD;K_Os7a0KX_ZPRpUpaM1nB9N!!OxFp~$y^Rz zekaRvE-#2T<`LhO!lDNDb14fg_Xz-crB9f+|7OSc;U~ z7(Fcc1Y}tZ0)OF30VJY_>junp5~Glx8I1}hzPJU%5L<;cKbcG7Z~SX1LKI*3`Em&b zuNkXdpzuqjs}4>X9fbxH!2A26Ayvp)X#5Zd04HJ5Ye1dOmR3qwy(co5Q&(V!ZjVy& zgGnQR!PGV$_h**1;bZKGj8JLe; zRk1VHq0n41TKBZhn^TkAyEqReSQ+}9O_V^TVaRgV?ZSf`70!b7Y7f13 zQ^B_(C4m^i*csK@Zulp<75&h;KKV`aOZ_!Dy{LN~nkheW50j6B0d^XTZKev5>Xn|C zgj8P|&b{U#f0Z$mF_ZKle zg!2{VTFs7|L!>u&O;*IrVbk>)%0he$GDHRQ25oF+)YfizvXG8`ELY+cMT3ui+Uxlr zqyhx0Xp98I$|I8_Ys;l@?6Rlw|Kex-(Eh0pu#?W0%$c?uY39-`Mp~h6c;FITw4!r3 z4!H<_6Rj#2l4sXJj@da(kpHXBFoivh;Bv*T zGZmj|C-!bf<0fKadN$U5NR{|s?pp)hd`%2ndg39U&o#E)$R$D|jGx`z`FJ-Mh_+Z` zi3th>;T1(I8pW2|-cwIDAe}e{y%sc)z8^{RrTlPeYJ)17)r6T5KP|-kSO2-Q@wL>O zV<_+hn`BA|c}2t_cS6^@VVDx4sC14Y%>DV6dRYiV`-(uj0Id=agI{_#Mg|unOLSbq z)T)2LPn(^PD@xJ|Wg?lX)`v%zPSS4B2Svr?pJpWmV!YEv;*@>(2cD&riClUPoLK#< zg8BZ$eNNraf3z{3KPTGK6mT_F+tmH`hi>UB;_;bChj;?$?iSIW%cEOl@QXgrRS>O* z8OQX?;%7HBJvmoZT*A$u7W`J}*Dg6Tjn1iSt&#d9H;joB&7^+lY5N z*b*gRWQWJx!2D$S^nEw{k|M4_Y}dOA@y=I7nBn9}G)r#s*q~82Kle;*bWMsIHfVRd zqY^u@wCMCI@ZlI;#J6&$udXK6C%6lhA`C>+jd2H5$fU5bF8TGS&-5vCZ-rpIQHAHT zN#d9kyGRA|Oykg+(0HTzULC3>VP`aK%;-cBStJ@^R1rZ-T#43M{3^JreN$fhC zRw3b&MJo+ztps%be(exFS104Z#jlU4xixu#Hzl+ae_6hnXe+?Q^-!=|u#K*e3T_&D z7{{;jFB1;iEOH*wxh(gPe^UdRtg)*$Oyn0h%Mzg@TZ!XTIU=IaryT8R&ac#r6eqU!+lKzJDk%*&rdkp8hO?sxH**?V%28s>DqMH z{E=R6<8lKTn!5?VGrrdFUySjQ@-fZQ^Wj(2ep6SHW>N1VdhkaV*Q43yw#hKWdOoEk zHX;A>cW4G|Xj<=#T;aO*#aK5fK;N+f$QEbh2p4lZBvz1hS^Jt@M`*7`l(X@Rbtb=G z=u^ZE?hagk4GH(3u+!PIgXUVpS+D-;^%=$Ex*^$J!~$kri-gPYCWOEIWxy{AH^{ob z?&7)>`zcG>$Y_pj<@7b}APzGsJyq!(n$HcIj zG!?`m5RHsa*;;KPQS_vC5@SsX8bFM|yv>f8@s}<1&y#K+L;U zbxu_r=1oHMol(&xwNz42A?qHRerRNh08UHjyO#RTIAeZ_sIUeAFa}_#_Xs~J_2ne| z+N~lzKvb)iG%nRqj7-_Fbe~j1U+AJqwg1wiuoqm|z95~wCukqRQ;2b0(hW0BIZN6Y z5X#K+|2$PbKj6}dplgF9yMTR~9Z})RY#M)A1aeH7R&5}BvhM7DtrI;jl_pw^X$)UK z$PctW3AfMapKZTz)iqgCsI-}t_Oof2QzSw*-fzsk)vk)=8nmj>m;)|y#-0wv4^i9y z1X=l2o{H%JXEt#=Z{T$eP8D9L<1WMa){W!7;qI`;D*LH4#rM~4lNPbW_4(|$#tsSV z&l4RfQAuH)Nj00iky&XCT2;Y|0UG8i9yIdZi<)-%OeGn~2F1>_^OcXO7Q6bQbacrE zI)W30Z{h@b?c$~O-{iA$32N&}?pQ-lXPr_sd$6WHcGHs7W+?LNzXeJX%}Fy_kZZCx zjoDmAg6ReD?E!GZ-jo67t4ORndr?BV?kU05c_dB))CH2PREFpWl_q=(7V*8TX?Xj8 zoLWC9;~ve-FpqUU@Y_N5Ey8Y!Go;j3erhbV6M1#Y_qWHC778|+&j9t%D_pGYi!)8e`|7b4x_I%*b`TZ`BY;9ZYCC}@Q z0>KGs{L?(Q--CknkBXNN{BI(=EJD-mM{bkaN`AitqhBHpomXa85RvMv@CNW&T#Qfg zPub2~vRdTY!z+&acd*vPl{0sKis;3GxFxaT_+!Q}d?gFmX(I*MkK*wVwIQqB8&~ZB^Tvi6jayWZ|9+je-5%*NXl;_o45Ok( zubP+FpCXE4f+cB+KhNUaryZF3nZFmJyuK*! zIV-ymK_-Q6RByc3Wn+3%*=)1$7G7t zlCmZO=G2m+-n1}$UjwXTziIzvkMSs+;%drRX;>(3376|n>)L?NQa`k2t}oo~NZfdl zbfWnU2_uw=>}7Vpa9fLCGNuMKU`8JQ_UEEFkBeOXIIM5F%h_LG0D#)qHn#eB?^crH zEMVSx_j#>)^;?;v&7~9G#NDbq98Gc0t{Zcm5}qMZ_7_`q6UEi?wM~M8c75V5)9$iz zOl~-GIGK#kZRiH&`+srI<3{@W@Sp0uaGuuQ<-F!BD4a|yq@3i{{S$bZ5<=BUFT2+B zb)a<)UWw^tKq&*~*+(l-{tcPk2Q@ta_nB4X*{|RwsO4KD8JpOZU}#mZ!Lr4Y6P>1r zpA=Dyvx@Yf`+>9Bj&4*yfPJcztz2uL&7tv@Xx%5RoGzG&497EW3wAK|NZv9NTx zCYboNaJRbn;i*pa^85{P0!6e4tTFHb#pg^DWNlsZ(``Ls(>$LmBOgOHaP7U2o{z!> zR<^ZI&DH=e4S2CXnj|wT-+qxzkA+WXShdHb$7;ny4e|J=ik>ea_FIYq6(*5ft+=Oz zu#7VsfoMQ&#&y%mnLkISF?bD@ZsS(A`dpR9Do=>oa;$n}CF9uB4@J6#ffb!4+0*Ww zhzqe5@k#y@`;$`6y05A+Y&Kh@-9$M^P?ToH-IR?P#KUeX=sz)k9uc0;^Wqm>+z2VS z@F!I@8kDfO5Qlr&iC2A{7W~T5v6}(tmMpFE*+X44R;vvwBV*WHvL);WWZPYn^x?Zt zX^~U_Qp*tZg~K1$W@t}@v}1P!A1a!}pIa^**#kBwsd8{U6H}dX?<$=lNqD;Xwpd(0 zO|;jwkoBUZ@HNIy)uOR$p5JU^fV-%{G1CRsJY%GN20Hj>hKIKChWOxGLcR0j*hK!W zz@9_}B~zgWV#WoF_!{!$P23P}R@z3=7ulh*mvWIZ1(u5WtFgzF8r7U-w5{d3A z?kcqH)MuK1A(D?B6rawXTLW!!kIQ_3&dN2>N!G@C7wK^aj4SvlUSZSnezEOgi&Nc6 zrOmxv8&mDa_n%A)wJYC3U*ZSXf&AHLL*X{aD{w3Jq4x7E##NnsE&OZ^tWGud2ZI@e zyZTBc&&nlDdIjuyU}M{x>;%RhLJaM}=*^rAo({+Uq=N6`Vv zIYZ0^Z61T#l_ax#huw!thkyHr@`RKKYY5ZP#_{vh@yCoa9WErNg!4+m{@QLHI7d_C6eG4(^*Bw#63Bj8W?l>SScNC`&7QB8>*eN z-t8=jB1$e9!UoH8T3CsLKF4!N<@u-W@Y;RREFuc>?<~1bSZp?@%zF!ZnOw}B^*P4F zYUYj`N?Rc*WD$MkzR-JO3Q%*%f1@)vuhNi2(`R)j?AHxNqS`bdAQak=&+-gtze4H2};B&{~ckmS1nh6B<%nHe^<}j8#5b{ zKXcKhh_d~rICrn<->Emk@Bo(w)kXZ>oy?xudy4a(xB6 z=RH+_`os2R{)?ix4Y?|*z-eK=Bg>lLmuxBR!1=!v1!g;}xAiZ9r&9S@=&!|DW6E4d zHP4m?N)3`V&npE0lFsPYqqjkaqRUsM#HR$+x7e4!I5*&$R_UcxJ#wIHmR)aM3(hMU zNh}rP3&Js3|Eps0IrZ-A(1xxK8aVh(8ZY9t(d*2#T{xy|ckChaYK?4vbGg_vA94GQ ze-R&R1BQ2EK;}%;lgWc{{5J0Vuz6Yo@FV4V%G($>s+e)Cm+g=Ab+S8q$=tnu<$3#a zXO`lQA`t70|HgeP;=d0tgm$~mnJVmW1^zV@_c!dWdFd7-=?*V(lkPG@uf;mGJ9t0ZQB2z!H9eM#p&UFZUE6a9~zczy)ifU|RqC3FFl z7jYws(qB8~iPeYRFzP2q>?>;& z(og}6ZC3KEuI0=RXJPW!h%sR>zL}QZqtaEqDJAJPJ@QzV+5)s25)S_{=}kJ}petq( znApgTpo{+nw`8sL4 z*oi}i>mDsN<*frV*fRS(I(@I8v__t zpG$c%KFW`%|Gj$7KcUUNT;uf(Wbf9g!FOiqBMTEE zWBO5oO;31j@s5H-RS4tPjJ94m2>brLKQpFqf)h{oK6ZgitQ!YB$eo3M)(&7ybbdC!Cae0P~h z{-0qk)U^~aih)zKH~c=9&Ga|fkt$^^R6hooa9h)oW&87;9-)JYDe}n@1`O@loAB8S zApjd@@t}H-lU}BN7UuGboQ(=YdF8g-3@NGOyHU9Ga6;^WH%_68C7lrWlC>tN%SPx% za@R&X4}DL0)%u)Q>AyWmu1Jsob=Agr7Q}D}F0sY&UtL3cR9h87SqDG=(L+Au>H&%n zu=LGFa$x67yzo58O}UFdJx)zK4xdLpbImA87$$EmQgj`XUa zk{RF?{axv>txM|y8&*{JtxGIbx=ps5ppUkR_qYgO5`3c|$IkQ7 z4%M*#mFR~*1?InppmDg}+D%0x|AWI74kBU{3(5|RB`mI!kDHiJ)nCb${qT5j+h~zW zoTDm?eKX=CpgYURyqLyB368u2_AyVI(#t^6C*JRPKU>_;ky{hs#c+{y$jyTO&l?X) zs9PHC`fr|i8}*y#viry_T_0H2wm(>VA!F|61uZ%CGRgkDG(emmdpO=VDLV1u7v*?B ziX|Tef%I&|fY)_y^oSNK)FfCKqgxy$g0MkbIUbVB1u*!gspr4!E4cM!9#?A=3g zXCeN&${`%43<9oTm+yHD@D9mL8pKN4G{jk^~8-k@y;}4G4LWwd_HKybizjZiTHwsb!Ma}H9hpC8jmcKRnl8?ciE^(|8ZCUr)8mamg~IXbTN)EZSYdEYE?g!fr- z&QpKm`v?0KB-6mnpCsio$IeN-XsRbNl>wE)x+Uv3$pcWvhdwTbNxAI6TDX;eOF#Zf zf+95j*4pa-krEPDl$3vOfzu`aUXSh}SW27nrB;n0=PfCUJ5*}J=nZjRRy9ZuaE0<* z^(8Km!>New2tT6^XU93$Y-{l-saID)y@Nhm-K(|+Wf&&&MOa%vEo@SS@hf=0x%|Bf z5Cbe|mcj23N9OuAUfqjw+$H=b`&E70wxI7j*QORD3Vda&Duzc54Sf@r)^@H(8k!k~ z`Zk=`V6MNb`a^R?O2A%n(#{g9>|3&KE1mK#HjdDC97>>JvKGsAjrORBe}tKnR8U)f zr@B^Hx?@parl-a>YDDS97`BGj+6SDC9IYAed+Q=4HO9Sq~7@Qm?q6`h!8^=U5&0a+E;%SX@k@dX=!ZF5n>?9!-elhT0PcP%c9u zKx6y*&lsxHg-KM~CuvKW4Eu5!gfwo|ZATB~ZTg!=gq5OeE1a%?Hk3CbFv>n^+wyS& z%4UG<;R~5;M#%ghwjekgNzm3k7JeAKmP{3rsZhD*4`cxi>f2smM)`#Ae3#`d-)%^+M&#RUzw`h%^_V4Bef+I^};dZ`(G(`2*`>4G6A z0gE}6?R7nd7rKy?jWNbbG*HRS=85VPY&XedPc8|nLMRw}JQ1H;esLsRdVvhE!F>|W zPBKA=^$0Tn7Eb@3NJxt}>nWmJ{YgQX@#Xe6R*KU5 z(xrM*PWgT$MwugF)-Vk7w5Sg=R(;#^14G9jRU9FYdXzwD9!mFp2TF~rOqfz<@s06) zN|&=&;oA9{x{UKQowd|_0$PJZeqXx#yW#FMwnt6XAH+E|9@=miU?EiuYXK66%?2t@ zlftS#oA*L-4Mp4 z(C)IE=<}~HIPzJFEEI|}CkF{=l`IKphE)|e>un7Fqo0c2yz&b#Sd^axeN$ecv$DB? ziZjHQaVWx9tutDz*y8P+!*sF;Z>7}q5CmD_&MLF5Fj9t&6C|7syK)p@p5`ayA!6kx zk4gHMT|62%$8I`;2EVF4vR92TM0RIzR9>+QsKOsBjV(&ONU{S zRQ2V8s{0&S)wWZcOE2mo)E{O`)E^vc7+&TaQ(te+WgR7EN+|JpGMTLnzb2tca3!NP z`Fsy!J+A57#INW}wy3y3)GEJlUrpWc&>UTL_@0rwo`i;>&KL*U{5xGjOUYASRQPyJ zJ}NT(u_%{ENi|EqU=Pe%c7ZGW)jN^GQGMJi-7HW1mzh&4J$-eP=2LY)wZbF?VALd)-&3w{fGCmgme(IM%KnmcvQS1HlY|5HjS<=2^8d{f&vg$yV z`jsAcs$DA`!BPT)V`T{06ZQCpLJCY0D3d5Cbr=CjK_k}x)#qrh@x)$|`uc!Fqx7hz z3Q%Q?0J+*sHyBYmb+wwYCMN~m1C_TIxHY`umDC>=_!(eo@(xIN zkeK8za3o34t`CEz;?!`$3hJ<9>!QFzSDN%U8IGlnn6N9X=+888&Z#sZr|)tuZ<5XB zdjBE{X0YB^de=K#lF7Yf(c6QV^}4 z`a_4fESbN~;i}(P`i&=p(d^k-vsPiIB(xi!@1wvNjjOHHq+Yny)Ykvc7KXsy@`Vii^a4^@r(ES)ggijM5Sv0qnX1CGgS`XG36F_L&{e zhX$@jGZAeFS1#)AG&PG)Aq*&7YPKkYvvHqP*m|5Xv%jYHWW0S9#NjkdM^LFMUN?b% zo2T>C!+YM4GR|Faq8=ymTZ7|4xw=prD0g^jkkYj;QnV?gI0N&GL%FZg+z#<5zkpLJ zzc_#QL6{@MLX7KFfP7VsnUkiLjI%>zA%Tz#$COJ@5_XP?%dl%w@BI1qsCyx;CkOMa z4=mbAU+IOM8NHSCXaZ-&XCP$1ZlrPL5Qc%e^({-hBQ(Som8 z{ieUk&8!LPUS3d)_N=QV^_Hinz790YzCE8Ondthk&~Q&$$RXsDC4XVmlttjr)wl{@ zq#0+m^Zo)PwxhS+@X;O}-j_j$5>7^&cohwkrruL?fX3>qay~ z%4LRIB48gq1&y}5^d@^#{Rz&1QKiFZn_7nbF>B|9BUOeB>Ha%{o@Uw?Il}K5HH{kE z$buSP!&nS{W3IC2{y_Fk)vzn}w=5O{wsc0`J4_nkwMvFCM9=D!FF^ZrG&I4AJ$~*K z0bJ|})s7NV>OsLwGb0-knh3%Cy;f}M5Im?%MwKVyV{AILlQ zcM>Wd{o_8iY_+%OOqQF*;UKH*X01LtxXZlGWY{`tT~RiJCOu{CEyrxj!Uk&)6E-DN zcVe5=%VJ>j$Bzpm1?|p|*?Oa^V2|Co@#`8OToz%UN!ex3a8#t?{XOfDm*&1yKXV6B ziX;Jz)K_ta$p`qmVRy#PsElfN9Fp_}hfofIH9igS{GJr~Vr%_1DF~Yp2l%s0+Gm;z zI3w^7lC*EfCS#woNk=e3tdVP0=U8Uuv?ntv66=^Eu$@!*sAU5eWsw%Btg4?wPvEuJ zmNd3z|9!jfO_H7BL2gw_LZw57!|9Bd#`8BI?dgo<|KaScg5v0dEuKJvOK^gI3GVI| zJh;2NySqbzI|TP-aoNS4;6WF6TU;0S%k#Z&_jPKjW~#gYHK(iQ{0^S9p=Qo>74qRY z0eLdo-!ouqm13y+ihD^Jgit0Zk!1^3@XF z3Nt$to**(NIz`7Mw26+Y>qGeE;p_HE*{rz}2N{fi8`Uk_95Rh^VgkU8X@kp61exh_ zTfdnY#Qa1w|CN%k*gJ^444X^#b_Fx3MMonl|HuWkAJqWEKIQm<7gg9;km@5QvEa{3 zd1-->&0DPG?HHi!xzYEpgdH*wA~#@^w;L@y!^%E!_5eVExv^8@1Z517042Q9%T&Z) z<6|Q-YJ_AC@+6qJ1{Hi4wv5YWNiwQPA}6@az@kIvg>pQcBY8VJWX)U{^~i%JdiEwc zBv*uvj|PYc9CsM=!-mgyNWu*ZjV^vmM8k((CtoVQe`-C!chr2mXAUDjI_rd0^ERYr z+ymb$c%@(j6bVwAu}_;%*zH*kOtwrIO6%~*PDbparXz|yXW8il>9P~v0ZR^e+&>(S zucc6iX>7bFhUHTZa8CX}A-M9B&8m$}MB@!qgVpLGsMLK;V#_4dS5=#lKdIVf8loHk zSfzD6jES3fc=%1714i*^G#+(ZM#-k0*GP0;B(;lg zHI&qEWHOIG8olty9eJY7U0Ccmzj?P6n;YwmOP4;c*-IA2edBY6aHU&QuMyEeRg~gr z5b_sjej4Uc(j5tjsEkf`$w;_KE3+TG{j66VG@pqnC6~*p-AG6C3^GOm!k^wLrTOnN z$hdgv2JdeEaiSbIv?@ztq=zcde7c&P{4eXgLnMW8_)tJ@)I_E6cW*0S@^+Y+tZ=Jm zJlc78Pz_r5`{5+X=TjQB05I^!?2UO7=raBn?eN}yjtzsKrUjBqk`f#NB_*`&ruC^& zRk~vDlVkanO?yh5{Q~k`=IX%;&*)L|(sd5X9+<7iK)KM-o=l4(Rg(;h!PCSE12RyK zi}Tr)th#OM7V%y*m={0?JyZivbw++Pr`}K^%CS?yzX<${7U@=j?KE-^UnCg53uP6T z6!{IeLi4)vQOjib%LU3XQb*b7xo$c#KK<`IB2T#`e+=q0Mw;(*aikK`Gb1aWD&d6|IH9KzSC z*ae?U89JvRCHDJ(J-TGcQrqMZw_4dOzXSvsQv16Z8`FY)aHDG1!(fbr+h5X=9;{QU zWOx>N^^R*$N$d0r`{6*t;XcwHSc@tQr#YX7Z|eIV99zpUwT*AQ96}^9feg#*0ksYH z+Pi3DgW&;dNdtcq-Zx!Usv4?0|FM2-?x2SLZLGzCw|f_t6#Jdup!@5|XoH?XDduia z^O2s;9z9A#a$6X;N{;O-#eZ9ru$wV3 zy(n!G`4TY%D>es1-xp@gcVT|>_WL@7#qlQs1N4m%`m330{BGQ+#NL6zW>Dy|GL^iQ z#zxWbLSSU?#4R~Q3IFXIl(l`T4(r*wQ4vjW^P#%boH#qm(M(* zPD7wq_f)t(-D^qCwLM&Uo}aY@u9?^;7|hpsv*SrcHdZJSK0X7)Znrbn0G2Bceg`A) z)Q(@)_By|4y?$HsWbeSr$Uk0|u;s&AiQd`P)0J~@Ya03XEBrLIouueJ=Jmv)dcV7A z|M?#VY03QHYi?`DCc*q8NZczv$mw*1pWtxdhK&Cn;P$YhBUfTNX)^CiB!EVz6V1*a;IYzTDxIUdm4ZdrG&bvWq~sIs0jry0Tp+& z!W^nedMZh0A>QVDC*z2d5KQ`H?wASFEM+BGm29gtH>&E*lu!bxxX!-s?+ZCcpkH(i zPPDoj*j8VkM9wCA>?y<>oUiXJzD@=JtMi_4fV9F#pZ76Qq6%%JW2>o*x_6&N91LBA z&O@E}@(y3($5P#jm(CuqAd&0;iLg zl;i3VK^Wat0TO&233c3U|DuaNy4o6>e8oa1m{edf)BDjCtIv`k!hqdAN;eVCMX!wU zo3e^{tCIGk?)qYfC@7}nLh=k_= zU1@M^EBxSK9a7iQ?_{ zuk6{E{ZOmk34O`B{(j<-Iv)69asUjMvT|Be(5(SYP$_ z%UGzu9WUBRJJT!-ZaEvR)Wpi5LUoYQOgwrV`8RO2J!2FyPSZ)}kn1aEwubH3)c5{7 zK21dX2wAy3AXmP(zAH#^ah!X@E%B<>$bw0R7@JpQbHrnU9I}f+?2`;Waoa}Cw?{1# z+Ir@v$Zu324z$F?f20Q6^IpdYZ9ZjR?Nc|VuU9|7(dqTN@u64HoYdkc>PC{$jVe~? z$6gtoGE#wcpP|Yj*FJ|$B&vzb98YUMZ1q-~zS-@Ep;o|cKRVl2kq>c`lTza;R(>x@ zms-(z44xp+T-pO^58O;RtPVpTj)%7hBTA3-$*9M%xw%CL=Z|loUm$&+cFiPRa@tDd zTP72X=$hUmi`?dtpMj+gL(%$ymzq@Z8}>F6w1(k>m%Yb4B=)8U== z0ACm@j}0fJchO_cgDy=m$kcfq1XN%Cd7Wu4&RS? z>3@&4W4OAPxMFEcN!q^n*B8ukPj$2IBMK#ozo4u<$oUthoC3Jxj)g>8eq9010tPNi zBK$>DxT2`$e;?^3mz|jy;R!K3 zM2$pUvN)Z7=JL1Lv6TqPvW$l%KX0o<#qvpl>&27EO}M<&(Pr~5Z{oMCF^Wm8QQ93v zf@3)cX?DP*gl-JYC)tQ>s%#DuzRtVm+4OGNwtpqyzajQIB`gBhsnMk>LyIbD9IeXM zjkJ!cSTBApvqyMJ8Ocet4z)}E{nu5vE71{YNxBJT_ZAdP!gxe7l7Ri}CF$JU#<6Ep zl`w5LGAJ4xj+vl)RfY64?gLsSr>53HV>dZQK``IpKDdvY;+#Y16b}oHfQW zE;0_%&0*FEHs6wMA}6Zc%gB@`$f+XYFDTp66zjEctkHVoO%s=s2CLTSA7`o(IKPus z^CjQ<#;V3xRbNQl{iBt; zVqhAiO8t|t47csVJV~V)YHR7VxM$NTrH3N<>9Ff2U(xCoB3lUtjk(+Hq^2B8in7}K zITui5-x0C4ia>{lnXFr(uv^G01#d59V@6O%eZ-FfT%RiD=5j6m= zVU8A6z5?$V2Obqg1ga)n>F)C86mrT5#a&uoYrv+3z3C`y zsy5Gyh=l@iOSuxlWGFsGQx0fc5DWQ|470!)LVkuDZvQu4EBlsxMC1D}e1^%YG_fsi zLfwu{<%Zz!;Q7!8fiyrQ??SN-i?%-4@=$911j0crzJ?s=3m1eu^bxk0Xd~R&3R-mKC^@YRqGnH zWghAjvCX4y)e(z812qggH86@T2)q2Lk^m%OOI$OFZDAdE-BWhesMwLZZ!qjL(Rujh zR(30KtuX<4^A)C2WSs4qdZd^G91+>N=zCA0{o}Eg((?&ygZPgsS?@tdWd#f-Mq9)7 zXcRP+RAj7l^J#?q4%D6Mla%UH?!>=J4RJPOONiiTsV}RcKQK0`b*Q2Ia)P0QGk=&Z zj!Ikbs2EKZCoZR}^h<>QDr*pw6}bUvi|_xU9L~fFGquv&A5#y zE;A6VEn3$)LDKDlQZv+Pe(yP)73EE99je`=FZ*-N@YZ&eIh+>$DRQ!10Z$y6K#Miy z0?J5Bn^tZHINhrBV`SY@$+7vsN$vtPan1%(?pO?*(pgupL_#axbQ?A`dNj+z<8%sF zwKFY+ed3JD%nS~PP1$ubb|X$II=}*Vh|71}ZQbz?d1E-SX6y2EHBPo6n#BxI} z3+&v7fajd~Jj-}t(S2QEn!wWZ$wPfoRo@x3_U40WYBhdnYNAnkI9;R7>Y>v%@T?L5 z0T51u$ZOsCSZ*k&bh1S`ERuw^DL>>(@+6)qW;XBe8|b4b;Q zLK`D47Knr~u|CU4)g8K_M`Jv>m9!!mZQ6Q2YRi6igq{ovuIX(^UkM8+w<>bTN&`i% zcYJfmsuZU8h%oUYmJMf4X74!PyjZ|h6^Er7;u*C)`8T=OjPHc^=~53&o559^YkPJ5 z`{^K0rL4JSx!-_4-ciF#`!>S6p`cfWBc&Tk@IAetL{3qKX4|PK#rI~MiW&XGg4o&G z80t|nwjbl=<7Lu{Ub{+kPL1tcv|E*{{SI{f|B=p{{y6PE~Ps z?+Qn1PunnY;5h=)@pSQU}E9{zSsPKYz z+&z9}`pHzpG>5KhWVsw_Njddz!FAWiC-8RWSBIITgG#uRJBy^-xBM9tI?O{pyZ?k% zaMdb~Tys_dw*03!;1;F3ld|OOJ=+ZFJK&6-VDaS=G3~X+$MHpboR~R%_u9^fv|ee6 zt7GKO9hK*UdlnT-S~W>h3*pYDRh7som5Z{6MVy1*L((AV6^^)%qnOH5H2ro2ZS^&t z$&ncU80`42m(%Q-$Cvv=dS`h8FiAjj_!5B$B`Br1mLio*=nLHbJ|51rAnHVyRy1l^ z`b}s)KL$gJ4cbWt%$ScNS~=uR=bBKK(*oCZiSkYf(Ux+~=73V#09oR*GZ42YUJt&x zjuHa3H8g?Jh3jH#LvjU^vfaTQi0z1pNvB!O>JV1> zgf=M5&v2dpcRwm`1pLZL@AYB4=v3hxMU~TwN)lLo4X`RSF6~}lZDC=zw%|;=-J-}% zL**22@#i^^d*kXqc#kn$W~jXJGQ}9*=+t1UVk(hVWYKMB=}gbfXs6euhaTJ;sbQyT zKTaBoa@3KlOR02WcBTtsAI>$H3g_o*kGkOq&88J4&{nEzL`HMjf-(oL=Asy!4z1!p z`D)l#)na>ct~525OWZsZcX5YE>0uV?tZ~p+(k2XsX;L#J9Fq1EL1PX~)YSryFMZ(y z>Moq!MYPJEnQxz9B=w_M1gE}|A#{GFg?G=klrTxd9QWpm~4GM9>jj~X}8E~!; zrdE;QY2aY*KUaIW0VdD|ZB^gSFw6+tqT71C{N_$r&fush2ON?L2`!^#w@CRVC8P^( zW5-nqO3&gNNTT~b$Cf-y2BZ;y(nL+8unSM71t7)MN%Qh{635PE#V`pVaFhiWAW7=m zy&-N>eXZ7XG?!*DYDYxVmB^it#{d=B(B-^)Mo(VnVK6wjw^C`Qw>$jm(Ae_5i_VO8 zBWnHg6i0}zD9PrPTgrEytoBaQaXur(OVz>OLq|Hkk|VNu0+q9~u8^lRjqUQlJVTz! z8ID4cIVTzE1K!+hgd<$v{MEcy_}%G0Vr~TsnlzAS79gjUGWo!COOf+!8N_+EmYFLZ;klRVK>ko2^;HcpIkfHa`HS%1dlWv_>Ov4s8F35Rfhy z@5@Vi30Q`AsBKGV5{@kzOU*ZIuMH@;OQNuGvVv5h1YL)5?!WxU6g(PKz61WJODxW2 zcyR0M8vxt_#6#LE|J?LfXuK8XmfYSBQr`SA4Ay-j)*EnGj==`)7QSt!UCS;7?-5u_ zo!1Qgw)Lg5s8!8f&ps`D>!a~y;m&W|cMFEQ{GM)`GFgH2m)y6 zUrkfy4crk2#J|eq*LzmF zWSPurHbQdDwr2CzwSbPGq2`;xih=UtSx8g44j6M;tdj)HQWQm48SCv(d8_|bQSV$~ zXhZfvdd_Xx!Z$Ewl8HmoXD3NmezG8AYBxjcX^wfB2HfN#9aP|V*bJiW7mb@rnRQyG z0|U2UR^X4C8dQxVf(YF?}E!$L)GT0M%5GAErv~>yGqvj z>+Fy;ZcPm0YXg(c-kO5_F2ApFKJZwGYcb#YBy|-Nb1298_y!?PNIV<&;GVpl#?1M*<;`!%RCO*7d zcXjI}ai1o_@;_eDMxxk)ocQZ!O(mA%@TnkEmU?-eLR@x)uThdd4#Bsvy7NU>SE9(O zc2f>Y^|HCl^YmA#tE1mL+l$KaQtg-7JcNYgFiJfhrf8lC*GGvCpt0HXBL2xT9JK3(|v74r-?o z;OmD2q>+5BvT0S-fS>1;hyK$f->ATO*C%qt`wXaY&I1tdN+n1b`ch+a#bs`uz+1V% zbZvs@oxB%d0rDKeT%-@f?lP%$~0B6kL*a7>Pz`}WZW z#W5D$(T{^qQpMd&4y3&?N*D!q5_6yyaHP3&AY6^y*<*FHru!24S-3f%yFlzRiDvXy z@@w!TZ82VTZJUaght?_>^_ssuN4A^Z0=(yoz7w0Jhc;(5rtnKP>)GZga$UFD$-i)k z3m?e5OzzOxmFUWFoD$15_`{)OUKTtx-#+bd;oyX)B>|_cytj@6Z0}A6IOly6Y74+u9-39DN{nk9>CRu5HqK67``txV)16Q#@a~l4sh$uSKGA%P_(@G~rE^ zyKDrMt1W3id>)qm@zs|}e7VH2go*Pv5|N!{$z}Hz5~bhtDy@p57}kTJ;ECkN=>bvm zkzAQnJQu@wF*V8Nn}CkLzo(bN$0Dk(ndP5FeHq-No~NUkQ7|-fNe(LiJ>$p0Sqaf} za-(LL8|)@<(DvKzf8&7hfB`-IA4{DMo5quzj(=mkoy(4UyzzuynG@!Tdwcs&q*Ui; zWKM0!b$s2V;>Unt0}%awqlkC@Ju)%;KIY8A78su4%{LSwa z?5FbpNPiaJsQlb=mmGAkrWGvk;)l?}k{o0T&4-6g>O7jg_-U^UXaFzoZr`eBfcp}d z?)AqKhx>2M>yx0Tn+*}Tpvms2&rr>W`S9fDphP-f_~)lP*{Eua6%zUA3=o(|*37h( z86VANP`aZUslIYl7EH}%SR{@J65v+Ak28 z%5({ujIsvWDQ!4TVZUb-^G|sxt7`=UG;)pv0lZ1e=cO$E$>kHS(P}oxfV(qT^FhU) zpLeSOV)=xq?k>+Pti5VkT{w3XIxsHu#+L`n=m+4Q7dea_OYZWd-AOdwNdshb&SD4%=)3WY|x!CdkRJlVH@RrRoWBOwKC-2TcfKT5_y2F-V{X$ zkI&SST?a{c?1JI1kqzz>gHoR3wosU^m-H5=8=^-sl7b+tZ2@uVS?=~ihrBShvK<_m zOEleAY9D#&XE%K)Th-O1%gIj**9l`Ej0VVe7FWlg^(UY1Q@3A0Aixa)fZJ8k%m>5k z&5aDohgJ*~DKk>PyAxS?&?Ecdp*O?+d!Fr2dM@bI4fL!4xe>1j5zwfI2rTJ zF-Sb&$)7f;>bzHJi1^_+G{?;SKPIqsz8z#HZ26`H>Xh7#h4!}ZeE`bidLtR?zInA; z`+;uuCJ-k*Egbu5C2|A_97cV1o4$2}z<4?fR_H*(8|k-ijYcrjr2+Tum6?Dexw~^* z(ijK`R2V6F@ETMM6<~U+lnD%D4$8kLR}d%{{jin~2RhFK`4>Pbn4}MdDQDQDZo3gr z{dAV4b8ny6nY65v+*)hPBt5Z_i=J+R_(wL@6{lhOR1eu@&#zY^Wg4j}e{O{`&*TEy z$b;1QeH;&8s{l{!A?YUS((#LQEO|rcHsy5PPpJ!wb0rPg?*6Z3Z0`RCU)^2}$`vX2 zBO`#JJ0dq^DqRc%UFv(%V;ht#uppU-7+N!|>XnRnsPx#!<%}?pd`Vl1HJ{9rM|8Rk z=nZ0XDBcYJsuLt?%hD~U{ZsMVamw0Av^lwIV8%*zm0UUn^joIvW0HZ)gRVx%zaNGL zeJ{I&(RzvdwCUa6O=a4KYTTK((yvEfC7v#`uM7OW*cs?*mjc9Z8dGj))3sU@DB4y0 zQ})mEunsvkt1B&D(beEdD<7Ng1$ya-x01 zA85dd5SV+Dda$BPUzimv64;@7E!I-}iNbMGZZ%BB2de>2CwAG1#>UMQZZ0KPZ)55f>U7AwtMUFGr{!(D z@++C5J{-WV4p$&0PwnQA%3&S21clFEY`asp(MtT4d=r-aX@aKM zQ(@<-fkDgb6uQlVfNWrAhm<21bWBmq_3w0(dNaOGh`;PmU*4#a+Dbg`7zXR{fv02} z&aA|0(T!gcy0s7yKW#`i;*#w*!G-HE8$3UOeGc3rOCpDAU?8g2jw_~J{C z33yqS9K|w%a?xf3^ZXrBNU^EaRzv5>GX6hU$c6sELqLy135bBF3$v- zmp&J(izKBpsC$NOcaLPx8-=G!r{2XKy(y#}4Eipg&&^7CK0io#bU_-zV2ab8V=6wr z^bi~5vX@pd41mKL_2hILbxM2rnFM{dL=4G*4|{9U(QNT4h|)qj?}JZvIveN=s^0y1 zE#1cu_wK#MS+Js03#!Ix7|)2JF1$@ZAjn&_Rq9ywosBg2AaQ918xeU={QN7bOK!kG zTQTz}qD;#7{akf4Tk&{N=0VH)P^R8Lwi&?RY~A|vlwGy68z?xJMnQQ8C?$j{6}6?< z=t#<)h}_sJhvnmr*5(P~Rb%+a z1B}b+6Zk26*DAgkG%lF>?TzP^`eCUrADe9ppTZ znqaK`}XM{=Es%DuC)wdbRsA zl#fVhe@Qk*B)7W^G#n@b%Zld#b(BhW4UV`i{&___09XdbF-YzkbPZmLV184x(B1X> z{XSP^H=}lDM36^)8`4V<#>3+2&r_(`Wrlrr`u`R=Ad1BQt9<_77dL~BkcRC}EFJ}f zdzgH-$V)R|aL%%#rzT=riyb`nD~Q5JsBC(q25(E4m)4N;G84;v6ps(1#R@5i-(MYk zl{}q`cj;OD0oQ$?+79RZ|kpAW?1Sv_b+7Zd~knY8XH_HoI{m0CO)5Ts*5JA{D zGkuTMU-#im@cW52h6}XtxK0kOU0J0y4Cp1#`vJAPZsSu&XxVHRDAZdkNOzuw8 zWfjlFveX)kXX`9ALr$fUO5s&|B1S~bQqaI`JC>=^(#ITCC5YJTF9Wug{6$KEGi6z% zU|YGxDfe`nZ0_M8GonSZO9sfI;xE5p)JwWj_pnmGe!qaN@mJ>yhKA$?QmM!SF&l=#rl$t}nL$Efu0<0dV zuPrG@V@x!8T$bGD<#$C`1MD7YP#!*8_KTH~?)?{NK1vF;5uu)x42(Tgf#hE$8o74q zI1y9CU`lABD}&+vgKkaLdwkaG&sUZxAI%?c*4|6A4cM0) z5PY^@X7XB9Vjc7DfRj3LAozq=$|m)_F_mS|g(ClOn;e@PkKuF6EJ@FQMYBtsBcL~g z-5~Z;ycphI^^IRGEjD;+lPtpttZM^8>*gDbY0X!Q$VW?CwwNuoA3Dy^n=^vv%n==A z(E~QcMee0BoaKPJ28911QkJQ!c=iCU!C#+e$d>s*jl zv6UgePg}-F2m|dYkAnqfFeliF?d&-t(j_G6qWz>!255_vV}3*y$t&sksZ?DO zgIV4tj$}7!rjW2j3xi39&X4h=0XZ)tmxk705kcfM{HuA?Pg&?~UsppIeQ6uwR-8EN z36i}hE_Hq|meuBJ`&H8}vL!OHx@7-4j!kmD{%8&0}G$SN*D=O?JfU z5_n?MNv~4)==E;@N$KG>7e2 z0OVu?{`E?sF#fQQ35heeQkPAycFs8YCv$k^;{8TK5=6%QGm?s||4d}7z{v8I+&W_{ zZ{doaWfh9d;M-5%&`48iZe(qP(f!BMDdaw4>CYAH!(YI-VvZ=zmr2a9aX?R!pa98@ zj$z0>1b@+ipo#_2pa3^sXZvrM+OPff$bqvyxXG=r5UmC^zEYihV@}HyoUmPE+SJ=o z#f;Rp(T{qxz*ZB4C6mnL0Z_J$xzFzY-zv9l;7OI-p&`$G73MP^LRV7m6kP;k#H0^C z|ItP}j*H?Fa_-FD!o{TCLHbD2)!ne-Rcbn3$f{5Nc0JPTn)PMj4)*73Z4(lL}TDD_EC(dRyPTdR_p$pEdOvrvjX_;KitW3nxP0Dk4*qNx%6)3d-A5hcY`?W0D5d zwk~R&jT8wCg9QGa(Ng1!307gx!Wlm{)mivu0$Ak9J`f=MdDz*Sv6=GtDA!m^AgAEpaOS^Y@`iFd7j6` z&qbd6+dBfhP;5H652A&GPw&5-uDjMHJV^^XPGkC5*%TUBe{uJ%<%Jnn$IUfB&!?r6 zPd@aDk#*hyZ3wd7Cuq4QJL%Qg@eTbKiXq#s2_Jv%*s}F z%EeE1(C)wfW4d#ABXIBrTme5Kw*eYKP$YAlLF-!Bp7SHxqRVGSA2j}sNK9eT9pb9Q zwYZf8TC`fcFpHPXG)GoBR{bw1I~hIFG@B#G&I9m`f&qqx=Zz#61uTq4$Wq)98q0lN zq??O*tZ)B$dk}VDqpd{rZt7gzlMvRyH5nih=Y;%TFiBsyI3eJLjO5-qwFvk-hIrWZ z$lYv>53(AmWiFG<+kaGYkGh24JVdU0<@df|J+TZkVD2AH*owKkfU@vPUv9qGS|Vnf z2h$iBU#(vJL9EgvqetIL8K1HtVBF478U__)*?jRA!6I_oJh5}WS2#G&&0k4lhQpES z-P@`gK)qk9|FCd%73PS~>jz(Tv^4@P6lL8IJh(wJREW4c_H_}mzLS~7JS{BQsKt7^#wm6*8@3zI!uEDXN zC%z>?V2at?9%5a=vCapeH5)Aub7y}h<>@^*bsi(TKPa#fWOT(X6TuM4jUs05DQ{+_ z*3wy5bf;3@&B zUZIh9EN~a9rP)7jx3w=`CrM3S<26Iz>4-AM8Kto-I{Cxf8P}ej;A{oiYc-N3uvdy1 z&$|;KWL#_N94N$b1|Opk8bM+FZ0qfY8QY2m;D3NUZm(`BqN3TrsSWjOy;C%BoZaz7 z9PidSgrW zyn4Er);kax?OWj!K-Qh;Ih3h0j6c$M%tyulhw6tL%)`)wJ-&JM;ogKu-y7A_8;z!< z?=Kpccc)x*KiM8GKPEo~208qlB98(5F{Sv%P~DiF_7Ml8qgKiSaR#ENYm^acKPsN_ zJoSazf6q$W5t`3fw4nkuH|(s^g@&^ zlM}JcFvR7~q@Sm5NZQ*?@qF!d% zk(b1L(MZFnNf`L`V~TVyjoDVP#)b2DeQ$JxpfI(-p2Yl%DV7#}hlv#dW6a{2-Ueo8 z7tNNL*#Y=VN+oR|uD;!46Wq2_nM>d=BR%74-MRp2-WHA%o|%97NOw}b_;Q7Bwk?6g z|9p{}9^lTsL7Pu(RVcCc7?|L29_hZVj4i{b4>jZmpNH;yK6`9#4YGnUaf>&9AssZK z)###%Pxf9HQPnn=BQ$bHjSKV**G!pFlMrB@w}Q4kU-)}{4YvN(JC7+Y!jJ@0kWhmn zI|-WiZZSn&?9d>+Y%{McYR$X15Jg*x1cD`tGq$Np^3?Hdh8#D%-y4Dudy!*?5w z)wh;1HWGd~UCsWU$%{<_ZE9%(N3ThO!J9}5r{3N(gVv5mxE3qEe_aGk_3rN|)m??L~V{QkBXriO+9@An?1*@vp|Aoy;HcI z?4GcGV_@#o72m2+1J8nz$VdFRC{)Lu8>NCPt3{Cbe+&6t*wj@~Sl~zJ*Cpo`N1;_8 zq(IM;F=*>m-@ar4n1-b2b_eE5wK-HX)hUnz!o~{%CFuw8%4wCBs`=0Pi zP0|UcxctFyJ$x)1efQR)y*HTnqL}~J6$e4&fNjsgv~qZ_mB9G@hv!>(1YfkhTbT&s zenM?0;?;}~9Bbj?u4r42JSaV~X2#Z^IyyhxYJZkH_!(~zhMeA!lGpjgD`CSAwC#IH*iPKE z{W`2pf=j!e!M+aRYzb-XJcQ}Ck9faO>x!)-kNNl18Ag)eOQ$I%9_}-%`z`D zrY<_7V7Ng{r1R_0O`RKKl?Ne$73n0@XB>6Wsa8rKik(xY*DA;@bUg;oGaq`a*%$7{ zI^=QR>0X|Ac#qV=Omr3`7;@#+710k+*!A8L&x)YgxSx z5B0nES$VA}PYB`0oI=FOdaeX1KE`nv2q&*RJoVo!n8ZXWkMoC((fkY5+A&Gd=iu8I zBMoH!&^{MTK;C0SRn(Hl^hG!y!LKG#VY0V#qOoOtS@0qNYq1a0vI+xpHnPca$|nBn zM%0T@ugzJbPrx)}vu&-eeurd&JoT&PA*vfjgefP8Iplbu50zrywViaxMA(9Ch>5=M zkjwa^%qW4pL_&? zYrDAFR_MdjH+<^cFh`!vyM>FAm+y`lIi!yCDBYZq9Q-|GVJ2bY&MGL+yY-nX0uQ}z zorTBhgm|Ah45I=!3OUBrSyF=V4g}D+|6pKB_U58)21huL@DX~DG5euKJ%?Jwm3FKL zq-{P0UOZYaa-mrAVyX%MsTgZn(_v|87w91VXvY*HZfe311=SbHAU@f{NB{>5cW#~j zml;IA`iMF)qv`zA63N;IFUUI2(#=ur>3I)e#{FgoUsc zmA}#dYGmv@ktSA#*t!*AS|^6CT}35t_}OYNX6v!?s{hi<tk@Tfn73fJYXyB4J!-;(&)s^P>4SBCH21EojO=L4pk8_$o-5xhkq|7zCg&4x zJhFvmSO12@!)M<3lx>riC+_&5=CC*?`+Z)EQH)zC+hWy{V*F(F%0S>7G9Z0p+$F=; zIrGXeegPB=RqXj(_{0}9VdC8f8pr&*UySMe5Xp$R)akc5?{!J?WX1QFpDRpAv427X z!>!AcsBFx9r={?4JL;E}H;r>w5P`J_w&o(-RcAPlVekDNYrzGUBTo%4=sFY}SDe%R zjYF^=y=ggf_yL`s#EjfMI8V;xcjORX&@@#h6w9C7CG=`2mVYGA5IrC|Z~K0aTAOQ+ zbBKc@&m=}Oxb&&#f|B=%=p@BG{n#dKvL3!x>=O4Vo`5$<+GFwdzZ3PB@B1vrsCJkk z4%j9Y=TyurFL3jO0TDZ)ew&d!l_Ux1p|2vFr+@8oHXeFBZhNWzhqtqOitCHMe7+$- z2myjS1a}&z@g!LA;O;a8*EEfX;2zu|xNBnxPNR(lf;29T)42Qao2q%3KVatV)_K|I z+`3Y=_gbH|L+u`fEZB6euet0>)OB{POzs#}ZRn|%GE1TJ6p`}W_HpR;skK}5i?z8@ zwa%6Jei13Y3UnQnZaOnbYoVPt4wgL?Uq9tSH*bA+eK}+~Mw@(rWhjVwe&r26$$61B ziXPXp0NSoGfZgq>OXoyI<{rA{{CSmvNjJ%LA1I6i51w?s`$`O0PNUAPVv|BqZ zSJb@64-q)j$#_&X9>Ke+-Hz!}ZXJo(C|9&Uqu0GRe`alj^6)s@v8{9OLe4s=g2gZb zHX)TKFouP}yW z)@jt)QNzus6V=B5Bzt_lsDMs?2jcxC#v%^>iBS9S3ZVIzi~0VhJgj1{Da_v= z{bysrBJ}|iCg{XX6-GS-pSc+77mAd_?78<`t3rtYpS0e2jAn%)e^wlKJGa=Vn2bqz z;X4IjQ);55}hj*bktw)UVSU_R#%N+~lg2WaKS_YLX2XlP@CiqszU5 zUbzGM7{Hs>#N~7oAw~1YLpp4Dhk{t-=@u7K|A8qMFdf>*4O?HRaCr=2Y3T5=1CT3hLuQrLB!N4X7wp?4bil^XAZRK>x*j?mGu- z39x7SE$TR@b~+t{D)kOs>5tF`L{P9s&T$BPM(yanAJ|@jz0LCjR%^G>`ZS{5C%%yP`%z0+hfP`kX@`el%0YeRqs`l;&6XK=G(9=DJ$=y6j9;%^?>KPW zF?g~XT8WH2yopT{2|Q~RRk6YY{h%@7VKyb(P92g6d04}09guNBO?k7o`bzjpLMR5H zhdrXEW>UHyU)yVGmrMB*_nATqV0MWTwd>3JSpn~n4q$)&ud|qk0Lj!drQ*y1gJkqd zq)5v?x5I-O-FDo`r?9D_#5@r3`85VhoC*BG_mtDp25d40_IYD$QnPxE%Ks{4hm}Rz znSh)}w?t_ASLkhyQwWWxH#%6iesf3@Yv2qiL7J_CuX&fet5e&27#8N73GW2=t-;EF zb-;hwAr;e@r(=|Jiglq-_T zZ2vd4)j&gxjXa4+Z%58+fUeX?9ZW}WK_n0vl_QUEhQ-x~(0r`H9RQjv-2*fE@35NG ziP};(i3Xa*dMMpjH%4uMPjQ=^K4<4%O+&524ijOzdgEBSbI^*l5gp-ocp;f~E9*e} z=~jonvE!ZwAF8_m|1SUb^PdNkd-$13;?~>TVa_?-t8U{WD+N>7VCx$q@Rh??ckjNF z2ceodiR9&ad?|C=)@KaJ2+lrwz&h~yjf225*mi;R86(``zSz=@*R;R5f=cj>WY1*@ zZ4^Whf({UaeLPoaWCwQRH;S~t8A*wOZF;_X+(&%`*BZA&UsUaCcqNr->`(qJthEVF z7Rmxh?X<*cUGBe4o3n4hw>|V!vuqkkj@M-Vou-jUDZpAn7D&W|+9bzOq~K-dO)a3c z)J*%*5d&MS^`%E$*HpEjNWgLNJj$HX;RO5exEyvfFb`uM{?+t``|-6{L)Gv_F(7O} zwJYY|o@C~V@r$wXtJkU*lp`lM!qRhvA7Ka$zz@AvzHU@( z(mSvo>gv|};0IIM)%(cJ4drkFz-ErL z!x$wNYNl9zjzZcj=Z71!aYg|@F3F;Av?XZP6)X)J(J{8;GrA*)J&46!2-;(DZ4ZK( zLk8Tb!WerFhPhpR>0rc7UEUr$Gp0TMfNJ+g_1R2^3VoL^*@jdByodGFC5x6U0U@`& zcT`7FpRG0BB&}eEk`~*lemza?4@nPeSv}QPVa+*RJ=CV|{uOIq=B`7LQ2P(ssN~(O<9I`2glX|bTk!MTM=7Oa2ss6D*~p- z2$48qv{^1bA$aLt4;K|~+VK)0WEoYtK?G&je;RX9JgdGu1$pX9hk<8(B!FY{9V|W& zM?ll;=?CyYQb%QgmxqV509QD2%8g;JabZ)rd`gO6far!}753F5xTf8iZ~JcZt>v8_ zY)-fgAJrVKsn6gcy3q71t1j^ttyylsE4MKUm~-yWdk6U{;N#k*c=@O*lE^jMw+GE> z-)~6HPQl@3_|`Zbs{nh!sI_6F5jh`$=e_o09r;mLzB&TVnN|W=stMJIIC3 zwrL=S-1BmuIAZ@D1vz5>V(5IR70!AZI87P-3f`f;qoTg6xeNFpX8J!{OZR)izN1It zUGCtu)DEh}iea$*sU#H}jCX7xSA|3czOcKtUb`>A<5fC@e-Z&MuP5XhlW2WcA=?=` zbW~4j#NqCaUHp#w)PW9nH&UzV#jq*uA1nQV`zU|alcv72v$Fa^^cH|1?7BEz_}kq% z#3lk0iLw0BMrjC8qrFr&_c*TUtd<(^8^*C-q6w`X)$E5}_)c^`g>Ao$#S9rS_mq=^ zu3hDs?ti0NF`^lsS*T5JKXa$`vHg|xF=eYgCdNiARRX3;@dD}8qvSK@?*Dsl=enjw zF6`-^^vJ!#)8FGfpyBDi0ccJ>>}f2|EyaOCs_c=!X?tY5@)Dig@fs?~q0;rs zb)#yN-7cLoW}9A#jyT&%q$5FL#kDs4VbHO=)ul5{5fcjDOo?VRA&RCSNYzA3gm04` zSpA9G!LG-D=J4brc#4`5o=aodY|=RxTXp~AVdo~n?_9|>>5ddMfvA<9L)_6-qlWN1 z93CI1^=5Si5&N*)`ov{53wtf2F43lBsDftaqto+k$k=Xe%<{|JddZDMF)oSzdLbt2 z=YBaDXoBi<&=Hb8+jSV~9YvjecSH?-uW*<^nj#8R+=>(T7g}IpG49UYI_}hi#c$u( zSPJ*oUinj78FS9g{X?G!Y_q2Ic@J*1;W43~@>F`4=;yl34~<@G5;kjIq3Az6G3*a5$+Ggf zdK$DILB`9@)I9yB@p)K!-z2|1=g^eWideB9f5{Jigg(o`LK> z4=l5)YuariwUxZfd@koFr2&nilNDpmbKcoUQ^_X}u_b#7CC$smrh7c4a?)jINcYM{ zW2hWo)1?&5AE#L$rLJg-TgWTchLC&~JKT!2`hF0+yD($AMJnm`z1^k1sw{fJ7YDTI zn0_fD&}3>yG~sdQ0<$|R#%`^*6B#fxNPGAq*k9K&uN4PwD>c!GTi9-DEjjdHEjgH^ z*|9_gAY>OD|Eg9%sn!o38Vtd)s<_~bWIAw(3_xPp3G`5RJ@#>luRM*}Wi+%lXJ&$G`GJ8_K^r;o5 zXzN$AbFfpbGNJfdeo5gV-|x%mUWhE(x3&lNhYA>bYa1@;t5FK}vl}AM`e?_zc_?iU zfp9IgRQvYZ(l+d4zj=GYr5yCo0w&7 zez|F|_8cGw*hZoNx4sp!tsAt`9k-2=Cd>#h@pQHi)^3QZrEgzWVgi~Tl;g~1T z&UwOrPDEt0&c0N+S&XZHFBQjgpiiz4W^z#sV|va=>6ETCHu1p>i<)O)h|^TLNKaVs zZ_^BxTq4_>>St@$#Pb1QN)0eBEqyLY@dW--{zm~!@nGHe=&mct`6`77cZt>!-cz9( zd!|UwM0xgqZcTfJJl(yBqPM@2?VKrJ%%>;gX}0K!#^ME2lJQ#hN0akgkEFROr#)Zu zM3TE_{?D34m%tAZpQncz;q8%}_FTV9$*aX}$t%f&tLgh&;|h(M=5igT4^v z;Y@p;`tb&JEN;OkT`gr}nsI0Ir;_)hJ{1D8r=ScJ^B*NzFPh(E_V1Z(_czsUtp&pl z?RgQhvg#BH?gQx34!VsaX9|`W0~c0FFgq`S}tqv)KlH`i$nS z<~cK+gz?o2D`(KV* z6izuowM8*Z1x2ko$~AUIDN$_XA;wa0o6joIaBrqzm4ceAUm+aO7(t7$MdeNlqa-l& z3jSVD$B|J=)?X+hq`dI62IZv|jdLMc^OVS*!m^BHuQxe*;gE(`@nE_P-Y)+*SE^M^ zua;xty?UIc#5rGzGU^9CM+}o^!OuUM`d_DbL&>FktqaO%@{Cd!lt{c1rc@z&lnwF; z(uTcnfXX;AT$YH4bm2hI00g5P~!Vx@REH-1)!thIkeQjU#d_ zH)@SSDCb=2VHoAuXaAMY!@W2k?wuogvY!Z9AcLn@!x@V;Q~%})I=P=9DU zHy*OFWjAp%b!=n$lvDP$uAvd4=WSc$(dw$nUD6U2ddnuMWal(BQ=ykbVZ5m{uLtU= z(?O*KUF`RZ*aGfK&$XLNaW;uIY@*U?taXB?Qi|Y-(JcMc-%`9YniJycCFMTa{?H1< z5{Y`9+;N=c#x|p;{O|Fn)K06GaC=f;P_}x@q|B*6WZ7L(a_A_TxiZm0^w{kY9L+0S zOD{I*Pxz->or6{n)hO#BgQV~^#~V$O{>T$n8qQgr(9@B1u}rucboLe#oanq==PSZd zAXMMGKZ=myikgsD)kM1{(J`sYZ`AR(aS~4yGDy;4VO1(J?U%nP(^4QWzZ(YW^XzvE z>O^dl#<_6(Dq%9VE74uuzBrk8FKW~J*jk91q!?V963ws3gHtx1Sk&OZzl0CO9DPdc zu)omO{!^x#O7j^#solUtfj>{qHb4FyLN3@^1clpBf?Wr^G z`;F?Bb}Gg-mxmabTE#UfSmFDvjW18R+AgkSG8tx$!T0F<hEpN}H|Y4TLOf?^PdSp1SQy1sj{-4k;1afBPd9_Z&dKdpSuq zi?lS!t8`N;>cN$y&9(4FLvegQ?8dhEKl_{4On!Bj+9!#qc{EVxj1f%pd7fXmCjFL}ixfdgFaz>Yl~xX8Qp#_|m`aZZ{z?puOlrC{&{j9O z#*Fx4)I?mk>7O{hkA37VI$9Mqxd{T(0|fMw5FE#RP`$V*xwWnHkQ4c@4w?yvuAyM!EYyMn@}jcx$!rgz!C~%^qi)p7$|f{Wlm}@6=1+UlR>Arb$S;X{*kJv zBJm}N6=TJ}DfhNi-^dI8HwueI$);|a zY^iG%G@4ChkNvEcjR?o+$yD~aqzDac5ycFaXTVOwmm9XrTmi>Gy#cY)*-BcDk?j*F zHy_ZeD}pRFq%H#Q0n?}V>Nwk%30o^ukILIeNa>8Pe<*kALr3zoHs#lYB-X95_D0wI zCW@-cpDj#rrz+(oGCRbZqLmE$F)Qg>$)qG@pUm4iZ8fBpQcY(q1%;g z6mC0GZd*_NKUu~&JVY0z{>rSOoqU?cRr?unGjdXNAPptMKatICDMCYh(k7*En>}hF z1{k)<=PS|MZod)?&;J5WZZs$hs!aYRIE^G6*$*dg_S5#zUCn65piMGeYVMYbPQADx ziFFq1a}!KGRSB*YK2>IzgA~e@r{2643A_+~qhwhKNBvIHJdgK3c5suOF&de<9JIZq z)1JUKbmI%_Ng~lODfr>76TuIsw2Z!>UrsBp(T-&ARHEMaJt;R~AXS!~B4v*~N9vfT zH!gM}?C|1j3)@TTcMV_UHX(cN*a>c8_cbBSINmAQj3Q1w)vWZ~C$45*0E28T-NcH`KoS7ZB*W71D#FG$@ zqas6#XLIpYl7SsD=b9aB^VchV+&)7`vkk5BJr-GJh7bD1k9n>3eT_MPq~ZA~jpsy1 zUVe`Co1R{E1WvnOhpfqqwI|SG1b!E3dVvU#4_DWpHe-ha=ck;XxPM z{U*EOcSQ}Fq~J%rzCo;7WlV-n{zB)p+Wp5}n77DV%oA-HkL&dx^_WkESpnkMhF zNvJO}4Y*`ywxP}j4TwRpV-KO9`ZolUpJ}S2x6oxUBokN6f$FPezO+)^v$ZsKn!~p3 zc!tHB{5pfC)1l(A-!u&Evy;j=DQk*V-f>NKgK7x$HEDno;1R8wX0cGz=Tm44(}MY44CT}jN?C*2(K^dc$^e821r z+1s+&h0w$;gxdypdjxL z<7LTHp*vwk;&!r1fRjMsnPNTqO=9?KB^D);;m=j&%YUyKJ=G&4@NaQCl=sX${_X2Y zQVeX8&f|h&@L9%&7%p`CX?9;FP6_RyH>UWS=rbM>8fsUZ>Ytm@Zg}+@Ba1{8+WT~e zQs+LvkvcJKgUqhONC|zmzn@iHY}@IbzJNt==Gk%U_ZCcN{@c zY7%ALa;4OGzuj@dx>|hpMWb&5{RP2&p1)j0*-Xl3I0S|ddYZ0|*msZS4_182&N zK~H6JJUmNI7Zm2L2q;RP>EFd}TdTl_?OR8tRLOYIOll;TcyOnUbULYN_rT!4 zWY@OfLHPEmyt1IxVL`s58sp2yFzerOlYdaj?Us(v5$Q4E`R_1O-2^e0jsa;SSU|$Hc89^&(8zbt= zLn0&ONN*~eJ{$3PN0lpP#H+wrwExwVnj~aSXxK4ha(p_f5S(b+2(zd;ED2Mx6li5q zr|uuNtI_+$B;#lkt#!1w9WqT;NTp490XL9!45a}iYd&zH_3AFmMKy4y>arDZW^<); z@;7F9|BN>zFTRely_N1zc*Bda`;E&qn(-zPEUj;YA{O4zzooPc^Y~e2$A1xOAT60c z5|0eT%oIUmO#_~Y_bqs-S$VD3OjSiKKc1uyN`b2!__tK z483$8<8f@}ZrCMdO$wv8H)T{MbET~Ro6^K`;z>scE^HtT7+g=~vrAU8qtNk>-r zcFt9)d{LX5-Sb+!GVR5ciCRk3eTFn;oYQ_Ae;mjtd)#@&036F$&NC~g(gua3Yk-Sw z1LwvLfsghTinkUb&>3ABa>qD9RLM8ZFH7~dRfk+@+gxRnPcIln>F z-=J=^O(FBj=9{1FOMunOVE;8FLr>;a)aEq}7!g)7jyCs}7}iDB-AnrvRtm>PiJiJV zRN=Ewn}t#oE^Af{-zCSNsz3V&w3Z{eM^yA37KLca3#M9qD}3W{gcX&R3NuiZS_W^E zh}O6b9QMnYUx9#XYU*{(n^^*_L)XCOxc*@bai$^HoTt&d)qhqDkdL& ziDc(q35wkBV4DSw+49yBpX)3c6#o`Vke;aH~aRQmFreg+ll+^PxI zB*AtVLxz@;7jN~dDs<%h*<*Ju(Jg?Y@z%R=uV=SZR_m7jKtW#`d517R(XEWec!f(z!kwo{eDMs6aAz^t`Sm_7tKAuoXq&RuB{{&n$^7M;*K0}fnuRJHVF{;XF7i<# zroje*svfnt{?=?2}MfEDB6~+_#5jWeLCxb;;!{t^h!e^F68Sp zYFF@%@9cZ(GqyKp^oLrPRa&qA&>j8*r`5p{P1M6H4!~)%nkvWUa=eU@Mf-&${~~qLE6BoZT{|9b zI;pdvM3+-1iL*~;P!Z_OvP}i?DP!HUWuR$W7QSbAifT5ef#KFHI%n$evsAyvLpT}V zPd#_z|6905Z!Ed;19u7KlR;@3HZ6}f#7uD)c(793dr5>@J;wGYTaqGlh)&YLAsQdXY4Ur<=s z#cR6^i2r`bf4<&~qjE~qxgm;9%v z$jV6yv~X5k35C~Zd*!GiL23(A+KXrBHitmR1%s=#!Ae851(}GNx5QO@=0u)rnPzt) zSqsC=h?#|!t6#4wpoJ$aTkRa(12-qg7v@ht%_>$j5zySasvX8Qj_kUqdK-t%QS(dw zCMJmKw`KB$+yX~pu|p|BPTz3^i4H->${0cmpFroK(s6A6<;a;sX{pzYkh{7lg(nAg z+1)XxTqnKwxNP{#Z3jxo-2AqKrq`U)wu8j)s}1*w{8f?ruE6czWad&ftu!5qi1CHU zMgS{3r#Z&9|D6$cOB@5MQM6Bv+B?DV1@6^{;g?M><+BUkQnR8+9c)6m)<6Kn=}hAB zN}zoD5&FUqPLJgF53X%`^A*?b8N3w~9DQLzKjEkqyGgThYp{~kG2?YPWCHB+SyN{< z-?2yw{M^i^AM%`#UGP(f+TP0=+8_GuTl@@$yC!&6D|IKZguYC5KubG1RchL7+p2}7 zI>4BYFjIRN$9owMGr!u`nLrE)h9dkQCGiB40zk!a~4S)`hj-NyAN#2{RBc0n-YZ-4ARPd5Q`2DtRF~T8!$uUy_tQ* zCgzbUhv=kX_^sN6O9-bA@(R9;G6$*p6C#K9$dK|2pKvx4+V1#VqDZ9#GH`~KJrf;R_$F*gHws9fJgfA8iY2<>7GHx^`~xTVZtO%cm8J9E-W9 z-S0x)x8roUpeHJeivZKhIM*DyVmgw?T+0hFTxYxc+MckS^dSVtH1&rL)+}scA9(RfdTR*Awk8a(;o51m? z_AO$aqfXJb{+1`}*Q2-2$bgwAVg|IDC+D{T7V}Rt?|z(QOY?-jA39U{W#wndkx+FH zH@e<=E3u*{I4(R^-idKu^n`?pL~K)v?LATarCmM3D0-R?l}hU;bRjh(sfn;hy;yv; z54X^JBi229;-616+k$iTVk;bXF^<;ESs$AuSiaA%mtN=f>|Q%WLHre>mmup(%;kW( z4|)=KPp)Jo<|PZpz^n9uVxzcdIvJsqEe4N$@@H$D3EKF9D0CXrJB49Ae94avtJ~La zPhQ1M-WRe!@U4VR2T#qosd@XI9dN~Z`s=;#u_?naAsK~H_4(l&p_oqlUUU;0RKyUE zu9>Ku>-dZhi@{TcuV?~i%gBZSk#&_8xxm=^47@SZPS?pWY(mii3!seTv#(jUBVdUtV;YpGk5?o}0EP+Hr0u2NA=lC1Gy>ezr zIkWM_aenWnt&!J2}uX&sruLT?YmRXlF_2T_tuncf7GI413gX{|kH&K;e^ z!_QwFD28!3P2Ixhdr!%fI63zP8Hr1L=k03+4m~!SkdlS%8;3%h_mgKVaUfip(Nic# z4H=B$KEppn)@)P~8UgW>`3PnY&mUBEwQ3CiN*5ck4$NcL2PYs>)myk`ilHyRw%Z+bO{)fL5?K8Jxfb88kNSwt1LCP?kY9r_&ZGrc{nHZ}`$-c(gqhOAFx!*= z8^|3QVd_w@!$Fak98&up8+mg2@-ROle+2D9z9sXVGUZ#M8u4@XIt~QAzHX3vRC+iV z+1eY$+(Yjc&NdiJ0Q)C7BJVavClYd;(ksxRB?AqkSMWem{uWC>25q!$V0~7_+X@rz z@vaL(C6~r+7=gIz(q?^Q(qcA@daAQ}d{%QRL$7cTPlw5n#JDf*Pl`?6b@-CjQH-ms zL$DQ9UoL4>3@phqgXQjHm$vc-!(~5dp87y zZlh>A`h+C|Pj!biFDVqJrR{pwp;rXQCGqiO&`vR3&j$r0OA| z{ci0O&SmyGmN&p`#g^XfGxo$H%(Y=54Ap`S;KiA9B@F3PtNU`TxE-Ied`-1-(&0@q&vok z3h37NDC`ou{Bt7wSvk&(t8%fxjv;|X6 z6!%B4HJw-nFi5y;hW^fT6!lBkTqh3UvUq=) zsnhe$X;OQGIn~&W$oz0U3;6xA5_VLMrW={JPPamySA4hilf?3gydN{-VNeMikL`H! z1ws$#?6ug~%&#r5i6T))V9nHGLg-omv4J#$O12wJ)KSeI=F{;h|6Q{?%WyFw34~~+ zcF|VCjcZoFRStX7uuDcn4=VA2GgSr^`K^k`OlrerMll{!jipWgv zVQ8ZeB{sF<2cPio5Ccv&KMz|J_EG!oVS9-7&7tvg$#z)SI0w$+=ezd$g*Chiw?Npi zuhZH8H=)J&OK|Ci)`G{NC1x-f)`hddB%IOdVa~XffEbV{n%A4+7Z`I9n3T-Ae`TJk z51&OIixHJ!5qKSU&V5$lR)1BdA3A092DIz1)a&c8U5Ao1L8s|zwvgMbtGTq!z0pVU zX6vc6{SXg#wBGFrJ>fQsx=QS@piB>v*kYbhkp^6wha??dg-i9acbHWjPn8qo;5@}9 z{*1XtTbABqZL5W4bvYdWLMt2Ua*#J!FY+Wu2obg=t6FFuZ>xjxR_z?^>`fiDf3&7(3c2F{oFzMZFw`7re!WOT>1Vx5LV#Q(zu;Ui z58s|+5)V$yIWx9MY8j6^o-1Wx^=#kLb_$0widEIb^qPYR8|)&9ir6RAxY zwwbcFyZb~Z)&Z@GxatW8ebyN3q`;Tskk%sX%WBuW&k8($$gpG6+=E#(5~u+#zBVE^ zqLO6e(YLPMKC7)t_q<2Zem$v_;;+YJal=E}C_DQVk0l*m8@PreBC2jE4ExW_kCsP? zsOsEKVo~&fu7Op~D<+ecs@TM)!yRH1Uj2=}M^EadQBnuy3YU2j&Wu$~UPhSM6?G*} zCksh|d-O-u^`D79fL|Ya?WsX7V}DgJaGs9<9f9e-0FcTGL*_$BFNq=?xNZl=A$4uVFI}M$g8smv)5B^V*{sXBi>@3 z;jh1IvZIYR<@{g}xgMZw5Yk}OU#Z0KQ7Qf~8^vuhLNn%h?C@C$y-=25ho-f`^5SQz zOV(%yX)>e|7{z>*7>uCXVgPu`+86%DGqD|vg+7!(LO>;u*!{p~wpReE7Ok#10H)+4 zQOgaik)IuP*~-=@Lq(*8xepMb!W7`5hv8+DX?*OnEzM;lKVYIhM=#$U4ZLq z$6qJERmr7i8s$m$!KsVd?xx$PZ#t#&6$1y7MJ3$QQ1#zKYwcbzN6*BWuPmP{SQB;xk`zS&d4At*ytmvwoc^7fM#hF$n$4q z{B`VH{7=Qt;pouANsMv-uvIEI-aA%ydRi>t>;(+%ehFxKoJg~(XFy;R;Vi4Fu3fku ze@C-XfPFea*lI)6w0m%mX=1q>Ab%-?j;gN?%ub#5aR`}p7y2cv0Kq)8W!MGm?jrAA&_8zjjp}J` zE#aV|2vzlBpT7fE&C_&Ul8o>_tfduj8~*XT2wN^iGt;t=W;{2`PK92KbixB+497V_ z{nIXm*;h_|v$YUQQ?FI!UskaE$Bk{5DmP5ZE^#WMmpaG~5Hi|9dy;^iN%n^IuOD;%3(z z(Oj`BVK0#;=<7w%ugv!@J+)LKW#Q>5i14KiQQVNtqHjRmNK*MZt`j|(Rm||V!bqUQ zrL~BQ({{)~E7NV#>nxyupR0cXaLA(Q{8PHiX)PI>0zP#Jj?IB{t=(uJ2UQvFd#3jI z1T0@F?jd=LM1pmo(7gkYsDDaUx#CzNN-}PcFC>r(Z7O9L?uE!Avg45%X*^05=G3_ADP#^D}aC8 z(S`9$)_w@!*DyJlb?RZLztWW;bp@T07!J{SJW0qZ3B3*Dm)hl_j2({ueNW+JE8dOO zkZT3I6Iveh*1qgsG-RLZ-jxw%+*5D02*Ke7RDYD{yf3};I7XHQmUFqZUj|0dUDAsg0p1+V(r;@fPUq2=eqRxL) ztX3q*lY{mnBf^HZIvp za~=9xXvN1Zm$`OYn5Gw#gmBfAPiUfN5_Ub+!arJYMuxfe_-Kj~#sia9L$PWik{|=e zc-k5DxDW-`H$n}x`D81cv1?)E_k~oz_uTS|IeD4(bUnTHsL;HY;-sHR+lf zycXO2SRJ7W+_37yh2Oae-zUL^t@u07we*U92MExHEDc+VXK>I{oH}Ig4}H3T@|oR2pd}75{!x@vDP9)M~_DsQ>AEeH_`T`QklU z|2lRw>|N0A{?kDeP)hj5Fj`$7@4iH8OY8;A^hwBEKa&@@AWC*YtM2+X7x_i#%^7vL z>$v%EjxRZQAZ+#Rv*x3CoK4EwtY{a)z0l`54DaHnMReqNx3_bU?7Jr{=zGUyM?Q6_BxyEafos*S~0yz)FyX`a~>&Y zOvIs?q?GxK^=a0)UQz45@yf*>k7iyod{C)vuK$a+t>sretTv_paZ z;UL6^8!^bsoL-2Cxe)O6zyF%B_IGht82WPwtggPqNzuUUx|5I5EAiaB<&U4KEfto& z0f<+k3%}4)_5(L*dfmd_O9m!OU{&P%^1>yKB@F17gT{AVh}KVQ`HgU{EIUdq`quof z#o+GUsI<}; z4!=X!=ysIViS~~2I*8X7!=-uJ(NBDmQl-TyTm$roE<(&ClSqRH*8HQlLyj`hPsFE$ zgNxoz4*Cb_ntLg)(181{2Y4PWAn#l~uSrxtJbbc|xR1Owsij$OFCCA7u&-B!1I$3< zJvCp)`FI%M_T3Q9f!GuJcVToLkq}`A96--9Yi``_TOQ2yqO>C{Ao-aW#_W6WV|STk zDQ(aRpM@yqIt%O3%Yk2QLhv}cu{b^1MFLlbohX3!tIho{9W<&TiR#M~7tl42UQP8_ zDUpFT^H=LLtbnlH&~A}%!!EPvC)^7|e8cA{b+}qLZn$vXJ?45m835N69Wwtr_t*V9 z3+G-mbH5fEjGhd#PAn>PE*E}yMR)zNSTT~7HmZeieOND+caA~^GDwoQ}hc{ zGsl(}n_jCv(ZaYo!62R`em&w7as=i-!Q?uwy!%%(*a7MBZ0`aD-v&UV>-k1 zJz>84UHhd+m`C(u=rv{wHE}0d1C`xm-e!LmSnWe@SIh)ka5tUwOjuF-S%6SBvQUdZ zajZ2sM?Eh_oW8l>wZAcw*I{X6}>BAm{~J@8biWHI{N~W zTRr}-ukPwo0M{z&^x3jt#xo}Jcr$+E^Rl$E8PAs1#eyRJ?o2Q%(iip}@sAQ6NO#)PlSFmG;9Q!vF?p4Q1raY*E?; zE@P15G=t@&@;~rQ39wvIn(rp8>}W!cNM6}aS&qb6@z%jvj@es)rAk;?8n3w!=@_xBbz^*4V1}YTtXMO*0<#{p6s3t!Z;GombDJdZs(r=(i%*N zN)Avew}5>m<|m%ymCFK%(%GX`FXO+)H8j>(jh;a#|L1y`8OUu)tJsy^bC(9&`~fre z00G!}yJ)aQjdSst4Q^4(JmtTOd7emYZeAP(-fRzdU@~-h%?c&*?@V6qOIDLRBYCzX zQM%yFRKVQV$OZWrItJz#Ccm5v@LAL^h+dX$)Bban0nM9)#^S~)EF$M2n~eWe-gkdB zu|?tPRYXBVY#4e+lp>+mC_RLLh>CQP5TqM=32+tZ5J*A`Ez(7rNH2k_ln^?h_Y0wg zD!m3C_r15?5AXd6Z_W=hYu21S-#Po+XV041vo`@lo&7MtWd*&fB1HDVji;)nSRY%a zYb>nz7+u`9HB3Km+rd?j{UAjRp%Y2{ddo;8!(+h1dO#=ArWd!toUFBV#GiNY}GRJIf$^w>34o>TofBP9!oNsY^b$Ac{H%WdXd%o%?sL}x;z%FUOuOc zg{i0DP$`q#)J|fwPaJ<}NBt3+r9iB!X6BugKHlbdFUv%AqY?mdDg02^J_q|^rXrOw zFyKKlEmxjFNLlXmc|aP6dZ>KJLadXwl2nN@Iht8J6;VupBaiC27ngXKO9@W0nT|cNpTJSir$NbD@!?w7JDad z=hgn!sd7sMBq6QaCiY^an`oC|8Zj<#B=R$WeoBtAI@4VP-GEA;(K=unN$&?Y3X#87?_DoxBPKDU@72zTj50&m2*BiQaIFWAl;_R?c5fQ{MZB- zX|Qb*2H_C2wV=M;?o=#`9DNGnaFg>3#aOL3ngs6Fn~2vs=Pz8H7czk%$bnVB2wG>6D~HqY1v!VU zIN`%~sqcrYE#D1Gm^dabDY+zunM-z#C=09`kh_kvE>bVl?t>ucQ3){dYpB4!LHU`H zYdLaIy8QHG1(muACv3Gu-&OGXe<#^h zn}gIniQeReD=)jZ*YRE32P@*HC@U`@U&B-CsMW9C!)N8HPdkdv19D9Q1D}w9+_IEa z39!Fer_0hQU*dv*$>kvObY+SC3>Z!u_RZmUt~62I{btfA*B?zhlU~)FR_Qq^`x_&G zV$D%`JQ-c8C&+2Sid%^CPF=X*!-8)qT2!F5$ci>x6x4MyI%k+}gOa zNV(MuYg#o>k?Guv&B<=OW3Yq6B~(~#%Thw1-SQ$WyR&ZOe%elORdT`mw4* zb)+Vk_z^@tBZQb(W~Tz4xOdlVJSIzu^@#S)R^jtpv8f)SV){Q%62SG9874D=qZPe> zl{ZWx$Yo2sq{bXvB$@t0pb;Ae^~EcP(61xa$N`;`hbLB#JC0KcCipo-+<30eb267{ zV#`)nH2uCXCpSEQ;V1ElsVoQw>Gw_1oR0fnH?Q>>mI*SA9h{lpJN>5T>#H zro7=_>+*&Zy7JQ_2$?Hs=V?GwwQ5fGXHW!%$9`s{L$p+NFgmtdYW2d-Dee(4jW(E6 zW8&8JZV7)&GK)DmI^Y3%Urt!3YDvtV*2B@Wg)kRTcc{V2$MPW31d4T;$c?Ih-z=C2 zZ$V(Aal;uN_cpbJ@T=-`^17i{Y5H0@y}UMy;z9^>L2PL&#_27GeJn^Vv_io-G1eby zTFkAZ{cIz$84(q6sW`~v>*C(^o<6#oLal?Qluz!IWti2Uhj(nbXchDtp49rnVN>0nwNyDRu z?=!W>%~!!%4#jgQhtDtt6!2=O4sd>52Kav3f{dUEk{TM(@+J7324jh5iW?_!BnlGj zKLZx%IcQH7f`B<@z^j!3Fr9rVSj)5qP)I()1L^zBbTdF;-6kkyfp1e}$*tI+6U(bw z3EsT|)7ymS@T;2veIa(_W1(mb&~q(woNn9A45-ki<@2AztKgHlDVR=zI;?AX2ev({ zB+w&epfnQ9wDK$q&%=2(K$;t!=s0E!<p0E? z!R=h|YDy%`t+WKzvSyz{ag&Y+jH=j})(S-`>b1RU)DeoU(;inM$aS(`&G7D-+4_D1 zr(9l1{e2{GECCca&?j&0M4L2i1MLIi^~QzsFv}zD$y`nC^P_sKX*<=8u8?t74_@_C zKdj^|zs|0E9vw6G&0d`esP}-c*orRb4(%bJyoeiz43f4Ndm3DUhe&r0sA5>gb}ZfW z@uJr-PR{c7OyNB0c|EeLQcg0f=cCXpD}MeCOMaV@N#6b@LB*w{(cT!3*iLAh#=LMp z9=CAv8N6!-d$l(Ur&Zm6&DDs&Qam331WNgvhn;zg73N4acJw;CRIxqDH0cxqEdAY1W(Hz2=vZ9j8@)wwHMSDC;pOW9 z^rk$O-4hLS9>c<8+Tzk=mh?UH;v*$_IX}I_6<< z7n=CS=n1YbR-jH7(F5d=*Kk#?BDhIc=7`n1cWCSpXE7Q{)(Vi4|*SYEfg!1oqUfQ!{Am>UcPEHf3LI$D6o!?*-mv#Ji00Zrd)J%5Ja<`gwfV)6cp?I0VKM&b<)sVV=eVZ(3LMJ;5wDI?rDv31 zlX!-sqaim0`%BThVqtL``9!oK)0+$2I=hbWfVtt*&T@}}ft>_+xVoV7|e zsUpe0OMXo;V)ciA54Fz2@#*&jOri%)fdkBD5r@7-Xs>FG@XSuVn(39r`X z6riQ$$-?~MY~eNV2;vJ|a}oqPw0|g9<@xNv3Wm82C*>Fy>XCY=G>|QSaP|(y@c#EW zuMcF&#~Nc*SQQF~28D|k|l|4NcK4?p*=U_Tyh ziLlPuTqMmeajagLea^JRr<;>r*qcm#&$RMDf&>(Sho7xMIC!B{poHEiZ8br*>>kdq zlmUPr4%fSWoj*V#hUZFe4SJ;3Bjul>lEKYAYGgDua z`k*r4U910A%Tr@xpl^>5CSm4z+nF-tTG$j1zyFW?o}u(J z$ca;Q0Rp(BV>CdTp&pa^E3Lhlek+N%OaM0qZDs^6MU?6sm|3;YC%F{#ED%fgct9Yr zXNoB!e0vU_T{Sd$vV)aQp39heRukXAxQZbE<~(vE&1!A=FPKJaRt|GDwh?iq{cBz` zDaQ~E`_K%SU;53evug}nDYk=oFwU^{939^)d3Mp8l5#c9vYHKh#&3rHbrBTPI8ukP zcNE9eH1gSOFZfW?tbE+q$wAGpfG>Qt+PZc5n@tz}7dRw5%x>H{IBFl=eGKc%T!g(t z^}yn})Z0AdwdVZ7qrGYfc}M2m25s;s>ep0?Aae@WaPbF^0SYIo5oe!oTBhq?3pfY= zJWB+n??4{U=?hJY>(AAN-?W8-Ps)5@e=^?!8*I`{HW8=`SKkIslIzGGGpyamj7BVCsxRD}Bc}ngnbkie+K#aTns4f#U%p3g>-~?~Fj^b3W0ND!GK) zg8{DSWl+~K+Dd2lu50?LVLsdt=;b3wP9G7l5g*|_>7nX0f}Z~EF`*Q3F*Na1Ht^hW znBGOQmo!6RoCfGwp5@8&2a2;tJ+L)@{4yvh2u z18{Y-Ea!YQCdWV585XtUPqMAwM+*vb^Yy#NWvZ88d>;#=h37rUppFxQJK^yw34N5f zIc9L=5IxmV&C|=h_H08@30Fa{g)Yco2jkyt!7A~_B%+-q+S(I@-YEMgL5fLBZQKc;@E3ZF&ZWaS`YMTq<#T&`FI85<7k4Af4&CLW61l1B}*cA&SDAg2h|O!|V^$}67c zg*JiX8*U@@C9w3nDkOlJ9~$layS*hPSf~0YS|EcT4Yn_A56vz`Ld1H8PI za7z=2pM81W^Jfp4k0tY#Cj`Ncd4}_qPbls1Rv}DSg2?>@ zVX!hipSEj_{#+xQ!k7FwiK-@kvF;{x>kQ*Ynn9GM<@~UFZ99f;2ffO85?6Ds^@iO= zm_0vru93ai2Z}#I|gubKuMj(+1?6tTI}*QX9$&?;aiMGv6V){-gvyb@Qkf zghdre!utC6U`gNE+u^P#j9E$sPwA=EdOBUwZSC^V!n*JD-UqGe|JK#4&c$g4i~Xz# z;W3%y{EPS3)Rdq+ug>TCtKJ9CYBqPhH;)2a(pAC&^8D)xFJ7%B;<{h?O?#N5S2Ju9 z7t55Jrj$0x@@ELuFdK_oo~J)5VkuY|8xD#D$LgrhI5%puEyI0Hb}N)ZAWLwlH>)bT z9J2AJxSpS`CZl>Ya^0hglKZ#*kh@T>bq_plI-8G{wXL+F)L(`uS#pjJmKlVKqkK>w z^-OJ3_@{#u+Rh!5Yzh%X&bw#+zshvy1&MURrSd%;`+W$LQAXli<(%fhiSH9lS)wm{ zq|zd%X$PVuz5`{vG~&fETpMiDEF;BHYGuASy8fGm_yvGcnj<9JoL#Zw;GROUH@n&y z4820FcZMl6E>DsX9aXZZa=tu;!0%OL1oW6glIE9dx!~0xRZgIM)ivWx)k=3$o;Z!N z$q2CG9C@Y3-dXBhqj#D3rCCWqYkG5Sw5CIYC_7Z8Fh|2u>HxJiCwtqf8SQYRESLEuCmFoL{#oI z?Tf$$={`y0Q%w{>Rn+S=&AQO$ppO&9jxqn(<#pIdY5yX@DMdV8^Z5v$yBZk8(9}sC z^$&C*KCokyPo$OnwE6vZozqw-)&uFiEKF1`mR00R4;Ytp&tOHdxW5$IUFWheYu=*% zPCD{4^Z?L{Z|34xZhMX-2UFq)W(LK% z&1IBS^J4{Zs?osK-263ED=W$)4NjDw+oYgOSN^8ddvJ3KcXcO>y5=8+l2P3ZAP;2C zR;5ieZ!P9{=^}sa%HfCI@8>zL_Y4-7tZot(4-ZDQNHmXv7iJ7Ck!61VSifyt|oSv{CNj9(Kp^j|U-H`Y8FFR>I-jp!P2$+1b@ zP2!|~9fc!RkGT4q5z%t|0pxus#r)VeHby*VDW)qhuxX2qlep_hIrP;1Vv}FO$*GQF z`U6OC8}> z2@@uar?*-ry2XPGTL4Lx^2aFH!RkUT73KNewqrKGNnB|$xdDEX)<}TN z1|zkKk;snRP44xyiQbthr@G#6Fa4A1cguSdwhyUG233aw;*xXhAlmM*1`%;IzM$#n zx>(H^b(l8z)$6|+U7Gi^_~nyydsMx)aHDn(OKyvsRc6ZH?8SMlqRTulZlyQ7&Nx* z(>uOf%fD=gv|Gn-lJeJG>9HoJeHwS)^>r}kM#7W!A*XBLRPyK}`-1nc=%Psl90 zO_s>ca#V?YMY6C2-ly@uTnVLMmy zN7_5bwu4oQuw${BpLkn4p;{8UJEo3SZn~vmB!~Bg%;`=-?g~1-k}NTpus$i1PKaTU z3g?2RM+$>`pol|noZTDV3=&+SoAT|i9*NA$(qEpqYfJogqBz3Y`5uVo-*yQOzj6NJ zdS%Z0Wpze2?T~`1VYSl;4UUjBYxCPG^&NX38$w+uCkK`C4mQ)ISC40 zpR*S9Yg1bZeACRqq5siQ(u3N=t5Ga5@2;-L#thMK<%Nm}@TK$9~j5DA=eH$S+?w3?bz-MfNkKJU9c0`4W~b!4oftA0;a? zE}hkeu`$*}+{`_NbD*|9z~o2*D%tKe_#K1y<^8bzJO`7BzG3My!mq_2H?s5Z*na4b*D-V03!^1G-g@?_pXB85Y$;ohWNyQmsJ&-g;&WQa z%a*9kp~{dw8 zpSyK*gKaJY*y_oQ9N`jFL8>?l+-?!fF#{5|UT+Kt zdn<1tqgFpU+ILW?A8HAr-c9d0FcfDhw-?Mb?Sth#=mDuesxnuX@S502Ywr1%#ZBE< z2bx|0$~v_8IAdjE)OSK(B(x6Uxa0tCz59C0{orPn3jPO+a2y^~ko_6qMF%iFlM` z|KCXvgMk+-DN2K3<=cV)r`{cH_kID)875jgZl#yw^LYL3**ul3Pd67{*uTTSgp?=0 zNqORyzGSGm)TA}v8@%42p!^x18k@X@>Q{Oj{(NhajpF~W(xHNqMVm}lCX+I&&D!;q zmdw09e`T##TJsj_wIHb7lm=t16jSc=@aL~WcC5KH=kE?WY3)4fs>qyrnKpvfq?K0O z4lLEjKf;3NTW8PqKlk)8RnZ-%ca&sPc1@xt);Xf&Y)(fc&0vjHz)b1hBRkR8*p&<0 z=WLHz!y~OMsbD}R`|c$-kgkP9z1+Z?!F(h~@lt4z-`8$bUi^keMP~3toHXZXnxBRH z-1ONOAlN6XK%}x^xvE^n$qJ$`QHcDznDx$MBOiR=Vm1=01hHPid+%R4;UBUxVK;(j ze5?J)o%>89eJ}iyr{Qp_%gdRD&uj_u=^MXP{>1pae37T?mZ8z_!Dy_pzHqNw<7=CF ziJ(gHpQq)RjpQKBZheQSGne0wX$))s2F!;!!Q?hu4t*n79%5f2Tq>s7q?|x++cmjA zGJ*QlKEwR%uQ*HF~TD(^PVe*m+*j?v+gAPV;*Yk@?!T^o4Rm( z(4%Z$#v}h|y#^*Ohv-V%&)}uk3-^c1?|R^!uT9vmU2FLQwoV=M5)zWg%^#eeOfWM2 zeb0OA=Z}k`%7Ac;x08g6BmU_FIsfNXtEGPIwMCu{zo&sp&umh{mJj6q^g5U?nPZKE z^2-*1<3#>==PXy5CYsax>@etI4E6C|!$YsHwD&t7o|lFf0j(Yd4`Z!-noNy4cgeGu zXQ0dtMzfYd4G8$rl?}p#v<2jy7wL252UeK{pE{ymT)NO@iMb4OE9Noo8q7R)b3Pu{ zJegp@?8Y#7mcWK{c^g~pW^_bqTMU5T&5dhGVUwbK*!5;r-o3FlA7B%i3&4?)bGDz?n>6*fFKSZ z*kvzA-H_Xicx%S8MB!=_5#X84mk5dHx zWq{J)v=p7MvPza@-p|1Pmw9(+p9q{;0{u2)e`A=}q&J+6aQ9M%EfbXLbYDMRa^Np* zs2)QNAcfdWC2pNx*WG-2w9A`iGxdp1qoqtty3E<-NhG&7TsA3T>(0hddHEW@85rW* zEfCX?Iadcco69vNoxZ!aau2Y6x-+&Yor<`R5P4Gh1rBm>&`?>?_x!+JNg>ybMWg-m zE^*J^nuHpR8ck|uG;we<7XUPHTJ>+0XPZf);Nti}=HmGZVJ!1@zAbMPOAZ|wjf@lzR`?9Dytte@+ zthBwXaruolo4wE@O&8qCY0AX5gx}D{0^QHIiygP@V~4(mem{D!-_xfk6`j2K$HT;K z$Bj`z3#1#KCl^$gY<5zX^)P;QEk>>!Dm&I{{in~TNUi_2dohD}lkh2Jf~0L&6Z%Lo zj2!hNDndKpQKCoIMc8TTr?-wzS#`PCxlf`O!n2doc3z6!4OtS=$WiM?P2Mt0xMm0- zM3%i~=i=@E990fpXFItkCR$xutBn8Y?d&2FVYsi-ODvjXa5Q?d ztsK;=n$ThI@52{*kg+t;5C5@W79m}&QfM2eq%q;c)UdN3a|>A6@8@~A#5_tPdXUvl zOK+&V`r{6~%Fmoe#>?Gq+oDn=w%?5V1+OWv_tqD_Zs6J`AYBt3U1&)XRLjAjuabL& zTWyAJu93CZpG5rVjQIyUeGeM)$R6}Uet1YHqgWI~;x|f6`pB=EDfa_(sJ`EBXu@yv z(Rg@ZJMU>xT2?V!IiYRCF2kvs$+s#kuNVW7N}&zj9G0`L{#2V<=bO~cwUY!0dA%%} zp;gWI{@o+H&$rUc{$f!fs+MqA`c=5raUfRclo=lFEW~WR@!;Rh-)jy26Q3$`OFMcD3#W6pcR1gGJmw&jmSGlTP@B-$aoguJA!)MvjSRFhZAxNmjSU(x zX*%9gxU&A^U{}@d*{a~b_}m}$q`@I+WFJh;l|2>Wl`~w??DQJ&3CP$jSu5Dx(6(O_ zvsyWLYMCR7fdMV09NBjCy|d2(#InwpUvLO+MW)>Ufw|=W`Ffr1l#DDKXC3}1`EPc2 zdV*~z$r>A*eQ0J`da{1a@=3n!SG{F{gTTEOeKLV_y2MC zgg)kV{x?_d-qcO6VFn#wY8u|=G`{t)AbdH6(_;ST$HB=<1`WV^ktkW>Xw0YkekyWp z;;l0vq@=Ya0NXbKBb~uFDJyd0;>swj?})Q!L8uJks)XzfPlAPG-3#g7STC>WafiFR z3x!ZuzcG)zl)Nf-flpIeh&c9w89$i@@P1_DJCLO{@1dR0T8XSRTNrnWbj_mTVn%wl z=UQo1yr>3_3QsUk%Cl7R|JAm2>|p4cmxg^hmODdb*Z;tsea1@oX0k5Ak$Yg8iu*A~ ze7F{VWLwilG+iTxb^hM-NmrGUg4K)sF04^H&#hj2Gc6(qOYHiErD{18G3lwjRnX~& zv1GkAl`;{U$V0yTxqUgJbgY|g4R}hM5?%Xq`|e;N&n&#a{vT48F)Jrfu-m(s`J?2~ zX}IV8w2KNBL#5e5Lb85$csaLVa|dM-8KgfKPfyln(RY&<;wJ`eK#LXv`(vZ{>_xk$ ziYsU0ha`kT|$J!=qlT_TGsU%8{ZyM>cvNAKccIp|kjUf7#f@rAPZUO!XLz_5QCO+Fmfq^6=_J_VYUn_}{VTiI-I4qpRu)?~iyaJe|LYW))eR zHXN8?WYwG+`8lHeRd>-_@t-AIP}_YzF>!CwV?V3!4{rR9;yG4;$!}#?3dgGIn&mRZ^ zr{(umbRMh7T*{!c7fU3SbMm-xr=+i-2ZP0_oAHB&S4iqFVSBT_&+`WJ7~F(t6}Y7Eurb-;bv{) g;V$w2$ot^?(bnw+gznmZ*Z+0V;xG1e?*B>qA2{SVMF0Q* literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 72a1bf27..24b331c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,15 +44,14 @@ dependencies = [ "appdirs", "doit", "ftfy", - "polling2", "pyinstaller_versionfile", - "requests>=2.11,<3.0", + "requests>=2.25,<3.0", "setuptools_scm", "types-appdirs", "types-mock", "types-requests", "types-setuptools", - "tableauserverclient>=0.19", + "tableauserverclient>=0.23", "urllib3>=1.24.3,<2.0", ] [project.optional-dependencies] diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 970c4b9f..4ee355e0 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -75,7 +75,7 @@ def run_command(args): session = Session() server = session.create_session(args, logger) view_content_url, wb_content_url = ExportCommand.parse_export_url_to_workbook_and_view(logger, args.url) - logger.debug([view_content_url, wb_content_url]) + logger.debug(["view_url:", view_content_url, "workbook:", wb_content_url]) if not view_content_url and not wb_content_url: view_example = "/workbook_name/view_name" message = "{} [{}]".format( @@ -104,7 +104,7 @@ def run_command(args): default_filename = "{}.png".format(view_item.name) - except TSC.ServerResponseException as e: + except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(""), e) except Exception as e: Errors.exit_with_error(logger, exception=e) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index ae7d3a54..38518bd8 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -92,7 +92,7 @@ def get_file_type_from_filename(logger, url, file_name): Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) logger.debug("filetype: {}".format(type_of_file)) - if type_of_file in ["pdf", "csv", "png", "twb", "twbx", "tdsx"]: + if type_of_file in ["pdf", "csv", "png", "twb", "twbx", "tdsx", "tds"]: return type_of_file Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) diff --git a/tabcmd/commands/extracts/create_extracts_command.py b/tabcmd/commands/extracts/create_extracts_command.py index 4dcf2ba0..3f39f04a 100644 --- a/tabcmd/commands/extracts/create_extracts_command.py +++ b/tabcmd/commands/extracts/create_extracts_command.py @@ -2,6 +2,7 @@ from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors +from tabcmd.commands.extracts.extracts import Extracts from tabcmd.commands.server import Server from tabcmd.execution.global_options import * from tabcmd.execution.localize import _ @@ -19,12 +20,11 @@ class CreateExtracts(Server): @staticmethod def define_args(create_extract_parser): group = create_extract_parser.add_argument_group(title=CreateExtracts.name) - set_ds_xor_wb_args(group) + set_ds_xor_wb_args(group, True) set_embedded_datasources_options(group) set_encryption_option(group) set_project_arg(group) set_parent_project_arg(group) - set_site_url_arg(group) @staticmethod def run_command(args): @@ -32,24 +32,27 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args, logger) - creation_call = None - try: - logger.debug( - "Extract params: encrypt={}, include_all={}, datasources={}".format( - args.encrypt, args.include_all, args.embedded_datasources - ) + logger.debug( + "Extract params: encrypt={}, include_all={}, datasources={}".format( + args.encrypt, args.include_all, args.embedded_datasources ) - + ) + try: + item = Extracts.get_wb_or_ds_for_extracts(args, logger, server) if args.datasource: - data_source_item = Server.get_data_source_item(logger, server, args.datasource) logger.info(_("createextracts.for.datasource").format(args.datasource)) - job = server.datasources.create_extract(data_source_item, encrypt=args.encrypt) + job: TSC.JobItem = server.datasources.create_extract(item, encrypt=args.encrypt) + + else: + if not args.include_all and not args.embedded_datasources: + Errors.exit_with_error( + logger, + _("extracts.workbook.errors.requires_datasources_or_include_all").format("deleteextracts"), + ) - elif args.workbook: - workbook_item = Server.get_workbook_item(logger, server, args.workbook) logger.info(_("createextracts.for.workbook_name").format(args.workbook)) - job = server.workbooks.create_extract( - workbook_item, + job: TSC.JobItem = server.workbooks.create_extract( + item, encrypt=args.encrypt, includeAll=args.include_all, datasources=args.embedded_datasources, diff --git a/tabcmd/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py index 20eaf5b8..ab32a458 100644 --- a/tabcmd/commands/extracts/delete_extracts_command.py +++ b/tabcmd/commands/extracts/delete_extracts_command.py @@ -2,6 +2,7 @@ from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors +from tabcmd.commands.extracts.extracts import Extracts from tabcmd.commands.server import Server from tabcmd.execution.global_options import * from tabcmd.execution.localize import _ @@ -19,12 +20,10 @@ class DeleteExtracts(Server): @staticmethod def define_args(delete_extract_parser): group = delete_extract_parser.add_argument_group(title=DeleteExtracts.name) - set_ds_xor_wb_args(group) + set_ds_xor_wb_args(group, True) set_embedded_datasources_options(group) - # set_encryption_option(group) set_project_arg(group) set_parent_project_arg(group) - group.add_argument("--url", help=_("createextracts.options.url")) @staticmethod def run_command(args): @@ -33,14 +32,21 @@ def run_command(args): session = Session() server = session.create_session(args, logger) try: + item = Extracts.get_wb_or_ds_for_extracts(args, logger, server) if args.datasource: logger.info(_("deleteextracts.for.datasource").format(args.datasource)) - data_source_item = Server.get_data_source_item(logger, server, args.datasource) - job = server.datasources.delete_extract(data_source_item) - elif args.workbook: + job: TSC.JobItem = server.datasources.delete_extract(item) + else: + if not args.include_all and not args.embedded_datasources: + Errors.exit_with_error( + logger, + _("extracts.workbook.errors.requires_datasources_or_include_all").format("deleteextracts"), + ) logger.info(_("deleteextracts.for.workbook_name").format(args.workbook)) - workbook_item = Server.get_workbook_item(logger, server, args.workbook) - job = server.workbooks.delete_extract(workbook_item) + job: TSC.JobItem = server.workbooks.delete_extract( + item, includeAll=args.include_all, datasources=args.embedded_datasources + ) + except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/extracts/extracts.py b/tabcmd/commands/extracts/extracts.py new file mode 100644 index 00000000..60c63c00 --- /dev/null +++ b/tabcmd/commands/extracts/extracts.py @@ -0,0 +1,30 @@ +from tabcmd.commands.constants import Errors +from tabcmd.commands.datasources_and_workbooks.datasources_and_workbooks_command import DatasourcesAndWorkbooks +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ + +import tableauserverclient as TSC + + +class Extracts(Server): + @staticmethod + def get_wb_or_ds_for_extracts(args, logger, server): + container = Server.get_project_by_name_and_parent_path( + logger, server, args.project_name, args.parent_project_path + ) + if args.datasource: + logger.debug(_("export.status").format(args.datasource)) + datasource = Server.get_data_source_item(logger, server, args.datasource, container) + return datasource + + elif args.workbook or args.url: + if args.workbook: + workbook_item: TSC.WorkbookItem = Server.get_workbook_item(logger, server, args.workbook, container) + else: + workbook_item: TSC.WorkbookItem = DatasourcesAndWorkbooks.get_wb_by_content_url( + logger, server, args.url + ) + logger.info(_("export.status").format(workbook_item.name)) + return workbook_item + + Errors.exit_with_error(logger, "Datasource or workbook required") diff --git a/tabcmd/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py index 73a467ff..b88f220b 100644 --- a/tabcmd/commands/extracts/refresh_extracts_command.py +++ b/tabcmd/commands/extracts/refresh_extracts_command.py @@ -1,8 +1,8 @@ -import polling2 # type: ignore import tableauserverclient as TSC from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors +from tabcmd.commands.extracts.extracts import Extracts from tabcmd.commands.server import Server from tabcmd.execution.global_options import * from tabcmd.execution.localize import _ @@ -17,12 +17,7 @@ class RefreshExtracts(Server): @staticmethod def define_args(refresh_extract_parser): group = refresh_extract_parser.add_argument_group(title=RefreshExtracts.name) - possible_targets = set_ds_xor_wb_args(group) - # hm, why did I do this instead of group.add_arg? - possible_targets.add_argument( - "--url", - help=_("createextracts.options.url"), - ) + set_ds_xor_wb_args(group, True) set_incremental_options(group) set_calculations_options(group) set_project_arg(group) @@ -34,13 +29,9 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args, logger) - refresh_action = "refresh" if args.addcalculations or args.removecalculations: - logger.warning( - "Data Acceleration tasks are deprecated and this parameter has no effect." - "It will be removed in a future update." - ) + logger.warning("Add/Remove Calculations tasks are not yet implemented.") # are these two mandatory? mutually exclusive? # docs: the REST method always runs a full refresh even if the refresh type is set to incremental. @@ -49,62 +40,28 @@ def run_command(args): # if args.synchronous: # docs: run a full refresh and poll until it completes # else: run a full refresh but don't poll for completion - container = None - if args.project_name: - try: - container = Server.get_project_by_name_and_parent_path( - logger, server, args.project_name, args.parent_project_path - ) - except Exception as ex: - logger.warning( - "Could not find project {}/{}. Continuing without.".format( - args.parent_project_path, args.project_name - ) - ) - job = None try: - # TODO: use the container in the search + item = Extracts.get_wb_or_ds_for_extracts(args, logger, server) if args.datasource: - logger.debug(_("export.status").format(args.datasource)) - datasource_id = Server.get_data_source_id(logger, server, args.datasource, container) logger.info(_("refreshextracts.status_refreshed").format(_("content_type.datasource"), args.datasource)) - job: TSC.JobItem = server.datasources.refresh(datasource_id) - - elif args.workbook: - logger.debug(_("export.status").format(args.workbook)) - workbook_id = Server.get_workbook_id(logger, server, args.workbook, container) + job: TSC.JobItem = server.datasources.refresh(item.id) + else: + job: TSC.JobItem = server.workbooks.refresh(item.id) logger.info(_("refreshextracts.status_refreshed").format(_("content_type.workbook"), args.workbook)) - job: TSC.JobItem = server.workbooks.refresh(workbook_id) - - elif args.url: - logger.error("URL not yet implemented") except Exception as e: Errors.exit_with_error(logger, _("refreshextracts.errors.error"), e) logger.info(_("common.output.job_queued_success")) + logger.debug("Extract refresh queued with JobID: {}".format(job.id)) if args.synchronous: # maintains a live connection to the server while the refresh operation is underway, polling every second # until the background job is done. logger.info("Waiting for refresh job to begin ....") - try: - polling2.poll(lambda: logger.info(".") and job.started_at is not None, step=1, timeout=args.timeout) - except polling2.TimeoutException as te: - Errors.exit_with_error(logger, _("messages.timeout_error.summary")) - logger.info("Job started at {}".format(job.started_at)) - - try: - polling2.poll( - lambda: logger.info("{}".format(job.progress)) and job.finish_code != -1, - step=1, - timeout=args.timeout, - ) - logger.info("Job completed at {}".format(job.completed_at)) - except polling2.TimeoutException as te: - Errors.exit_with_error(logger, _("messages.timeout_error.summary")) - - else: - logger.info(_("common.output.job_queued_success")) - logger.debug("Extract refresh started with JobID: {0}".format(job.id)) + job_done = server.jobs.wait_for_job(job_id=job, timeout=args.timeout) + logger.info("Job completed: ") + logger.info(job_done) + except Exception as je: + Errors.exit_with_error(logger, je) diff --git a/tabcmd/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py index 357fccb5..85efcaae 100644 --- a/tabcmd/commands/group/create_group_command.py +++ b/tabcmd/commands/group/create_group_command.py @@ -35,4 +35,4 @@ def run_command(args): if args.continue_if_exists and Errors.is_resource_conflict(e): logger.info(_("tabcmd.result.already_exists").format(_("content_type.group"), args.name)) return - Errors.exit_with_error(logger, _("tabcmd.result.failed.create_group")) + Errors.exit_with_error(logger, exception=e) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index f215e3a8..d315e6dd 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -47,7 +47,7 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional # TODO: typing should reflect that this returns TSC.TableauItem and item_endpoint is of type TSC.QuerysetEndpoint[same] item_log_name: str = "[{0}] {1}".format(type(item_endpoint).__name__, item_name) if container: - container_name: str = "({0}) {1}".format(container.__class__, container.name) + container_name: str = "[{0}] {1}".format(container.__class__.__name__, container.name) item_log_name = "{0}/{1}".format(container_name, item_log_name) logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index da6b9f76..762e73a2 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -13,11 +13,8 @@ class ListCommand(Server): """ # strings to move to string files - local_strings = { - "tabcmd_content_listing": "===== Listing {0} content for user {1}...", - "tabcmd_listing_label_name": "NAME:", - "tabcmd_listing_label_id": "ID:", - } + tabcmd_content_listing = "===== Listing {0} content for user {1}..." + tabcmd_listing_label_name = "NAME: {}" name: str = "list" description: str = "List content items of a specified type" @@ -39,7 +36,7 @@ def run_command(args): content_type = args.content try: - logger.info(ListCommand.local_strings.tabcmd_content_listing.format(content_type, session.username)) + logger.info(ListCommand.tabcmd_content_listing.format(content_type, session.username)) if content_type == "projects": items = server.projects.all() @@ -51,9 +48,13 @@ def run_command(args): items = server.flows.all() for item in items: - logger.info(ListCommand.local_strings.tabcmd_listing_label_name.rjust(10), item.name) + logger.info(ListCommand.tabcmd_listing_label_name.format(item.name)) if args.details: logger.info(item) + if content_type == "workbooks": + server.workbooks.populate_views(item) + for v in item.views: + logger.info(v) except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 2657829d..60c72fd0 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -163,11 +163,6 @@ def set_project_arg(parser): return parser -def set_site_url_arg(parser): - parser.add_argument("--url", help="The canonical name for the resource as it appears in the URL") - return parser - - def set_ds_xor_wb_options(parser): target_type_group = parser.add_mutually_exclusive_group(required=False) target_type_group.add_argument("-d", "--datasource", action="store_true", help="The name of the target datasource.") @@ -181,7 +176,8 @@ def set_ds_xor_wb_args(parser, url=False): target_type_group.add_argument("-d", "--datasource", help="The name of the target datasource.") target_type_group.add_argument("-w", "--workbook", help="The name of the target workbook.") if url: - target_type_group.add_argument("--url", "-U", help=_("deleteextracts.options.url")) + # -U conflicts with --username, they are not case sensitive + target_type_group.add_argument("--url", help=_("deleteextracts.options.url")) return parser @@ -201,7 +197,6 @@ def set_site_status_arg(parser): return parser -# create-site/update-site - lots of these options are never used elsewhere # mismatched arguments: createsite says --url, editsite says --site-id # just let both commands use either of them def set_site_id_args(parser): @@ -215,7 +210,7 @@ def set_site_id_args(parser): return parser -# these options are all shared in create-site and edit-site +# create-site/update-site - lots of these options are never used elsewhere def set_common_site_args(parser): parser = set_site_id_args(parser) diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 43004ba5..5e6ed25a 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -38,7 +38,6 @@ import io mock_args = argparse.Namespace() -mock_args.logging_level = "info" fake_item = MagicMock() fake_item.name = "fake-name" @@ -71,6 +70,14 @@ def _set_up_session(mock_session, mock_server): mock_session.assert_not_called() global mock_args mock_args = argparse.Namespace(logging_level="DEBUG") + # set values for things that should always have a default + # should refactor so this can be automated + mock_args.continue_if_exists = False + mock_args.project_name = None + mock_args.parent_project_path = None + mock_args.parent_path = None + mock_args.timeout = None + mock_args.username = None # auth def test_login(self, mock_session, mock_server): @@ -158,11 +165,13 @@ def test_runschedule(self, mock_session, mock_server): def test_create_extract(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_server.workbooks = getter + mock_server.projects = getter mock_args.encrypt = False mock_args.include_all = True mock_args.datasource = None mock_args.embedded_datasources = None - mock_args.workbook = "workbook" + mock_args.workbook = "workbook-name" + print(mock_args) create_extracts_command.CreateExtracts.run_command(mock_args) mock_session.assert_called() @@ -177,6 +186,7 @@ def test_delete_extract(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_server.datasources = getter mock_args.datasource = "datasource-name" + mock_server.projects = getter delete_extracts_command.DeleteExtracts.run_command(mock_args) mock_session.assert_called() @@ -198,13 +208,14 @@ def test_refresh_extract(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_args.datasource = "datasource" mock_server.datasources = getter + mock_server.projects = getter mock_args.workbook = None mock_args.addcalculations = None mock_args.removecalculations = None mock_args.incremental = None mock_args.synchronous = None - mock_args.project_name = None - mock_args.parent_project_path = None + print(mock_args) + refresh_extracts_command.RefreshExtracts.run_command(mock_args) mock_session.assert_called() diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 117b67a8..a695c665 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -129,20 +129,20 @@ def _get_datasource(self, server_file): arguments = [command, server_file] _test_command(arguments) - def _create_extract(self, wb_name): + def _create_extract(self, type, wb_name): command = "createextracts" - arguments = [command, "-w", wb_name, "--encrypt"] + arguments = [command, type, wb_name, "--encrypt"] _test_command(arguments) # variation: url - def _refresh_extract(self, wb_name): + def _refresh_extract(self, type, wb_name): command = "refreshextracts" - arguments = [command, "--workbook", wb_name] + arguments = [command, type, wb_name] _test_command(arguments) - def _delete_extract(self, wb_name): + def _delete_extract(self, type, item_name): command = "deleteextracts" - arguments = [command, "-w", wb_name] + arguments = [command, type, item_name, "--include-all"] _test_command(arguments) def _list(self, item_type: str): @@ -151,7 +151,7 @@ def _list(self, item_type: str): _test_command(arguments) # actual tests - TWBX_FILE_WITH_EXTRACT = "extract-data-access.twbx" + TWBX_FILE_WITH_EXTRACT = "WorkbookWithExtract.twbx" TWBX_WITH_EXTRACT_NAME = "WorkbookWithExtract" TWBX_WITH_EXTRACT_SHEET = "sheet1" TWBX_FILE_WITHOUT_EXTRACT = "simple-data.twbx" @@ -159,7 +159,9 @@ def _list(self, item_type: str): TWBX_WITHOUT_EXTRACT_SHEET = "Testsheet1" TDSX_WITH_EXTRACT_NAME = "WorldIndicators" TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" + # only works on linux servers or something TWB_WITH_EMBEDDED_CONNECTION = "embedded_connection_waremart.twb" + EMBEDDED_TWB_NAME = "waremart" @pytest.mark.order(1) def test_login(self): @@ -264,109 +266,95 @@ def test_delete_projects(self): @pytest.mark.order(10) def test_wb_publish(self): - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - arguments = self._publish_args(file, name_on_server) + arguments = self._publish_args(file, OnlineCommandTest.TWBX_WITH_EXTRACT_NAME) _test_command(arguments) - @pytest.mark.order(10) + @pytest.mark.order(11) def test_wb_get(self): + # add .twbx to the end to tell the server what we are getting self._get_workbook(OnlineCommandTest.TWBX_WITH_EXTRACT_NAME + ".twbx") - @pytest.mark.order(10) + @pytest.mark.order(11) def test_view_get_pdf(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".pdf") - @pytest.mark.order(10) + @pytest.mark.order(11) def test_view_get_csv(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".csv") - @pytest.mark.order(10) + @pytest.mark.order(11) def test_view_get_png(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".png") - @pytest.mark.order(11) - def test_wb_delete(self): - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - self._delete_wb(name_on_server) - @pytest.mark.order(11) def test_wb_publish_embedded(self): - name_on_server = OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION + pytest.skip("waremart failed to establish a connection to your datasource.") file = os.path.join("tests", "assets", OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION) - arguments = self._publish_args(file, name_on_server) + arguments = self._publish_args(file, OnlineCommandTest.EMBEDDED_TWB_NAME) arguments = self._publish_creds_args(arguments, waremart_user, waremart_password, True) _test_command(arguments) @pytest.mark.order(12) def test_publish_ds(self): - name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) - arguments = self._publish_args(file, name_on_server) + arguments = self._publish_args(file, OnlineCommandTest.TDSX_WITH_EXTRACT_NAME) _test_command(arguments) @pytest.mark.order(13) def test__get_ds(self): - ds_name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME - self._get_datasource(ds_name_on_server + ".tdsx") + self._get_datasource(OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + ".tdsx") - @pytest.mark.order(14) - def test__delete_ds(self): - name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME - self._delete_ds(name_on_server) + @pytest.mark.order(13) + def test_refresh_ds_extract(self): + self._refresh_extract("-d", OnlineCommandTest.TDSX_WITH_EXTRACT_NAME) - @pytest.mark.order(15) + @pytest.mark.order(14) def test_delete_extract(self): - # fails because the extract has a bad data connection :/ - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_args(file, name_on_server) - self._delete_extract(name_on_server) + self._delete_extract("-d", OnlineCommandTest.TDSX_WITH_EXTRACT_NAME) @pytest.mark.order(16) def test_create_extract(self): - # Fails because it 'already has an extract' :/ - name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME - self._create_extract(name_on_server) + pytest.skip( + "Can't create extract for data source '3f3c204c-3c28-4fa6-9fb6-80c848a20338' because it is already extracted." + ) + self._create_extract("-d", OnlineCommandTest.TDSX_WITH_EXTRACT_NAME) @pytest.mark.order(17) - def test_refresh_extract(self): - # must be a datasource owned by the test user - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - arguments = self._publish_args(file, name_on_server) - _test_command(arguments) + def test_refresh_wb_extract(self): + self._refresh_extract("-w", OnlineCommandTest.TWBX_WITH_EXTRACT_NAME) + @pytest.mark.order(19) + def test_wb_delete(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - self._refresh_extract(name_on_server) self._delete_wb(name_on_server) @pytest.mark.order(19) - def test_export_wb_pdf(self): - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - arguments = self._publish_args(file, name_on_server) - _test_command(arguments) + def test__delete_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._delete_ds(name_on_server) + @pytest.mark.order(19) + def test_export_wb_pdf(self): command = "export" - friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + friendly_name = ( + OnlineCommandTest.TWBX_WITH_EXTRACT_NAME + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" + ) arguments = [command, friendly_name, "--fullpdf", "-f", "exported_wb.pdf"] _test_command(arguments) @pytest.mark.order(19) def test_export_view_pdf(self): - name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - arguments = self._publish_args(file, name_on_server) - _test_command(arguments) command = "export" - friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" + friendly_name = ( + OnlineCommandTest.TWBX_WITH_EXTRACT_NAME + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" + ) arguments = [command, friendly_name, "--pdf", "-f", "exported_view.pdf"] _test_command(arguments) From 6a2311c9046f7b1e6a89c4252185ce9f30b35681 Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 10 Mar 2023 23:25:56 -0800 Subject: [PATCH 06/14] Jac/readme updates (#235) * remove 'known gaps' and pointed to release notes. * add log statement explaining workbooks won't be filtered * edit contributing file * add python 3.11 --- README.md | 21 ++-- contributing.md | 98 +++++++++++-------- pyproject.toml | 8 +- .../export_command.py | 3 + 4 files changed, 70 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index e1703650..810f817c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ e.g * `tabcmd help` ###or -## Install on the command line (requires Python 3.7+) +## Install on the command line + +(Note: this requires Python 3.7+. Generally we will actively support versions of Python that are still in security support). ```shell pip install tabcmd @@ -83,21 +85,12 @@ see the user documentation at https://tableau.github.io/tabcmd/ ## Release Notes -Version 2.0 is the first version of tabcmd built in python. +Version 2 is the first version of tabcmd built in python. It is specifically targeted to support users of Tableau Online, who are required to have MFA enabled. -(MFA support is not available in tabcmd 2022.2). It does not yet fully replace the existing tabcmd client.\ -**Known gaps** -- handling custom views in get/export commands -- several commands that can only be run by a Server Admin: - - editdomain / listdomains - - initialuser - - reset_openid_sub - - runschedule - - set - - syncgroup - - upgradethumbnails - - validateidpmetadata +(MFA support is not available in the tabcmd program that ships with Tableau Server). +Version 2 does not yet fully replace the existing tabcmd client, in particular it **does not support most server admin actions**. +For known gaps in supported functionality, see the latest [release notes](https://github.com/tableau/tabcmd/releases) ## About diff --git a/contributing.md b/contributing.md index 723e8f86..417cd22c 100644 --- a/contributing.md +++ b/contributing.md @@ -1,26 +1,35 @@ # For developers * [Install tabcmd](#install-tabcmd) -* [Run tabcmd](#run-tabcmd) -* [Development commands](#development-commands) * [Contributing](#contributing) -* [To add a new command](#to-add-a-new-command) -* [Why Python\?](#why-python) -* [Project Structure](#project-structure) - +* [Development](#development) + * [Dev scripts](#dev-scripts) + * [Why Python\?](#why-python) + * [Project structure](#project-structure) + * [To add a new command](#to-add-a-new-command) + * [Localization](#localization) +* [Releases](#releases) + * [Versioning](#versioning) + * [Packaging](#packaging) + + +These instructions are for people who want to download the code and edit it directly. If you are interested in tabcmd but not the code, see [here](Readme.md). +####To work with tabcmd, you need to have **Python 3.7+** installed. -## Install tabcmd -These instructions are only necessary if you want to download the code and run it directly. If you are interested in tabcmd but not the code, see [here](Readme.md). -####To work with tabcmd, you need to have **Python 3.7+** installed. +## Contributing -### +Code contributions and improvements by the community are welcomed! +See the LICENSE file for current open-source licensing and use information. +Before we can accept pull requests from contributors, we require a signed [Contributor License Agreement (CLA)](http://tableau.github.io/contributing.html). + ## Development -To work on the tabcmd code, use these scripts. On Windows, +### Dev scripts +To work on the tabcmd code, use these scripts. _(note that running mypy and black is required for code being submitted to the repo)_ - build @@ -38,42 +47,14 @@ _(note that running mypy and black is required for code being submitted to the r - do test coverage calculation (https://coverage.readthedocs.io/en/6.3.2) > bin/coverage.sh -- To trigger publishing to pypi tag a commit on main with 'pypi'. Versioning is done with -- setuptools_scm so it will be a x.y.dev0 pre-release version unless you first tag the -- commit with a new version tag. e.g -> git tag -d pypi && git push --delete origin pypi -> git tag v2.0.4 && git tag pypi && git push --tags - -# Missing step: a job that is triggered by pypi tag and creates a release -# Missing trigger: when pypi-release is done, begin the app smoke test -- packaging to an exe is done with pyinstaller. You can only build an executable for -- the platform you build on. -- On github this job is triggered by creating a release (or manually) -> doit version <-- produce a current version and metadata file to package -> pyinstaller tabcmd-windows.spec .... # see package.yml for OS-specific arguments - - Packaging produces dist/tabcmd.exe (or equivalent). -- Run the package -> dist/tabcmd/tabcmd.exe --help - - -## Why Python? +### Why Python? * Cross-platform * Build on our existing Python [Tableau Server Client](https://github.com/tableau/server-client-python/) -## Contributing - -Code contributions and improvements by the community are welcomed! - -See the LICENSE file for current open-source licensing and use information. - -Before we can accept pull requests from contributors, we require a signed [Contributor License Agreement (CLA)](http://tableau.github.io/contributing.html). - - -## Project structure +### Project structure The core design principles for this app are - it must provide the functionality of the instance of tabcmd, with drop-in replacement CLI options - it should be able to call [tsc](https://github.com/tableau/server-client-python/) for all server actions @@ -84,12 +65,43 @@ The core design principles for this app are 3. the 'commands' module contains the logic required to translate the tabcmd CLI interface into calls to tsc. This is completely dissociated from the parsers, and could theoretically be called from a completely different interface. 4. The 'execution' module is the core logic. TabcmdController gets an argparse parser, then attaches all the defined parsers to it and associates one command with each parser. -## To add a new command +### To add a new command 0. choose the single word that will be used as your command. Let's call this one `dream` 1. add parsers/dream_parser.py, and use methods from parent_parser to define the arguments 2. add commands/dreams/dream_command.py. It must have a method run_command.py(args) and the args object must contain all information needed from the user. 3. in map_of_parsers.py, add an entry for your new parser, like "dreams": DreamParser.dream_parser 4. in map_of_commands.py, add an entry for your new command, like "dream": ("dream", DreamCommand, "Think about picnics")," -5. add tests! +5. add tests! + +### Localization + +Strings are stored in /tabcmd/locales/[language]/*.properties by id and referred to in code as +> string = _("string.id") + +For runtime execution these files must be converted to .mo via .po +> doit mo +## Releases +To trigger publishing to pypi tag a commit on main with 'pypi'. +When pypi-release is done, begin the app smoke test action. + +### Versioning +Versioning is done with setuptools_scm and based on git tags. +It will be a x.y.dev0 pre-release version except for commits with a new version tag. e.g +> git tag v2.0.4 && git push --tags + +A new tag is created with the name of each release on github. + +### Packaging +Before packaging, we produce a current metadata file to include in the bundle +> doit version + +Packaging is done with pyinstaller, which will build an executable for the platform it runs on. +A github action runs on Mac, Windows and Linux to generate each executable. + +> pyinstaller tabcmd-windows.spec .... + +Packaging produces executables in the dist folder. To run: +> dist/tabcmd/tabcmd.exe --help + diff --git a/pyproject.toml b/pyproject.toml index 24b331c9..17ac9ecf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ packages = ["tabcmd"] tabcmd = ["tabcmd.locales/**/*.mo"] [tool.black] line-length = 120 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] extend-exclude = '^/bin/*' [tool.mypy] disable_error_code = [ @@ -30,14 +30,16 @@ description="A command line client for working with Tableau Server." authors = [{name="Tableau", email="github@tableau.com"}] license = {file = "LICENSE"} readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.7" # https://devguide.python.org/versions/ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10" + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12" ] dependencies = [ 'argparse', diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 4ee355e0..9a52fe5b 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -121,6 +121,7 @@ def run_command(args): @staticmethod def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: if args.filter: + logger.debug("filter = {}".format(args.filter)) params = args.filter.split("&") for value in params: ExportCommand.apply_filter_value(logger, request_options, value) @@ -130,6 +131,8 @@ def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger def download_wb_pdf(server, workbook_item, args, logger): logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) + if args.filter or args.url.find("?") > 0: + logger.info("warning: Filter values will not be applied when exporting a complete workbook") ExportCommand.apply_values_from_url_params(logger, pdf_options, args.url) ExportCommand.apply_pdf_options(logger, pdf_options, args) logger.debug(pdf_options.get_query_params()) From c46a8ca0518d2ff904cfafd712b24b141d03caf0 Mon Sep 17 00:00:00 2001 From: Jac Date: Sat, 11 Mar 2023 13:57:35 -0800 Subject: [PATCH 07/14] Always log version (#234) When people ask for help they usually show default logs and don't know what version they have. --- tabcmd/execution/tabcmd_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 3798b30c..1a4544d2 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -5,6 +5,8 @@ from .logger_config import log from .parent_parser import ParentParser +from tabcmd.version import version + class TabcmdController: @staticmethod @@ -27,6 +29,7 @@ def run(parser, user_input=None): print("logging:", namespace.logging_level) logger = log(__name__, namespace.logging_level or logging.INFO) + logger.info("Tabcmd {}".format(version)) if (hasattr("namespace", "password") or hasattr("namespace", "token_value")) and hasattr("namespace", "func"): # don't print whole namespace because it has secrets logger.debug(namespace.func) From 1361c922a96e86e7beebc2b8cfa0d2a2de584896 Mon Sep 17 00:00:00 2001 From: Jac Date: Sat, 11 Mar 2023 14:17:07 -0800 Subject: [PATCH 08/14] Jac/content listing (#230) * add tests * format error output * add ID to output, add a "nothing found" message instead of just exiting silently. --- tabcmd/commands/site/list_command.py | 20 +++++--- tabcmd/commands/site/list_sites_command.py | 4 +- tabcmd/tabcmd.py | 10 ++-- tests/commands/test_listing_commands.py | 58 ++++++++++++++++++++++ tests/commands/test_run_commands.py | 11 ++++ tests/e2e/online_tests.py | 8 +++ 6 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 tests/commands/test_listing_commands.py diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 762e73a2..64e4fa48 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -1,5 +1,3 @@ -import tableauserverclient as TSC - from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors from tabcmd.commands.server import Server @@ -13,8 +11,12 @@ class ListCommand(Server): """ # strings to move to string files - tabcmd_content_listing = "===== Listing {0} content for user {1}..." - tabcmd_listing_label_name = "NAME: {}" + local_strings = { + "tabcmd_content_listing": "===== Listing {0} content for user {1}...", + "tabcmd_listing_label_name": "\tNAME: {}", + "tabcmd_listing_label_id": "ID: {}", + "tabcmd_content_none": "No content found.", + } name: str = "list" description: str = "List content items of a specified type" @@ -36,7 +38,7 @@ def run_command(args): content_type = args.content try: - logger.info(ListCommand.tabcmd_content_listing.format(content_type, session.username)) + logger.info(ListCommand.local_strings["tabcmd_content_listing"].format(content_type, session.username)) if content_type == "projects": items = server.projects.all() @@ -47,14 +49,18 @@ def run_command(args): elif content_type == "flows": items = server.flows.all() + if not items or len(items) == 0: + logger.info(ListCommand.local_strings["tabcmd_content_none"]) for item in items: - logger.info(ListCommand.tabcmd_listing_label_name.format(item.name)) if args.details: - logger.info(item) + logger.info("\t{}".format(item)) if content_type == "workbooks": server.workbooks.populate_views(item) for v in item.views: logger.info(v) + else: + logger.info(ListCommand.local_strings["tabcmd_listing_label_id"].format(item.id)) + logger.info(ListCommand.local_strings["tabcmd_listing_label_name"].format(item.name)) except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index e077072e..f124fbe6 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -31,8 +31,8 @@ def run_command(args): sites, pagination = server.sites.get() logger.info(_("listsites.status").format(session.username)) for site in sites: - logger.info("NAME:".rjust(10), site.name) - logger.info("SITEID:".rjust(10), site.content_url) + logger.info("NAME: {}".format(site.name)) + logger.info("SITEID: {}".format(site.id)) if args.get_extract_encryption_mode: logger.info("EXTRACTENCRYPTION:", site.extract_encryption_mode) except Exception as e: diff --git a/tabcmd/tabcmd.py b/tabcmd/tabcmd.py index 7a03f692..70b8b63d 100644 --- a/tabcmd/tabcmd.py +++ b/tabcmd/tabcmd.py @@ -17,10 +17,12 @@ def main(): print("Keyboard Interrupt: exiting") sys.exit(1) except Exception as e: - print(sys.stderr, "Unhandled exception: {}".format(type(e).__name__)) - print( - sys.stderr, - f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}", + sys.stderr.writelines( + [ + "ERROR\n", + "Unhandled exception: {}\n".format(type(e).__name__), + f"at line {e.__traceback__.tb_lineno} of {__file__}: {e}\n", + ] ) sys.exit(1) diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py new file mode 100644 index 00000000..5d45590f --- /dev/null +++ b/tests/commands/test_listing_commands.py @@ -0,0 +1,58 @@ +import argparse +from unittest.mock import MagicMock + +from tabcmd.commands.site.list_command import ListCommand +from tabcmd.commands.site.list_sites_command import ListSiteCommand + +import unittest +from unittest import mock + +mock_logger = mock.MagicMock() + +fake_item = mock.MagicMock() +fake_item.name = "fake-name" +fake_item.id = "fake-id" +fake_item.extract_encryption_mode = "ENFORCED" + +getter = MagicMock() +getter.get = MagicMock("get", return_value=([fake_item], 1)) + +mock_args = argparse.Namespace() +mock_args.logging_level = "INFO" + + +@mock.patch("tabcmd.commands.auth.session.Session.create_session") +@mock.patch("tableauserverclient.Server") +class ListingTests(unittest.TestCase): + def test_list_sites(self, mock_server, mock_session): + mock_server.sites = getter + mock_args.get_extract_encryption_mode = False + mock_session.return_value = mock_server + out_value = ListSiteCommand.run_command(mock_args) + + def test_list_content(self, mock_server, mock_session): + mock_server.flows = getter + mock_args.content = "flows" + mock_session.return_value = mock_server + out_value = ListCommand.run_command(mock_args) + + def test_list_wb_details(self, mock_server, mock_session): + mock_server.workbooks = getter + mock_args.content = "workbooks" + mock_session.return_value = mock_server + mock_args.details = True + out_value = ListCommand.run_command(mock_args) + + def test_list_datasources(self, mock_server, mock_session): + mock_server.datasources = getter + mock_args.content = "datasources" + mock_session.return_value = mock_server + mock_args.details = True + out_value = ListCommand.run_command(mock_args) + + def test_list_projects(self, mock_server, mock_session): + mock_server.projects = getter + mock_args.content = "projects" + mock_session.return_value = mock_server + mock_args.details = True + out_value = ListCommand.run_command(mock_args) diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 5e6ed25a..f8c5d24f 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -26,6 +26,7 @@ delete_site_command, edit_site_command, list_sites_command, + list_command, ) from tabcmd.commands.user import ( add_users_command, @@ -393,3 +394,13 @@ def test_create_user(self, mock_session, mock_server): mock_args.require_all_valid = False create_users_command.CreateUsersCommand.run_command(mock_args) mock_session.assert_called() + + def test_list_content(self, mock_session, mock_server): + RunCommandsTest._set_up_session(mock_session, mock_server) + mock_args.content = "workbooks" + list_command.ListCommand.run_command(mock_args) + mock_args.content = "projects" + list_command.ListCommand.run_command(mock_args) + mock_args.content = "flows" + list_command.ListCommand.run_command(mock_args) + # todo: details, filters diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index a695c665..22c1f993 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -257,6 +257,14 @@ def test_create_projects(self): def test_list_projects(self): self._list("projects") + @pytest.mark.order(8) + def test_list_flows(self): + self._list("flows") + + @pytest.mark.order(8) + def test_list_workbooks(self): + self._list("workbooks") + @pytest.mark.order(10) def test_delete_projects(self): if not project_admin: From 80cb75761d4681b9f9fd7f51f969187d6cf178ef Mon Sep 17 00:00:00 2001 From: Jac Date: Sat, 11 Mar 2023 14:21:49 -0800 Subject: [PATCH 09/14] Jac/parse publish options (#236) * implement thumbnail options. (including 'not yet implemented' message for --thumbnail-group) * Implement overwrite/append/replace arguments (including not-yet-implemented message for --replace) * clarified help content for extract-encryption and some other not-implemented options * added tests for all valid publish command options * error log cleanup: better stacktrace, less repetition --- tabcmd/commands/auth/session.py | 2 +- tabcmd/commands/constants.py | 23 ++++--- .../datasources_and_workbooks_command.py | 6 +- .../export_command.py | 4 +- .../publish_command.py | 47 ++++++++++--- tabcmd/commands/user/user_data.py | 2 +- tabcmd/execution/global_options.py | 64 +++++++++++++----- tabcmd/execution/parent_parser.py | 2 +- tests/commands/test_run_commands.py | 4 ++ tests/parsers/test_parser_publish.py | 67 +++++++++++++++++++ 10 files changed, 180 insertions(+), 41 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 2e7107ef..1dcf34a7 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -249,7 +249,7 @@ def _sign_in(self, tableau_auth) -> TSC.Server: self.username = self.tableau_server.users.get_by_id(self.user_id).name self.logger.info(_("common.output.succeeded")) except Exception as e: - Errors.exit_with_error(self.logger, e) + Errors.exit_with_error(self.logger, exception=e) self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) return self.tableau_server diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index c491a135..263fb763 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,5 +1,8 @@ import inspect import sys +from typing import Optional + +import tableauserverclient from tabcmd.execution.localize import _ @@ -44,7 +47,7 @@ def log_stack(logger): stack = inspect.stack() here = stack[0] file, line, func = here[1:4] - start = 0 + start = 1 n_lines = 5 logger.debug(HEADER_FMT % (file, func)) for frame in stack[start + 1 : n_lines]: @@ -54,15 +57,17 @@ def log_stack(logger): logger.info("Error printing stack trace:", e) @staticmethod - def exit_with_error(logger, message=None, exception=None): + def exit_with_error(logger, message: Optional[str] = None, exception: Optional[Exception] = None): try: - Errors.log_stack(logger) if message and not exception: logger.error(message) - if exception: + Errors.log_stack(logger) + elif exception: if message: - logger.debug("Error message: " + message) - Errors.check_common_error_codes_and_explain(logger, exception) + logger.info("Error message: " + message) + Errors.check_common_error_codes_and_explain(logger, exception) + else: + logger.info("No exception or message provided") except Exception as exc: print(sys.stderr, "Error during log call from exception - {} {}".format(exc.__class__, message)) try: @@ -72,7 +77,7 @@ def exit_with_error(logger, message=None, exception=None): sys.exit(1) @staticmethod - def check_common_error_codes_and_explain(logger, exception): + def check_common_error_codes_and_explain(logger, exception: Exception): # most errors contain as much info in the message as we can get from the code # identify any that we can add useful detail for and include them here if Errors.is_expired_session(exception): @@ -83,5 +88,7 @@ def check_common_error_codes_and_explain(logger, exception): # "session.session_expired_login")) # session.renew_session() return - else: + if exception.__class__ == tableauserverclient.ServerResponseError: logger.error(exception) + else: + logger.exception(exception) diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 1a2169f5..fb0d5daf 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -28,7 +28,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: logger.debug(req_option.get_query_params()) matching_views, paging = server.views.get(req_option) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) if len(matching_views) < 1: Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found")) return matching_views[0] @@ -42,7 +42,7 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI logger.debug(req_option.get_query_params()) matching_workbooks, paging = server.workbooks.get(req_option) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) if len(matching_workbooks) < 1: Errors.exit_with_error(logger, message=_("dataalerts.failure.error.workbookNotFound")) return matching_workbooks[0] @@ -56,7 +56,7 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou logger.debug(req_option.get_query_params()) matching_datasources, paging = server.datasources.get(req_option) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) if len(matching_datasources) < 1: Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) return matching_datasources[0] diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 9a52fe5b..23012b51 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -54,7 +54,9 @@ def define_args(export_parser): help="Set the page size of the exported PDF", ) - group.add_argument("--width", default=800, help="Set the width of the image in pixels. Default is 800 px") + group.add_argument( + "--width", default=800, help="[Not Yet Implemented] Set the width of the image in pixels. Default is 800 px" + ) group.add_argument("--filename", "-f", help="filename to store the exported data") group.add_argument("--height", default=600, help=_("export.options.height")) group.add_argument( diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index ce04fa31..5aaf7db2 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -29,6 +29,8 @@ def define_args(publish_parser): ) set_publish_args(group) set_project_r_arg(group) + set_overwrite_option(group) + set_append_replace_option(group) set_parent_project_arg(group) @staticmethod @@ -52,8 +54,7 @@ def run_command(args): args.project_name = "default" args.parent_project_path = "" - publish_mode = PublishCommand.get_publish_mode(args) # --overwrite, --replace - logger.info("Publishing as " + publish_mode) + publish_mode = PublishCommand.get_publish_mode(args, logger) if args.db_username: creds = TSC.models.ConnectionCredentials(args.db_username, args.db_password, embed=args.save_db_password) @@ -66,18 +67,27 @@ def run_command(args): source = PublishCommand.get_filename_extension_if_tableau_type(logger, args.filename) logger.info(_("publish.status").format(args.filename)) if source in ["twbx", "twb"]: + if args.thumbnail_group: + raise AttributeError("Generating thumbnails for a group is not yet implemented.") + if args.thumbnail_username and args.thumbnail_group: + raise AttributeError("Cannot specify both a user and group for thumbnails.") + new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) try: + print(creds) new_workbook = server.workbooks.publish( new_workbook, args.filename, publish_mode, + # args.thumbnail_username, not yet implemented in tsc + # args.thumbnail_group, connection_credentials=creds, as_job=False, skip_connection_check=False, ) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) + logger.info(_("publish.success") + "\n{}".format(new_workbook.webpage_url)) elif source in ["tds", "tdsx", "hyper"]: @@ -88,13 +98,34 @@ def run_command(args): new_datasource, args.filename, publish_mode, connection_credentials=creds ) except Exception as exc: - Errors.exit_with_error(logger, exc) + Errors.exit_with_error(logger, exception=exc) logger.info(_("publish.success") + "\n{}".format(new_datasource.webpage_url)) + # todo write tests for this method @staticmethod - def get_publish_mode(args): + def get_publish_mode(args, logger): + # default: fail if it already exists on the server + default_mode = TSC.Server.PublishMode.CreateNew + publish_mode = default_mode + + if args.replace: + raise AttributeError("Replacing an extract is not yet implemented") + + if args.append: + if publish_mode != default_mode: + publish_mode = None + else: + # only relevant for datasources, but tsc will throw an error for us if necessary + publish_mode = TSC.Server.PublishMode.Append + if args.overwrite: - publish_mode = TSC.Server.PublishMode.Overwrite - else: - publish_mode = TSC.Server.PublishMode.CreateNew + if publish_mode != default_mode: + publish_mode = None + else: + # Overwrites the workbook, data source, or data extract if it already exists on the server. + publish_mode = TSC.Server.PublishMode.Overwrite + + if not publish_mode: + Errors.exit_with_error(logger, "Invalid combination of publishing options (Append, Overwrite, Replace)") + logger.debug("Publish mode selected: " + publish_mode) return publish_mode diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index b9012078..5251aba4 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -260,7 +260,7 @@ def act_on_users( try: group = UserCommand.find_group(logger, server, args.name) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) n_users_handled: int = 0 number_of_errors: int = 0 diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 60c72fd0..695820db 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -126,7 +126,7 @@ def set_encryption_option(parser): "--encrypt", dest="encrypt", action="store_true", # set to true IF user passes in option --encrypt - help="Encrypt the newly created extract.", + help="Encrypt the newly created extract. [N/a on Tableau Cloud: extract encryption is controlled by Site Admin]", ) return parser @@ -230,7 +230,8 @@ def set_common_site_args(parser): "--extract-encryption-mode", choices=encryption_modes, type=case_insensitive_string_type(encryption_modes), - help="The extract encryption mode for the site can be enforced, enabled or disabled. ", + help="The extract encryption mode for the site can be enforced, enabled or disabled. " + "[N/a on Tableau Cloud: encryption mode is always enforced] ", ) parser.add_argument( @@ -275,8 +276,6 @@ def set_filename_arg(parser, description=_("get.options.file")): def set_publish_args(parser): parser.add_argument("-n", "--name", help="Name to publish the new datasource or workbook by.") - set_overwrite_option(parser) - creds = parser.add_mutually_exclusive_group() creds.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") creds.add_argument( @@ -302,34 +301,63 @@ def set_publish_args(parser): navigate through the workbook", ) parser.add_argument( - "--replace", action="store_true", help="Use the extract file to replace the existing data source." + "--disable-uploader", + action="store_true", + help="[DEPRECATED - has no effect] Disable the incremental file uploader.", ) - parser.add_argument("--disable-uploader", action="store_true", help="Disable the incremental file uploader.") - parser.add_argument("--restart", help="Restart the file upload.") + parser.add_argument("--restart", help="[DEPRECATED - has no effect] Restart the file upload.") parser.add_argument( "--encrypt-extracts", action="store_true", - help="Encrypt extracts in the workbook, datasource, or extract being published to the server", + help="Encrypt extracts in the workbook, datasource, or extract being published to the server. " + "[N/a on Tableau Cloud: extract encryption is controlled by Site Admin]", ) + + # These two only apply for a workbook, not a datasource thumbnails = parser.add_mutually_exclusive_group() - thumbnails.add_argument("--thumbnail-username", help="Not yet implemented") - thumbnails.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API + thumbnails.add_argument( + "--thumbnail-username", + help="If the workbook contains user filters, the thumbnails will be generated based on what the " + "specified user can see. Cannot be specified when --thumbnail-group option is set.", + ) + thumbnails.add_argument( + "--thumbnail-group", + help="[Not yet implemented] If the workbook contains user filters, the thumbnails will be generated based on what the " + "specified group can see. Cannot be specified when --thumbnail-username option is set.", + ) parser.add_argument("--use-tableau-bridge", action="store_true", help="Refresh datasource through Tableau Bridge") -def set_overwrite_option(parser): +# these two are used to publish an extract to an existing data source +def set_append_replace_option(parser): append_group = parser.add_mutually_exclusive_group() append_group.add_argument( - "-o", - "--overwrite", + "--append", action="store_true", - help="Overwrites the workbook, data source, or data extract if it already exists on the server.", + help="Set to true to append the data being published to an existing data source that has the same name. " + "The default behavior is to fail if the data source already exists. " + "If append is set to true but the data source doesn't already exist, the operation fails.", ) + + # what's the difference between this and 'overwrite'? + # This is meant for when a) the local file is an extract b) the server item is an existing data source append_group.add_argument( - "--append", + "--replace", + action="store_true", + help="Use the extract file being published to replace data in the existing data source. The default " + "behavior is to fail if the item already exists.", + ) + + +# this is meant to be like replacing like +def set_overwrite_option(parser): + parser.add_argument( + "-o", + "--overwrite", action="store_true", - help="Append the extract file to the existing data source.", + help="Overwrites the workbook, data source, or data extract if it already exists on the server. The default " + "behavior is to fail if the item already exists.", ) @@ -351,12 +379,12 @@ def set_calculations_options(parser): calc_group.add_argument( "--addcalculations", action="store_true", - help="DEPRECATED [has no effect] Add precalculated data operations in the extract data source.", + help="[Not implemented] Add precalculated data operations in the extract data source.", ) calc_group.add_argument( "--removecalculations", action="store_true", - help="DEPRECATED [has no effect] Remove precalculated data in the extract data source.", + help="[Not implemented] Remove precalculated data in the extract data source.", ) return calc_group diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 21890728..ba379619 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -101,7 +101,7 @@ def parent_parser_with_global_options(): parser.add_argument( "--continue-if-exists", action="store_true", # default behavior matches old tabcmd - help=strings[9], + help=strings[9], # kind of equivalent to 'overwrite' in the publish command ) parser.add_argument( diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index f8c5d24f..dbb9bf55 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -149,6 +149,10 @@ def test_publish(self, mock_session, mock_server): mock_args.tabbed = True mock_args.db_username = None mock_args.oauth_username = None + mock_args.append = False + mock_args.replace = False + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None mock_server.projects = getter publish_command.PublishCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/parsers/test_parser_publish.py b/tests/parsers/test_parser_publish.py index 261aeb6e..65aaa20d 100644 --- a/tests/parsers/test_parser_publish.py +++ b/tests/parsers/test_parser_publish.py @@ -1,4 +1,5 @@ import unittest +from unittest import skip from tabcmd.commands.datasources_and_workbooks.publish_command import PublishCommand from .common_setup import * @@ -38,3 +39,69 @@ def test_publish_parser_save_password(self): ] args = self.parser_under_test.parse_args(mock_args) assert args.save_db_password is True, args + + def test_publish_parser_save_oauth(self): + mock_args = [ + commandname, + "filename.twbx", + "--oauth-username", + "user", + "--save-oauth", + ] + args = self.parser_under_test.parse_args(mock_args) + assert args.save_oauth is True, args + assert args.oauth_username == "user", args + + def test_publish_parser_thumbnails(self): + mock_args = [commandname, "filename.twbx", "--thumbnail-username"] # no value for thumbnail-user + with self.assertRaises(SystemExit): + args = self.parser_under_test.parse_args(mock_args) + + mock_args = [commandname, "filename.twbx", "--thumbnail-username", "goofy"] + args = self.parser_under_test.parse_args(mock_args) + assert args.thumbnail_username == "goofy", args + + @skip("Not yet implemented") + def test_publish_parser_thumbnail_group(self): + mock_args = [commandname, "filename.twbx", "--thumbnail-group", "goofy"] + args = self.parser_under_test.parse_args(mock_args) + assert args.thumbnail_group == "goofy", args + + """ + append | replace | overwrite -> result + -------- + true | F/empty | F/empty -> append + true | F/empty | true -> ERROR + true | true | F/empty -> ERROR + .... basically, replace == overwrite, append != r/o + """ + + def test_publish_parser_append_options(self): + mock_args = [commandname, "filename.twbx", "--append"] + args = self.parser_under_test.parse_args(mock_args) + + def test_publish_parser_replace_and_append(self): + mock_args = [commandname, "filename.twbx", "--append", "--replace"] + with self.assertRaises(SystemExit): + args = self.parser_under_test.parse_args(mock_args) + + def test_publish_parser_replace_options(self): + mock_args = [commandname, "filename.twbx", "--overwrite"] + args = self.parser_under_test.parse_args(mock_args) + + mock_args = [commandname, "filename.twbx", "--replace"] + args = self.parser_under_test.parse_args(mock_args) + + mock_args = [commandname, "filename.twbx", "--replace", "--overwrite"] + args = self.parser_under_test.parse_args(mock_args) + + def test_publish_parser_deprecated_options(self): + # does nothing in new tabcmd, but shouldn't break anything + mock_args = [commandname, "filename.twbx", "--disable-uploader"] + args = self.parser_under_test.parse_args(mock_args) + mock_args = [commandname, "filename.twbx", "--restart", "argument"] + args = self.parser_under_test.parse_args(mock_args) + + def test_publish_parser_use_bridge_option(self): + mock_args = [commandname, "filename.twbx", "--use-tableau-bridge"] + args = self.parser_under_test.parse_args(mock_args) From c2857e9f2f9ea423ea54b01e3dd1f572a4dc35f1 Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 24 Mar 2023 13:54:27 -0700 Subject: [PATCH 10/14] proxy settings (#241) * implement --proxy, --no-proxy options * implement but not tested --use-certificate option * tweak output * specify black version --- pyproject.toml | 2 +- tabcmd/__main__.py | 5 +- tabcmd/commands/auth/session.py | 86 ++++++++--- tabcmd/execution/parent_parser.py | 2 +- tests/commands/test_session.py | 234 ++++++++++++++++-------------- 5 files changed, 195 insertions(+), 134 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17ac9ecf..a6e022b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ dependencies = [ ] [project.optional-dependencies] test = [ - "black", + "black>=22,<23", "mock", "mypy", "pytest>=7.0", diff --git a/tabcmd/__main__.py b/tabcmd/__main__.py index 50eace8f..5bd5fb1f 100644 --- a/tabcmd/__main__.py +++ b/tabcmd/__main__.py @@ -3,9 +3,8 @@ try: from tabcmd.tabcmd import main except ImportError as e: - print(sys.stderr, e) - print(sys.stderr, "Tabcmd needs to be run as a module, it cannot be run as a script") - print(sys.stderr, "Try running python -m tabcmd") + print("Exception thrown running program: `{}`, `{}`".format(e, e.__context__), file=sys.stderr) + print("[Possible cause: Tabcmd needs to be run as a module, try running `python -m tabcmd`]", file=sys.stderr) sys.exit(1) if __name__ == "__main__": diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 1dcf34a7..87101791 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -43,7 +43,7 @@ def __init__(self): self.no_prompt = False self.certificate = None self.no_certcheck = False - self.no_proxy = True + self.no_proxy = False self.proxy = None self.timeout = None @@ -73,10 +73,10 @@ def _update_session_data(self, args): self.token_name = args.token_name or self.token_name self.token_value = args.token_value or self.token_value - self.no_prompt = args.no_prompt or self.no_prompt + self.no_prompt = args.no_prompt # have to set this on every call? self.certificate = args.certificate or self.certificate - self.no_certcheck = args.no_certcheck or self.no_certcheck - self.no_proxy = args.no_proxy or self.no_proxy + self.no_certcheck = args.no_certcheck # have to set this on every call? + self.no_proxy = args.no_proxy # have to set this on every call? self.proxy = args.proxy or self.proxy self.timeout = self.timeout_as_integer(self.logger, args.timeout, self.timeout) @@ -150,28 +150,45 @@ def _create_new_token_credential(self): else: Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token name")) - def _set_connection_options(self) -> TSC.Server: + def _open_connection_with_opts(self) -> TSC.Server: self.logger.debug("Setting up request options") - # args still to be handled here: - # proxy, --no-proxy, - # cert http_options: Dict[str, Any] = {"headers": {"User-Agent": "Tabcmd/{}".format(version)}} + if self.no_certcheck: http_options["verify"] = False urllib3.disable_warnings(category=InsecureRequestWarning) + + """ + Do we want to do the same format check as old tabcmd? + For now I think we can trust requests to handle a bad proxy + Pattern pattern = Pattern.compile("([^:]*):([0-9]*)"); + if not matches: + throw new ReportableException(m_i18n.getString("sessionoptions.errors.bad_proxy_format", proxyArg)); + """ if self.proxy: - # do we catch this error? "sessionoptions.errors.bad_proxy_format" - self.logger.debug("Setting proxy: ", self.proxy) + self.logger.debug("Setting http proxy: {}".format(self.proxy)) + proxies = {"http": self.proxy} + http_options["proxies"] = proxies + if self.no_proxy: + # override any proxy that was set + http_options["proxies"] = None + if self.timeout: http_options["timeout"] = self.timeout + + if self.certificate: + http_options["cert"] = self.certificate + try: self.logger.debug(http_options) + # this is the only place we open a connection to the server + # so the request options are all set for the session now tableau_server = TSC.Server(self.server_url, http_options=http_options) except Exception as e: self.logger.debug( - "Connection args: server {}, site {}, proxy {}, cert {}".format( - self.server_url, self.site_name, self.proxy, self.certificate + "Connection args: server {}, site {}, proxy {}/no-proxy {}, cert {}".format( + self.server_url, self.site_name, self.proxy, self.no_proxy, self.certificate ) ) Errors.exit_with_error(self.logger, "Failed to connect to server", e) @@ -196,11 +213,10 @@ def _verify_server_connection_unauthed(self): Errors.exit_with_error(self.logger, exception=e) def _create_new_connection(self) -> TSC.Server: - self.logger.info(_("session.new_session")) self._print_server_info() self.logger.info(_("session.connecting")) try: - self.tableau_server = self._set_connection_options() + self.tableau_server = self._open_connection_with_opts() except Exception as e: Errors.exit_with_error(self.logger, "Failed to connect to server", e) return self.tableau_server @@ -211,22 +227,33 @@ def _read_existing_state(self): def _print_server_info(self): self.logger.info("===== Server: {}".format(self.server_url)) + if self.proxy: + self.logger.info("===== Proxy: {}".format(self.proxy)) if self.username: self.logger.info("===== Username: {}".format(self.username)) + if self.certificate: + self.logger.info("===== Certificate: {}".format(self.certificate)) else: self.logger.info("===== Token Name: {}".format(self.token_name)) site_display_name = self.site_name or "Default Site" self.logger.info(_("dataconnections.classes.tableau_server_site") + ": {}".format(site_display_name)) + # side-effect: sets self.username def _validate_existing_signin(self): - self.logger.info(_("session.continuing_session")) # when do these two messages show up? self.logger.info(_("session.auto_site_login")) try: if self.tableau_server and self.tableau_server.is_signed_in(): - response = self.tableau_server.users.get_by_id(self.user_id) - self.logger.debug(response) - if response.status_code.startswith("200"): - return self.tableau_server + server_user = self.tableau_server.users.get_by_id(self.user_id).name + if not self.username: + self.username = server_user + if not self.username == server_user: + Errors.exit_with_error( + self.logger, + message="Local username `{}` does not match server username `{}`".format( + self.username, server_user + ), + ) + return self.tableau_server except TSC.ServerResponseError as e: self.logger.info(_("publish.errors.unexpected_server_response"), e) except Exception as e: @@ -245,12 +272,14 @@ def _sign_in(self, tableau_auth) -> TSC.Server: self.site_id = self.tableau_server.site_id self.user_id = self.tableau_server.user_id self.auth_token = self.tableau_server._auth_token - if not self.username: - self.username = self.tableau_server.users.get_by_id(self.user_id).name - self.logger.info(_("common.output.succeeded")) + success = self._validate_existing_signin() except Exception as e: Errors.exit_with_error(self.logger, exception=e) - self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) + if success: + self.logger.info(_("common.output.succeeded")) + else: + Errors.exit_with_error(self.logger, message="Sign in failed") + return self.tableau_server def _get_saved_credentials(self): @@ -290,6 +319,7 @@ def create_session(self, args, logger): else: # no login arguments given - look for saved info # maybe we're already signed in! if self.tableau_server: + self.logger.info(_("session.continuing_session")) signed_in_object = self._validate_existing_signin() self.logger.debug(signed_in_object) # or maybe we at least have the credentials saved @@ -297,7 +327,7 @@ def create_session(self, args, logger): credentials = self._get_saved_credentials() if credentials and not signed_in_object: - # logging in, not using an existing session + self.logger.debug("We are not logged in yet but we have credentials to log in with") self.tableau_server = self._create_new_connection() self._verify_server_connection_unauthed() signed_in_object = self._sign_in(credentials) @@ -358,7 +388,12 @@ def _read_from_json(self): try: with open(str(file_path), "r") as file_contents: data = json.load(file_contents) + if data is None or data == {}: + return content = data["tableau_auth"] + if content is None: + return + self._save_data_from_json(content) except json.JSONDecodeError as e: self._wipe_bad_json(e, "Error reading data from session file") except IOError as e: @@ -368,8 +403,11 @@ def _read_from_json(self): except Exception as e: self._wipe_bad_json(e, "Unexpected error reading session details from file") + def _save_data_from_json(self, content): try: auth = content[0] + if auth is None: + self._wipe_bad_json(ValueError(), "Empty session file") self.auth_token = auth["auth_token"] self.server_url = auth["server"] self.site_name = auth["site_name"] diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index ba379619..bc81f59c 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -85,7 +85,7 @@ def parent_parser_with_global_options(): ) proxy_group.add_argument( "--no-proxy", - action="store_false", + action="store_true", # default to false help=_("session.options.no-proxy"), ) diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 5f4c8297..dfecc3a3 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -2,10 +2,9 @@ from argparse import Namespace import unittest from unittest import mock -from unittest.mock import patch, mock_open +from unittest.mock import MagicMock from tabcmd.commands.auth.session import Session -import os args_to_mock = Namespace( username=None, @@ -50,9 +49,9 @@ logger = logging.getLogger("tests") -def _set_mocks_for_json_file_saved_username(mock_json_load, auth_token, username): +def _set_mocks_for_json_file_saved_username(mock_json_lib, auth_token, username): mock_auth = vars(mock_data_from_json) - mock_json_load.return_value = {"tableau_auth": [mock_auth]} + mock_json_lib.load.return_value = {"tableau_auth": [mock_auth]} mock_auth["auth_token"] = auth_token mock_auth["username"] = username mock_auth["server"] = fakeserver @@ -60,27 +59,28 @@ def _set_mocks_for_json_file_saved_username(mock_json_load, auth_token, username mock_auth["no_certcheck"] = True -def _set_mocks_for_json_file_exists(mock_path, does_it_exist=True): - os.path = mock_path - mock_path.expanduser.return_value = "" - mock_path.join.return_value = "" - mock_path.exists.return_value = does_it_exist - return mock_path +def _set_mocks_for_json_file_exists(mock_path, mock_json_lib, does_it_exist=True): + mock_json_lib.JSONDecodeError = ValueError + path = mock_path() + path.expanduser.return_value = "" + path.join.return_value = "" + path.exists.return_value = does_it_exist - -def _set_mock_file_content(mock_load, expected_content): - mock_load.return_value = expected_content - return mock_load + mock_auth = vars(mock_data_from_json) + if does_it_exist: + mock_json_lib.load.return_value = [mock_auth] + else: + mock_json_lib.load.return_value = None + return path -@mock.patch("json.dump") -@mock.patch("json.load") +@mock.patch("tabcmd.commands.auth.session.json") @mock.patch("os.path") @mock.patch("builtins.open") class JsonTests(unittest.TestCase): - def test_read_session_from_json(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) - _set_mocks_for_json_file_saved_username(mock_load, "AUTHTOKEN", "USERNAME") + def test_read_session_from_json(self, mock_open, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) + _set_mocks_for_json_file_saved_username(mock_json, "AUTHTOKEN", "USERNAME") test_session = Session() test_session._read_from_json() assert hasattr(test_session.auth_token, "AUTHTOKEN") is False, test_session @@ -88,16 +88,16 @@ def test_read_session_from_json(self, mock_open, mock_path, mock_load, mock_dump assert test_session.username == "USERNAME" assert test_session.server_url == fakeserver, test_session.server_url - def test_save_session_to_json(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) + def test_save_session_to_json(self, mock_open, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) test_session = Session() test_session.username = "USN" test_session.server = "SRVR" test_session._save_session_to_json() - assert mock_dump.was_called() + assert mock_json.dump.was_called() - def clear_session(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) + def clear_session(self, mock_open, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) test_session = Session() test_session.username = "USN" test_session.server = "SRVR" @@ -105,13 +105,13 @@ def clear_session(self, mock_open, mock_path, mock_load, mock_dump): assert test_session.username is None assert test_session.server is None - def test_json_not_present(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path, False) + def test_json_not_present(self, mock_open, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) assert mock_open.was_not_called() - def test_json_invalid(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) - _set_mock_file_content(mock_load, "just a string") + def test_json_invalid(self, mock_open, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) + mock_json.load = "just a string" test_session = Session() assert test_session.username is None @@ -215,34 +215,30 @@ def test_dont_show_prompt_if_user_said_no(self): """ -@mock.patch("json.dump") -@mock.patch("json.load") +@mock.patch("tabcmd.commands.auth.session.json") @mock.patch("os.path") @mock.patch("builtins.open") @mock.patch("getpass.getpass") class CreateSessionTests(unittest.TestCase): @mock.patch("tableauserverclient.Server") - def test_create_session_first_time_no_args( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump - ): - mock_path = _set_mocks_for_json_file_exists(mock_path, False) - assert mock_path.exists("anything") is False + def test_create_session_first_time_no_args(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) + test_args = Namespace(**vars(args_to_mock)) - mock_tsc().users.get_by_id.return_value = None new_session = Session() with self.assertRaises(SystemExit): auth = new_session.create_session(test_args, None) @mock.patch("tableauserverclient.Server") - def test_create_session_first_time_with_token_arg( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump - ): - mock_path = _set_mocks_for_json_file_exists(mock_path, False) - assert mock_path.exists("anything") is False + def test_create_session_first_time_with_token_arg(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) + new_session = Session() + new_session.tableau_server = mock_tsc() + _set_mock_signin_validation_succeeds(new_session.tableau_server, "u") + test_args = Namespace(**vars(args_to_mock)) test_args.token_name = "tn" test_args.token_value = "foo" - new_session = Session() auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token @@ -251,46 +247,52 @@ def test_create_session_first_time_with_token_arg( assert new_session.token_name == "tn", new_session @mock.patch("tableauserverclient.Server") - def test_create_session_first_time_with_password_arg( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump - ): - mock_path = _set_mocks_for_json_file_exists(mock_path, False) + def test_create_session_first_time_with_password_arg(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json): + name = "uuuu" + new_session = Session() + new_session.tableau_server = mock_tsc() + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) + _set_mock_signin_validation_succeeds(new_session.tableau_server, name) + test_args = Namespace(**vars(args_to_mock)) - test_args.username = "uuuu" + test_args.username = name test_args.password = "pppp" - new_session = Session() auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token - assert new_session.username == "uuuu", new_session + assert new_session.username == name, new_session assert mock_tsc.has_been_called() @mock.patch("tableauserverclient.Server") def test_create_session_first_time_with_password_file_as_password( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump + self, mock_tsc, mock_pass, mock_file, mock_path, mock_json ): - mock_path = _set_mocks_for_json_file_exists(mock_path, False) + username = "uuuu" + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) + new_session = Session() + _set_mock_signin_validation_succeeds(mock_tsc(), username) test_args = Namespace(**vars(args_to_mock)) - test_args.username = "uuuu" + test_args.username = username # filename = os.path.join(os.path.dirname(__file__),"test_credential_file.txt") # test_args.password_file = os.getcwd()+"/test_credential_file.txt" test_args.password_file = "filename" with mock.patch("builtins.open", mock.mock_open(read_data="my_password")): - new_session = Session() auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token - assert new_session.username == "uuuu", new_session + assert new_session.username == username, new_session assert new_session.password_file == "filename", new_session assert mock_tsc.has_been_called() @mock.patch("tableauserverclient.Server") def test_create_session_first_time_with_password_file_as_token( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump + self, mock_tsc, mock_pass, mock_file, mock_path, mock_json ): - mock_path = _set_mocks_for_json_file_exists(mock_path, False) + _set_mocks_for_json_file_exists(mock_path, mock_json, does_it_exist=False) + server = mock_tsc() + _set_mock_signin_validation_succeeds(server, "testing") test_args = Namespace(**vars(args_to_mock)) test_args.token_name = "mytoken" test_args.password_file = "filename" @@ -304,9 +306,9 @@ def test_create_session_first_time_with_password_file_as_token( assert mock_tsc.has_been_called() @mock.patch("tableauserverclient.Server") - def test_load_saved_session_data(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump): - _set_mocks_for_json_file_exists(mock_path, True) - _set_mocks_for_json_file_saved_username(mock_json_load, "auth_token", "username") + def test_load_saved_session_data(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) + _set_mocks_for_json_file_saved_username(mock_json, "auth_token", "username") test_args = Namespace(**vars(args_to_mock)) new_session = Session() new_session._read_existing_state() @@ -317,11 +319,9 @@ def test_load_saved_session_data(self, mock_tsc, mock_pass, mock_file, mock_path assert mock_tsc.has_been_called() @mock.patch("tableauserverclient.Server") - def test_create_session_with_active_session_saved( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump - ): - _set_mocks_for_json_file_exists(mock_path, True) - _set_mocks_for_json_file_saved_username(mock_json_load, "auth_token", None) + def test_create_session_with_active_session_saved(self, mock_tsc, mock_pass, mock_file, mock_path, mock_json): + _set_mocks_for_json_file_exists(mock_path, mock_json) + _set_mocks_for_json_file_saved_username(mock_json, "auth_token", None) test_args = Namespace(**vars(args_to_mock)) test_args.token_value = "tn" test_args.token_name = "tnnnn" @@ -335,12 +335,14 @@ def test_create_session_with_active_session_saved( @mock.patch("tableauserverclient.Server") def test_create_session_with_saved_expired_username_session( - self, mock_tsc, mock_pass, mock_file, mock_path, mock_json_load, mock_json_dump + self, mock_tsc, mock_pass, mock_file, mock_path, mock_json ): - _set_mocks_for_json_file_saved_username(mock_json_load, "auth_token", "username") - _set_mocks_for_json_file_exists(mock_path, True) - tsc_under_test = CreateSessionTests._set_mock_tsc_not_signed_in(mock_tsc) - CreateSessionTests._set_mock_tsc_sign_in_succeeds(tsc_under_test) + test_username = "monster" + server = mock_tsc() + _set_mocks_for_json_file_exists(mock_path, mock_json) + _set_mocks_for_json_file_saved_username(mock_json, "auth_token", test_username) + _set_mock_tsc_sign_in_succeeds(server) + _set_mock_signin_validation_succeeds(server, test_username) test_args = Namespace(**vars(args_to_mock)) mock_pass.getpass.return_value = "success" test_args.password = "eqweqwe" @@ -350,24 +352,31 @@ def test_create_session_with_saved_expired_username_session( assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token assert auth.auth_token == "cookiieeeee" - assert new_session.username == "username", new_session + assert new_session.username == test_username, new_session assert mock_tsc.use_server_version.has_been_called() - @staticmethod - def _set_mock_tsc_not_signed_in(mock_tsc): - tsc_in_test = mock.MagicMock(name="manually mocking tsc") - mock_tsc.return_value = tsc_in_test - tsc_in_test.is_signed_in.return_value = False # CreateSessionTests.return_False - tsc_in_test.server_info.get.return_value = Exception - return tsc_in_test - @staticmethod - def _set_mock_tsc_sign_in_succeeds(mock_tsc): - tscauth_mock = mock.MagicMock(name="tsc.auth") - mock_tsc.auth = tscauth_mock - mock_tsc.auth_token = "cookiieeeee" - mock_tsc.site_id = "1" - mock_tsc.user_id = "0" +def _set_mock_tsc_not_signed_in(mock_tsc): + tsc_in_test = mock.MagicMock(name="manually mocking tsc") + tsc_in_test.is_signed_in.return_value = False # CreateSessionTests.return_False + tsc_in_test.server_info.get.return_value = Exception + return tsc_in_test + + +def _set_mock_tsc_sign_in_succeeds(mock_tsc): + tscauth_mock = mock.MagicMock(name="tsc.auth") + mock_tsc.auth = tscauth_mock + mock_tsc.auth_token = "cookiieeeee" + mock_tsc.site_id = "1" + mock_tsc.user_id = "0" + + +def _set_mock_signin_validation_succeeds(mock_tsc, username): + mock_u_factory = MagicMock("user") + mock_u = mock_u_factory() + mock_tsc.users.get_by_id.return_value = mock_u + mock_u.name = username + mock_tsc.is_signed_in.return_value = True class TimeoutArgTests(unittest.TestCase): @@ -392,7 +401,7 @@ class TimeoutIntegrationTest(unittest.TestCase): def test_connection_times_out(self): test_args = Namespace(**vars(args_to_mock)) new_session = Session() - test_args.timeout = 10 + test_args.timeout = 2 test_args.username = "u" test_args.password = "p" @@ -403,36 +412,51 @@ def test_connection_times_out(self): # should test connection doesn't time out? -@mock.patch("tableauserverclient.Server") class ConnectionOptionsTest(unittest.TestCase): - def test_certcheck_on(self, mock_tsc): - mock_tsc.add_http_options = mock.MagicMock() - mock_session = mock.MagicMock() + def test_user_agent(self): + mock_session = Session() + mock_session.server_url = "fakehost" + connection = mock_session._open_connection_with_opts() + assert connection._http_options["headers"]["User-Agent"].startswith("Tabcmd/") + + def test_no_certcheck(self): + mock_session = Session() + mock_session.server_url = "fakehost" mock_session.no_certcheck = True mock_session.site_id = "s" mock_session.user_id = "u" - server = "anything" - mock_session._set_connection_options() - assert mock_tsc.add_http_options.has_been_called() + connection = mock_session._open_connection_with_opts() + assert connection._http_options["verify"] == False - def test_certcheck_off(self, mock_tsc): - mock_session = mock.MagicMock() - server = "anything" + def test_cert(self): + mock_session = Session() + mock_session.server_url = "fakehost" mock_session.site_id = "s" mock_session.user_id = "u" - mock_session._set_connection_options() - mock_tsc.add_http_options.assert_not_called() + mock_session.certificate = "my-cert-info" + connection = mock_session._open_connection_with_opts() + assert connection._http_options["cert"] == mock_session.certificate - """ - def test_cert(self, mock_tsc): - assert False, 'feature not implemented' + def test_proxy_stuff(self): + mock_session = Session() + mock_session.server_url = "fakehost" + mock_session.site_id = "s" + mock_session.user_id = "u" + mock_session.proxy = "proxy:port" + connection = mock_session._open_connection_with_opts() + assert connection._http_options["proxies"] == {"http": mock_session.proxy} - def test_proxy_stuff(self, mock_tsc): - assert False, 'feature not implemented' + def test_timeout(self): + mock_session = Session() + mock_session.server_url = "fakehost" + mock_session.site_id = "s" + mock_session.user_id = "u" + mock_session.timeout = 10 + connection = mock_session._open_connection_with_opts() + assert connection._http_options["timeout"] == 10 - def test_timeout(self, mock_tsc): - assert False, 'feature not implemented' +""" class CookieTests(unittest.TestCase): def test_no_file_if_no_cookie(self): From fc992e4a8b4e82e8d5034fe6db8f8b21e5a9e4bb Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 26 Apr 2023 15:22:19 -0700 Subject: [PATCH 11/14] freeze tsc dependency (#248) * freeze tsc dependency to 0.25 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6e022b4..4ead88bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "types-mock", "types-requests", "types-setuptools", - "tableauserverclient>=0.23", + "tableauserverclient==0.25", "urllib3>=1.24.3,<2.0", ] [project.optional-dependencies] From 248aa850422b75c74061397b7662898d6591b0d4 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 27 Apr 2023 15:16:23 -0700 Subject: [PATCH 12/14] add --token-file option (#243) * Update session.py * update test for new functionality --- tabcmd/commands/auth/session.py | 21 ++++++++++----------- tests/commands/test_session.py | 5 +++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 87101791..a8b480eb 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -34,6 +34,7 @@ def __init__(self): self.token_name = None self.token_value = None self.password_file = None + self.token_file = None self.site_name = None # The site name, e.g 'alpodev' self.site_id = None # The site id, e.g 'abcd-1234-1234-1244-1234' self.server_url = None @@ -69,7 +70,8 @@ def _update_session_data(self, args): if self.site_name == "default": self.site_name = "" self.logging_level = args.logging_level or self.logging_level - self.password_file = args.password_file + self.password_file = args.password_file or self.password_file + self.token_file = args.token_file or self.token_file self.token_name = args.token_name or self.token_name self.token_value = args.token_value or self.token_value @@ -136,8 +138,8 @@ def _create_new_credential(self, password, credential_type): def _create_new_token_credential(self): if self.token_value: token = self.token_value - elif self.password_file: - token = Session._read_password_from_file(self.password_file) + elif self.token_file: + token = Session._read_password_from_file(self.token_file) elif self._allow_prompt(): token = getpass.getpass("Token:") else: @@ -303,17 +305,11 @@ def create_session(self, args, logger): self.logger = logger or log(__class__.__name__, self.logging_level) credentials = None - if args.password: + if args.password or args.password_file: self._end_session() # we don't save the password anywhere, so we pass it along directly credentials = self._create_new_credential(args.password, Session.PASSWORD_CRED_TYPE) - elif args.password_file: - self._end_session() - if args.username: - credentials = self._create_new_credential(args.password, Session.PASSWORD_CRED_TYPE) - else: - credentials = self._create_new_credential(args.password, Session.TOKEN_CRED_TYPE) - elif args.token_value: + elif args.token_value or args.token_file: self._end_session() credentials = self._create_new_token_credential() else: # no login arguments given - look for saved info @@ -363,6 +359,7 @@ def _clear_data(self): self.server = None self.last_login_using = None self.password_file = None + self.token_file = None self.last_command = None self.tableau_server = None @@ -418,6 +415,7 @@ def _save_data_from_json(self, content): self.token_value = auth["personal_access_token"] self.last_login_using = auth["last_login_using"] self.password_file = auth["password_file"] + self.token_file = auth["token_file"] self.no_prompt = auth["no_prompt"] self.no_certcheck = auth["no_certcheck"] self.certificate = auth["certificate"] @@ -466,6 +464,7 @@ def _serialize_for_save(self): "personal_access_token": self.token_value, "last_login_using": self.last_login_using, "password_file": self.password_file, + "token_file": self.token_file, "no_prompt": self.no_prompt, "no_certcheck": self.no_certcheck, "certificate": self.certificate, diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index dfecc3a3..14ce7b45 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -14,6 +14,7 @@ server=None, token_name=None, token_value=None, + token_file=None, logging_level=None, no_certcheck=None, no_prompt=False, @@ -295,14 +296,14 @@ def test_create_session_first_time_with_password_file_as_token( _set_mock_signin_validation_succeeds(server, "testing") test_args = Namespace(**vars(args_to_mock)) test_args.token_name = "mytoken" - test_args.password_file = "filename" + test_args.token_file = "filename" with mock.patch("builtins.open", mock.mock_open(read_data="my_token")): new_session = Session() auth = new_session.create_session(test_args, None) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token - assert new_session.password_file == "filename", new_session + assert new_session.token_file == "filename", new_session assert mock_tsc.has_been_called() @mock.patch("tableauserverclient.Server") From f8d22e517c822da1d651e611ceb3b6fd9baa1aef Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 26 Jul 2023 23:59:54 -0700 Subject: [PATCH 13/14] Merge down changes from main (#258) * freeze tsc dependency (#248) (#249) --- .github/workflows/package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index e26db65a..a0b41ea2 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -68,4 +68,5 @@ jobs: - uses: actions/upload-artifact@v3 with: + name: ${{ matrix.OUT_FILE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }} From 4741edb31000f05d58bea9e35acbfb3eaa9d266f Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 27 Jul 2023 00:29:00 -0700 Subject: [PATCH 14/14] Remove duplicated tests in merge Also change a print statement to log statement --- tabcmd/commands/auth/session.py | 17 +++++----------- .../publish_command.py | 3 ++- tests/commands/test_session.py | 20 ------------------- tests/e2e/setup_e2e.py | 2 +- 4 files changed, 8 insertions(+), 34 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index a8b480eb..ba824fca 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -247,14 +247,9 @@ def _validate_existing_signin(self): if self.tableau_server and self.tableau_server.is_signed_in(): server_user = self.tableau_server.users.get_by_id(self.user_id).name if not self.username: + self.logger.info("Fetched user details from server") self.username = server_user - if not self.username == server_user: - Errors.exit_with_error( - self.logger, - message="Local username `{}` does not match server username `{}`".format( - self.username, server_user - ), - ) + return self.tableau_server except TSC.ServerResponseError as e: self.logger.info(_("publish.errors.unexpected_server_response"), e) @@ -291,8 +286,7 @@ def _get_saved_credentials(self): credentials = self._create_new_token_credential() else: return None - if credentials: - self.logger.info(_("session.options.password-file")) + return credentials # external entry point: @@ -317,13 +311,12 @@ def create_session(self, args, logger): if self.tableau_server: self.logger.info(_("session.continuing_session")) signed_in_object = self._validate_existing_signin() - self.logger.debug(signed_in_object) - # or maybe we at least have the credentials saved + if not signed_in_object: credentials = self._get_saved_credentials() if credentials and not signed_in_object: - self.logger.debug("We are not logged in yet but we have credentials to log in with") + self.logger.debug("Signin details found:") self.tableau_server = self._create_new_connection() self._verify_server_connection_unauthed() signed_in_object = self._sign_in(credentials) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 5aaf7db2..99c4ea25 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -74,7 +74,8 @@ def run_command(args): new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) try: - print(creds) + if creds: + logger.debug("Workbook credentials object: " + creds) new_workbook = server.workbooks.publish( new_workbook, args.filename, diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index d14cee32..44b20a8a 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -120,26 +120,6 @@ def test_json_invalid(self, mock_open, mock_path, mock_json): test_session = Session() assert test_session.username is None - def clear_session(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) - test_session = Session() - test_session.username = "USN" - test_session.server = "SRVR" - test_session._clear_data() - assert test_session.username is None - assert test_session.server is None - - def test_json_not_present(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path, False) - assert mock_open.was_not_called() - - def test_json_invalid(self, mock_open, mock_path, mock_load, mock_dump): - _set_mocks_for_json_file_exists(mock_path) - _set_mock_file_content(mock_load, "just a string") - test_session = Session() - assert test_session.username is None - - @mock.patch("getpass.getpass") class BuildCredentialsTests(unittest.TestCase): @classmethod diff --git a/tests/e2e/setup_e2e.py b/tests/e2e/setup_e2e.py index 4edecf7c..29213fe7 100644 --- a/tests/e2e/setup_e2e.py +++ b/tests/e2e/setup_e2e.py @@ -24,7 +24,7 @@ def login(extra="--language", value="en"): credentials.server, "--site", credentials.site, - "--token", + "--token-value", credentials.token, "--token-name", credentials.token_name,