def shlex_process_stdin(process_command, helptext): """ Use shlex to process stdin line-by-line. Also prints help text. Requires that @process_command be a Click command object, used for processing single lines of input. helptext is prepended to the standard message printed to interactive sessions. """ # if input is interactive, print help to stderr if sys.stdin.isatty(): safeprint( ( "{}\n".format(helptext) + "Lines are split with shlex in POSIX mode: " "https://docs.python.org/library/shlex.html#parsing-rules\n" "Terminate input with Ctrl+D or <EOF>\n" ), write_to_stderr=True, ) # use readlines() rather than implicit file read line looping to force # python to properly capture EOF (otherwise, EOF acts as a flush and # things get weird) for line in sys.stdin.readlines(): # get the argument vector: # do a shlex split to handle quoted paths with spaces in them # also lets us have comments with # argv = shlex.split(line, comments=True) if argv: try: process_command.main(args=argv) except SystemExit as e: if e.code != 0: raise
def check_completed(): completed = client.task_wait( task_id, timeout=polling_interval, polling_interval=polling_interval ) if completed: if heartbeat: safeprint("", write_to_stderr=True) # meowing tasks wake up! if meow: safeprint( r""" _.. /}_{\ /.-' ( a a )-.___...-'/ ==._.== ; \ i _..._ /, {_;/ {_//""", write_to_stderr=True, ) # TODO: possibly update TransferClient.task_wait so that we don't # need to do an extra fetch to get the task status after completion res = client.get_task(task_id) formatted_print(res, text_format=FORMAT_SILENT) status = res["status"] if status == "SUCCEEDED": click.get_current_context().exit(0) else: click.get_current_context().exit(1) return completed
def resolve_id_or_name(client, bookmark_id_or_name): # leading/trailing whitespace doesn't make sense for UUIDs and the Transfer # service outright forbids it for bookmark names, so we can strip it off bookmark_id_or_name = bookmark_id_or_name.strip() res = None try: UUID(bookmark_id_or_name) # raises ValueError if argument not a UUID except ValueError: pass else: try: res = client.get_bookmark(bookmark_id_or_name.lower()) except TransferAPIError as exception: if exception.code != 'BookmarkNotFound': raise if not res: # non-UUID input or UUID not found; fallback to match by name try: # n.b. case matters to the Transfer service for bookmark names, so # two bookmarks can exist whose names vary only by their case res = next(bookmark_row for bookmark_row in client.bookmark_list() if bookmark_row['name'] == bookmark_id_or_name) except StopIteration: safeprint( u'No bookmark found for "{}"'.format(bookmark_id_or_name), write_to_stderr=True) click.get_current_context().exit(1) return res
def do_link_login_flow(): """ Prompts the user with a link to authorize the CLI to act on their behalf. """ # get the NativeApp client object native_client = internal_auth_client() # start the Native App Grant flow, prefilling the # named grant label on the consent page if we can get a # hostname for the local system label = platform.node() or None native_client.oauth2_start_flow(refresh_tokens=True, prefill_named_grant=label, requested_scopes=SCOPES) # prompt linkprompt = 'Please log into Globus here' safeprint('{0}:\n{1}\n{2}\n{1}\n'.format( linkprompt, '-' * len(linkprompt), native_client.oauth2_get_authorize_url())) # come back with auth code auth_code = click.prompt( 'Enter the resulting Authorization Code here').strip() # finish login flow exchange_code_and_store_config(native_client, auth_code)
def check_completed(): completed = client.task_wait(task_id, timeout=polling_interval, polling_interval=polling_interval) if completed: if heartbeat: safeprint('', write_to_stderr=True) # meowing tasks wake up! if meow: safeprint(r""" _.. /}_{\ /.-' ( a a )-.___...-'/ ==._.== ; \ i _..._ /, {_;/ {_//""", write_to_stderr=True) # TODO: possibly update TransferClient.task_wait so that we don't # need to do an extra fetch to get the task status after completion res = client.get_task(task_id) formatted_print(res, text_format=FORMAT_SILENT) status = res['status'] if status == 'SUCCEEDED': click.get_current_context().exit(0) else: click.get_current_context().exit(1) return completed
def whoami_command(linked_identities): """ Executor for `globus whoami` """ client = get_auth_client() # get userinfo from auth. # if we get back an error the user likely needs to log in again try: res = client.oauth2_userinfo() except AuthAPIError: safeprint( "Unable to get user information. Please try " "logging in again.", write_to_stderr=True, ) click.get_current_context().exit(1) print_command_hint( "For information on which identities are in session see\n" " globus session show\n" ) # --linked-identities either displays all usernames or a table if verbose if linked_identities: try: formatted_print( res["identity_set"], fields=[ ("Username", "username"), ("Name", "name"), ("ID", "sub"), ("Email", "email"), ], simple_text=( None if is_verbose() else "\n".join([x["username"] for x in res["identity_set"]]) ), ) except KeyError: safeprint( "Your current login does not have the consents required " "to view your full identity set. Please log in again " "to agree to the required consents.", write_to_stderr=True, ) # Default output is the top level data else: formatted_print( res, text_format=FORMAT_TEXT_RECORD, fields=[ ("Username", "preferred_username"), ("Name", "name"), ("ID", "sub"), ("Email", "email"), ], simple_text=(None if is_verbose() else res["preferred_username"]), )
def shlex_process_stdin(process_command, helptext): """ Use shlex to process stdin line-by-line. Also prints help text. Requires that @process_command be a Click command object, used for processing single lines of input. helptext is prepended to the standard message printed to interactive sessions. """ # if input is interactive, print help to stderr if sys.stdin.isatty(): safeprint(('{}\n'.format(helptext) + 'Lines are split with shlex in POSIX mode: ' 'https://docs.python.org/library/shlex.html#parsing-rules\n' 'Terminate input with Ctrl+D or <EOF>\n'), write_to_stderr=True) # use readlines() rather than implicit file read line looping to force # python to properly capture EOF (otherwise, EOF acts as a flush and # things get weird) for line in sys.stdin.readlines(): # get the argument vector: # do a shlex split to handle quoted paths with spaces in them # also lets us have comments with # argv = shlex.split(line, comments=True) if argv: try: process_command.main(args=argv) except SystemExit as e: if e.code != 0: raise
def do_link_auth_flow(session_params={}, force_new_client=False): """ Prompts the user with a link to authenticate with globus auth and authorize the CLI to act on their behalf. """ # get the ConfidentialApp client object auth_client = internal_auth_client( requires_instance=True, force_new_client=force_new_client) # start the Confidential App Grant flow auth_client.oauth2_start_flow( redirect_uri=auth_client.base_url + 'v2/web/auth-code', refresh_tokens=True, requested_scopes=SCOPES) # prompt additional_params = {"prompt": "login"} additional_params.update(session_params) linkprompt = 'Please authenticate with Globus here' safeprint('{0}:\n{1}\n{2}\n{1}\n' .format(linkprompt, '-' * len(linkprompt), auth_client.oauth2_get_authorize_url( additional_params=additional_params))) # come back with auth code auth_code = click.prompt( 'Enter the resulting Authorization Code here').strip() # finish auth flow exchange_code_and_store_config(auth_client, auth_code) return True
def autoactivate(client, endpoint_id, if_expires_in=None): """ Attempts to auto-activate the given endpoint with the given client If auto-activation fails, parses the returned activation requirements to determine which methods of activation are supported, then tells the user to use 'globus endpoint activate' with the correct options(s) """ kwargs = {} if if_expires_in is not None: kwargs["if_expires_in"] = if_expires_in res = client.endpoint_autoactivate(endpoint_id, **kwargs) if res["code"] == "AutoActivationFailed": message = ( "The endpoint could not be auto-activated and must be " "activated before it can be used.\n\n" + activation_requirements_help_text(res, endpoint_id) ) safeprint(message, write_to_stderr=True) click.get_current_context().exit(1) else: return res
def resolve_id_or_name(client, bookmark_id_or_name): # leading/trailing whitespace doesn't make sense for UUIDs and the Transfer # service outright forbids it for bookmark names, so we can strip it off bookmark_id_or_name = bookmark_id_or_name.strip() res = None try: UUID(bookmark_id_or_name) # raises ValueError if argument not a UUID except ValueError: pass else: try: res = client.get_bookmark(bookmark_id_or_name.lower()) except TransferAPIError as exception: if exception.code != "BookmarkNotFound": raise if not res: # non-UUID input or UUID not found; fallback to match by name try: # n.b. case matters to the Transfer service for bookmark names, so # two bookmarks can exist whose names vary only by their case res = next( bookmark_row for bookmark_row in client.bookmark_list() if bookmark_row["name"] == bookmark_id_or_name ) except StopIteration: safeprint( u'No bookmark found for "{}"'.format(bookmark_id_or_name), write_to_stderr=True, ) click.get_current_context().exit(1) return res
def rm_command(ignore_missing, star_silent, recursive, enable_globs, endpoint_plus_path, label, submission_id, dry_run, deadline, skip_activation_check, notify, meow, heartbeat, polling_interval, timeout): """ Executor for `globus rm` """ endpoint_id, path = endpoint_plus_path client = get_client() # attempt to activate unless --skip-activation-check is given if not skip_activation_check: autoactivate(client, endpoint_id, if_expires_in=60) delete_data = DeleteData(client, endpoint_id, label=label, recursive=recursive, ignore_missing=ignore_missing, submission_id=submission_id, deadline=deadline, skip_activation_check=skip_activation_check, interpret_globs=enable_globs, **notify) if not star_silent and enable_globs and path.endswith('*'): # not intuitive, but `click.confirm(abort=True)` prints to stdout # unnecessarily, which we don't really want... # only do this check if stderr is a pty if (err_is_terminal() and term_is_interactive() and not click.confirm( 'Are you sure you want to delete all files matching "{}"?'. format(path), err=True)): safeprint('Aborted.', write_to_stderr=True) click.get_current_context().exit(1) delete_data.add_item(path) if dry_run: formatted_print(delete_data, response_key='DATA', fields=[('Path', 'path')]) # exit safely return # Print task submission to stderr so that `-Fjson` is still correctly # respected, as it will be by `task wait` res = client.submit_delete(delete_data) task_id = res['task_id'] safeprint('Delete task submitted under ID "{}"'.format(task_id), write_to_stderr=True) # do a `task wait` equivalent, including printing and correct exit status task_wait_with_io(meow, heartbeat, polling_interval, timeout, task_id, client=client)
def list_commands(): def _print_cmd(command): # print commands with short_help indent = 4 min_space = 2 # if the output would be pinched too close together, or if the command # name would overflow, use two separate lines if len(command.name) > _command_length - min_space: safeprint(" " * indent + command.name) safeprint(" " * (indent + _command_length) + command.short_help) # otherwise, it's all cool to cram into one line, just ljust command # names so that they form a nice column else: safeprint( " " * indent + "{}{}".format(command.name.ljust(_command_length), command.short_help) ) def _print_cmd_group(command, parent_names): parents = " ".join(parent_names) if parents: parents = parents + " " safeprint("\n=== {}{} ===\n".format(parents, command.name)) def _recursive_list_commands(command, parent_names=None): if parent_names is None: parent_names = [] # names of parent commands, including this one, for passthrough to # recursive calls new_parent_names = copy.copy(parent_names) + [command.name] # group-style commands are printed as headers if isinstance(command, click.MultiCommand): _print_cmd_group(command, parent_names) # get the set of subcommands and recursively print all of them group_cmds = [ v for v in command.commands.values() if isinstance(v, click.MultiCommand) ] func_cmds = [v for v in command.commands.values() if v not in group_cmds] # we want to print them all, but func commands first for cmd in func_cmds + group_cmds: _recursive_list_commands(cmd, parent_names=new_parent_names) # individual commands are printed solo else: _print_cmd(command) # get the root context (the click context for the entire CLI tree) root_ctx = click.get_current_context().find_root() _recursive_list_commands(root_ctx.command) # get an extra newline at the end safeprint("")
def wait_for_code(self): # workaround for handling control-c interrupt. # relevant Python issue discussing this behavior: # https://bugs.python.org/issue1360 try: return self._auth_code_queue.get(block=True, timeout=3600) except Queue.Empty: safeprint("Login timed out. Please try again.", write_to_stderr=True) sys.exit(1)
def callback(ctx, param, value): if not value or ctx.resilient_parsing: return if value == "BASH": safeprint(bash_shell_completer) elif value == "ZSH": safeprint(zsh_shell_completer) else: raise ValueError('Unsupported shell completion') click.get_current_context().exit(0)
def callback(ctx, param, value): if not value or ctx.resilient_parsing: return if value == "BASH": safeprint(bash_shell_completer) elif value == "ZSH": safeprint(zsh_shell_completer) else: raise ValueError("Unsupported shell completion") click.get_current_context().exit(0)
def wait_for_code(self): # workaround for handling control-c interrupt. # relevant Python issue discussing this behavior: # https://bugs.python.org/issue1360 try: return self._auth_code_queue.get(block=True, timeout=3600) except Queue.Empty: safeprint('Login timed out. Please try again.', write_to_stderr=True) sys.exit(1)
def list_commands(): def _print_cmd(command): # print commands with short_help indent = 4 min_space = 2 # if the output would be pinched too close together, or if the command # name would overflow, use two separate lines if len(command.name) > _command_length - min_space: safeprint(' '*indent + command.name) safeprint(' '*(indent + _command_length) + command.short_help) # otherwise, it's all cool to cram into one line, just ljust command # names so that they form a nice column else: safeprint(' '*indent + '{}{}'.format( command.name.ljust(_command_length), command.short_help)) def _print_cmd_group(command, parent_names): parents = ' '.join(parent_names) if parents: parents = parents + ' ' safeprint('\n=== {}{} ===\n'.format(parents, command.name)) def _recursive_list_commands(command, parent_names=None): if parent_names is None: parent_names = [] # names of parent commands, including this one, for passthrough to # recursive calls new_parent_names = copy.copy(parent_names) + [command.name] # group-style commands are printed as headers if isinstance(command, click.MultiCommand): _print_cmd_group(command, parent_names) # get the set of subcommands and recursively print all of them group_cmds = [v for v in command.commands.values() if isinstance(v, click.MultiCommand)] func_cmds = [v for v in command.commands.values() if v not in group_cmds] # we want to print them all, but func commands first for cmd in (func_cmds + group_cmds): _recursive_list_commands(cmd, parent_names=new_parent_names) # individual commands are printed solo else: _print_cmd(command) # get the root context (the click context for the entire CLI tree) root_ctx = click.get_current_context().find_root() _recursive_list_commands(root_ctx.command) # get an extra newline at the end safeprint('')
def filename_command(): """ Executor for `globus config filename` """ try: config = get_config_obj(file_error=True) except IOError as e: safeprint(e, write_to_stderr=True) click.get_current_context().exit(1) else: safeprint(config.filename)
def invoke(self, ctx): # if no subcommand was given (but, potentially, flags were passed), # ctx.protected_args will be empty # improves upon the built-in detection given on click.Group by # no_args_is_help , since that treats options (without a subcommand) as # being arguments and blows up with a "Missing command" failure # for reference to the original version (as of 2017-02-26): # https://github.com/pallets/click/blob/02ea9ee7e864581258b4902d6e6c1264b0226b9f/click/core.py#L1039-L1052 if self.no_args_is_help and not ctx.protected_args: safeprint(ctx.get_help()) ctx.exit() return super(GlobusCommandGroup, self).invoke(ctx)
def login_command(force, no_local_server): # if not forcing, stop if user already logged in if not force and check_logged_in(): safeprint(_LOGGED_IN_RESPONSE) return # use a link login if remote session or user requested if no_local_server or is_remote_session(): do_link_login_flow() # otherwise default to a local server login flow else: do_local_server_login_flow()
def show_command(parameter): """ Executor for `globus config show` """ section = "cli" if '.' in parameter: section, parameter = parameter.split('.', 1) value = lookup_option(parameter, section=section) if value is None: safeprint('{} not set'.format(parameter)) else: safeprint('{} = {}'.format(parameter, value))
def session_hook(exception): """ Expects an exception with an authorization_paramaters field in its raw_json """ safeprint( "The resource you are trying to access requires you to " "re-authenticate with specific identities." ) params = exception.raw_json["authorization_parameters"] message = params.get("session_message") if message: safeprint("message: {}".format(message)) identities = params.get("session_required_identities") if identities: id_str = " ".join(identities) safeprint( "Please run\n\n" " globus session update {}\n\n" "to re-authenticate with the required identities".format(id_str) ) else: safeprint( 'Please use "globus session update" to re-authenticate ' "with specific identities".format(id_str) ) exit_with_mapped_status(exception.http_status)
def show_command(parameter): """ Executor for `globus config show` """ section = "cli" if "." in parameter: section, parameter = parameter.split(".", 1) value = lookup_option(parameter, section=section) if value is None: safeprint("{} not set".format(parameter)) else: safeprint("{} = {}".format(parameter, value))
def _print_as_text(): # if we're given simple text, print that and exit if simple_text is not None: safeprint(simple_text) return # if there's a preamble, print it beofre any other text if text_preamble is not None: safeprint(text_preamble) # if there's a response key, key into it data = (response_data if response_key is None else response_data[response_key]) # do the various kinds of printing if text_format == FORMAT_TEXT_TABLE: _assert_fields() print_table(data, fields) elif text_format == FORMAT_TEXT_RECORD: _assert_fields() colon_formatted_print(data, fields) elif text_format == FORMAT_TEXT_RAW: safeprint(data) elif text_format == FORMAT_TEXT_CUSTOM: _custom_text_formatter(data) # if there's an epilog, print it after any text if text_epilog is not None: safeprint(text_epilog)
def print_unix_response(res): res = _jmespath_preprocess(res) try: unix_formatted_print(res) # Attr errors indicate that we got data which cannot be unix formatted # likely a scalar + non-scalar in an array, though there may be other cases # print good error and exit(2) (Count this as UsageError!) except AttributeError: safeprint( 'UNIX formatting of output failed.' '\n ' 'This usually means that data has a structure which cannot be ' 'handled by the UNIX formatter.' '\n ' 'To avoid this error in the future, ensure that you query the ' 'exact properties you want from output data with "--jmespath"', write_to_stderr=True) click.get_current_context().exit(2)
def whoami_command(linked_identities): """ Executor for `globus whoami` """ client = get_auth_client() # get userinfo from auth. # if we get back an error the user likely needs to log in again try: res = client.oauth2_userinfo() except AuthAPIError: safeprint( 'Unable to get user information. Please try ' 'logging in again.', write_to_stderr=True) click.get_current_context().exit(1) print_command_hint( "For information on which identities are in session see\n" " globus session show\n") # --linked-identities either displays all usernames or a table if verbose if linked_identities: try: formatted_print(res["identity_set"], fields=[("Username", "username"), ("Name", "name"), ("ID", "sub"), ("Email", "email")], simple_text=(None if is_verbose() else "\n".join( [x["username"] for x in res["identity_set"]]))) except KeyError: safeprint( "Your current login does not have the consents required " "to view your full identity set. Please log in again " "to agree to the required consents.", write_to_stderr=True) # Default output is the top level data else: formatted_print( res, text_format=FORMAT_TEXT_RECORD, fields=[("Username", "preferred_username"), ("Name", "name"), ("ID", "sub"), ("Email", "email")], simple_text=(None if is_verbose() else res["preferred_username"]))
def set_command(value, parameter): """ Executor for `globus config set` """ conf = get_config_obj() section = "cli" if "." in parameter: section, parameter = parameter.split(".", 1) # ensure that the section exists if section not in conf: conf[section] = {} # set the value for the given parameter conf[section][parameter] = value # write to disk safeprint("Writing updated config to {}".format(conf.filename)) conf.write()
def remove_command(parameter): """ Executor for `globus config remove` """ conf = get_config_obj() section = "cli" if '.' in parameter: section, parameter = parameter.split('.', 1) # ensure that the section exists if section not in conf: conf[section] = {} # remove the value for the given parameter del conf[section][parameter] # write to disk safeprint('Writing updated config to {}'.format(conf.filename)) conf.write()
def do_bash_complete(): comp_line = os.environ.get("COMP_LINE", "") comp_words, quoted = safe_split_line(comp_line) cur_index = int(os.environ.get("COMP_POINT", len(comp_line))) # now, figure out the current word in the line by "parsing" # the chunk of it up to cur_index partial_comp_words, _ = safe_split_line(comp_line[:cur_index]) cur = partial_comp_words[-1] if comp_line[-1].isspace(): cur = None completed_args = comp_words[1:] else: completed_args = comp_words[1:-1] choices = [name for (name, helpstr) in get_all_choices(completed_args, cur, quoted)] safeprint("\t".join(choices), newline=False) click.get_current_context().exit(0)
def do_bash_complete(): comp_line = os.environ.get('COMP_LINE', '') comp_words, quoted = safe_split_line(comp_line) cur_index = int(os.environ.get('COMP_POINT', len(comp_line))) # now, figure out the current word in the line by "parsing" # the chunk of it up to cur_index partial_comp_words, _ = safe_split_line(comp_line[:cur_index]) cur = partial_comp_words[-1] if comp_line[-1].isspace(): cur = None completed_args = comp_words[1:] else: completed_args = comp_words[1:-1] choices = [name for (name, helpstr) in get_all_choices(completed_args, cur, quoted)] safeprint('\t'.join(choices), newline=False) click.get_current_context().exit(0)
def _custom_text_format(identities): """ Non-verbose text output is customized """ def resolve_identity(value): """ helper to deal with variable inputs and uncertain response order """ for identity in identities: if identity["id"] == value: return identity["username"] if identity["username"] == value: return identity["id"] return "NO_SUCH_IDENTITY" # standard output is one resolved identity per line in the same order # as the inputs. A resolved identity is either a username if given a # UUID vice versa, or "NO_SUCH_IDENTITY" if the identity could not be # found for val in resolved_values: safeprint(resolve_identity(val))
def init_command(default_output_format, default_myproxy_username): """ Executor for `globus config init` """ # now handle the output format, requires a little bit more care # first, prompt if it isn't given, but be clear that we have a sensible # default if they don't set it # then, make sure that if it is given, it's a valid format (discard # otherwise) # finally, set it only if given and valid if not default_output_format: safeprint( textwrap.fill( 'This must be one of "json" or "text". Other values will be ' 'ignored. ENTER to skip.')) default_output_format = click.prompt( 'Default CLI output format (cli.output_format)', default='text', ).strip().lower() if default_output_format not in ('json', 'text'): default_output_format = None if not default_myproxy_username: safeprint(textwrap.fill("ENTER to skip.")) default_myproxy_username = click.prompt( "Default myproxy username (cli.default_myproxy_username)", default="", show_default=False).strip() # write to disk safeprint('\n\nWriting updated config to {0}'.format( os.path.expanduser('~/.globus.cfg'))) write_option(OUTPUT_FORMAT_OPTNAME, default_output_format) write_option(MYPROXY_USERNAME_OPTNAME, default_myproxy_username)
def do_local_server_auth_flow(session_params=None, force_new_client=False): """ Starts a local http server, opens a browser to have the user authenticate, and gets the code redirected to the server (no copy and pasting required) """ session_params = session_params or {} # start local server and create matching redirect_uri with start_local_server(listen=("127.0.0.1", 0)) as server: _, port = server.socket.getsockname() redirect_uri = "http://localhost:{}".format(port) # get the ConfidentialApp client object and start a flow auth_client = internal_auth_client( requires_instance=True, force_new_client=force_new_client ) auth_client.oauth2_start_flow( refresh_tokens=True, redirect_uri=redirect_uri, requested_scopes=SCOPES ) additional_params = {"prompt": "login"} additional_params.update(session_params) url = auth_client.oauth2_get_authorize_url(additional_params=additional_params) # open web-browser for user to log in, get auth code webbrowser.open(url, new=1) auth_code = server.wait_for_code() if isinstance(auth_code, LocalServerError): safeprint("Authorization failed: {}".format(auth_code), write_to_stderr=True) click.get_current_context().exit(1) elif isinstance(auth_code, Exception): safeprint( "Authorization failed with unexpected error:\n{}".format(auth_code), write_to_stderr=True, ) click.get_current_context().exit(1) # finish auth flow and return true exchange_code_and_store_config(auth_client, auth_code) return True
def do_zsh_complete(): commandline = os.environ["COMMANDLINE"] comp_words, quoted = safe_split_line(commandline) comp_words = comp_words[1:] if comp_words and not commandline.endswith(" "): cur = comp_words[-1] completed_args = comp_words[:-1] else: cur = None completed_args = comp_words def clean_help(helpstr): r""" Replace " with \" ' with '"'"' ` with \` $ with \$ Because we'll put these single quote chars in '...' quotation, we need to do ' -- end single quotes "'" -- single quote string (will concatenate in ZSH) ' -- start single quotes again """ return ( helpstr.replace('"', '\\"') .replace("'", "'\"'\"'") .replace("`", "\\`") .replace("$", "\\$") ) choices = get_all_choices(completed_args, cur, quoted) choices = [ '{}\\:"{}"'.format(name, clean_help(helpstr)) for (name, helpstr) in choices ] safeprint("_arguments '*: :(({}))'".format("\n".join(choices)), newline=False)
def exchange_code_and_store_config(native_client, auth_code): """ Finishes login flow after code is gotten from command line or local server. Exchanges code for tokens and gets user info from auth. Stores tokens and user info in config. """ # do a token exchange with the given code tkn = native_client.oauth2_exchange_code_for_tokens(auth_code) tkn = tkn.by_resource_server # extract access tokens from final response transfer_at = (tkn['transfer.api.globus.org']['access_token']) transfer_at_expires = ( tkn['transfer.api.globus.org']['expires_at_seconds']) transfer_rt = (tkn['transfer.api.globus.org']['refresh_token']) auth_at = (tkn['auth.globus.org']['access_token']) auth_at_expires = (tkn['auth.globus.org']['expires_at_seconds']) auth_rt = (tkn['auth.globus.org']['refresh_token']) # get the identity that the tokens were issued to auth_client = AuthClient(authorizer=AccessTokenAuthorizer(auth_at)) res = auth_client.oauth2_userinfo() # revoke any existing tokens for token_opt in (TRANSFER_RT_OPTNAME, TRANSFER_AT_OPTNAME, AUTH_RT_OPTNAME, AUTH_AT_OPTNAME): token = lookup_option(token_opt) if token: native_client.oauth2_revoke_token(token) # write new tokens to config write_option(TRANSFER_RT_OPTNAME, transfer_rt) write_option(TRANSFER_AT_OPTNAME, transfer_at) write_option(TRANSFER_AT_EXPIRES_OPTNAME, transfer_at_expires) write_option(AUTH_RT_OPTNAME, auth_rt) write_option(AUTH_AT_OPTNAME, auth_at) write_option(AUTH_AT_EXPIRES_OPTNAME, auth_at_expires) safeprint(_LOGIN_EPILOG.format(res["preferred_username"]))
def do_zsh_complete(): commandline = os.environ['COMMANDLINE'] comp_words, quoted = safe_split_line(commandline) comp_words = comp_words[1:] if comp_words and not commandline.endswith(' '): cur = comp_words[-1] completed_args = comp_words[:-1] else: cur = None completed_args = comp_words def clean_help(helpstr): r""" Replace " with \" ' with '"'"' ` with \` $ with \$ Because we'll put these single quote chars in '...' quotation, we need to do ' -- end single quotes "'" -- single quote string (will concatenate in ZSH) ' -- start single quotes again """ return (helpstr .replace('"', '\\"') .replace("'", "'\"'\"'") .replace("`", "\\`") .replace("$", "\\$")) choices = get_all_choices(completed_args, cur, quoted) choices = ['{}\\:"{}"'.format(name, clean_help(helpstr)) for (name, helpstr) in choices] safeprint("_arguments '*: :(({}))'".format('\n'.join(choices)), newline=False)
def _custom_text_format(res): explicit_pauses = [ field for field in EXPLICIT_PAUSE_MSG_FIELDS # n.b. some keys are absent for completed tasks if res.get(field[1]) ] effective_pause_rules = res['pause_rules'] if not explicit_pauses and not effective_pause_rules: safeprint('Task {} is not paused.'.format(task_id)) click.get_current_context().exit(0) if explicit_pauses: formatted_print( res, fields=explicit_pauses, text_format=FORMAT_TEXT_RECORD, text_preamble='This task has been explicitly paused.\n', text_epilog='\n' if effective_pause_rules else None) if effective_pause_rules: formatted_print( effective_pause_rules, fields=PAUSE_RULE_DISPLAY_FIELDS, text_preamble=( 'The following pause rules are effective on this task:\n'))
def autoactivate(client, endpoint_id, if_expires_in=None): """ Attempts to auto-activate the given endpoint with the given client If auto-activation fails, parses the returned activation requirements to determine which methods of activation are supported, then tells the user to use 'globus endpoint activate' with the correct options(s) """ kwargs = {} if if_expires_in is not None: kwargs['if_expires_in'] = if_expires_in res = client.endpoint_autoactivate(endpoint_id, **kwargs) if res["code"] == "AutoActivationFailed": message = ("The endpoint could not be auto-activated and must be " "activated before it can be used.\n\n" + activation_requirements_help_text(res, endpoint_id)) safeprint(message, write_to_stderr=True) click.get_current_context().exit(1) else: return res
def do_local_server_auth_flow(session_params={}, force_new_client=False): """ Starts a local http server, opens a browser to have the user authenticate, and gets the code redirected to the server (no copy and pasting required) """ # start local server and create matching redirect_uri with start_local_server(listen=('127.0.0.1', 0)) as server: _, port = server.socket.getsockname() redirect_uri = 'http://localhost:{}'.format(port) # get the ConfidentialApp client object and start a flow auth_client = internal_auth_client( requires_instance=True, force_new_client=force_new_client) auth_client.oauth2_start_flow( refresh_tokens=True, redirect_uri=redirect_uri, requested_scopes=SCOPES) additional_params = {"prompt": "login"} additional_params.update(session_params) url = auth_client.oauth2_get_authorize_url( additional_params=additional_params) # open web-browser for user to log in, get auth code webbrowser.open(url, new=1) auth_code = server.wait_for_code() if isinstance(auth_code, LocalServerError): safeprint('Authorization failed: {}'.format(auth_code), write_to_stderr=True) click.get_current_context().exit(1) elif isinstance(auth_code, Exception): safeprint('Authorization failed with unexpected error:\n{}'. format(auth_code), write_to_stderr=True) click.get_current_context().exit(1) # finish auth flow and return true exchange_code_and_store_config(auth_client, auth_code) return True
def do_link_auth_flow(session_params=None, force_new_client=False): """ Prompts the user with a link to authenticate with globus auth and authorize the CLI to act on their behalf. """ session_params = session_params or {} # get the ConfidentialApp client object auth_client = internal_auth_client( requires_instance=True, force_new_client=force_new_client ) # start the Confidential App Grant flow auth_client.oauth2_start_flow( redirect_uri=auth_client.base_url + "v2/web/auth-code", refresh_tokens=True, requested_scopes=SCOPES, ) # prompt additional_params = {"prompt": "login"} additional_params.update(session_params) linkprompt = "Please authenticate with Globus here" safeprint( "{0}:\n{1}\n{2}\n{1}\n".format( linkprompt, "-" * len(linkprompt), auth_client.oauth2_get_authorize_url(additional_params=additional_params), ) ) # come back with auth code auth_code = click.prompt("Enter the resulting Authorization Code here").strip() # finish auth flow exchange_code_and_store_config(auth_client, auth_code) return True
def _print_cmd(command): # print commands with short_help indent = 4 min_space = 2 # if the output would be pinched too close together, or if the command # name would overflow, use two separate lines if len(command.name) > _command_length - min_space: safeprint(' '*indent + command.name) safeprint(' '*(indent + _command_length) + command.short_help) # otherwise, it's all cool to cram into one line, just ljust command # names so that they form a nice column else: safeprint(' '*indent + '{}{}'.format( command.name.ljust(_command_length), command.short_help))
def local_id(personal): """ Executor for `globus endpoint local-id` """ if personal: try: ep_id = LocalGlobusConnectPersonal().endpoint_id except IOError as e: safeprint(e, write_to_stderr=True) click.get_current_context().exit(1) if ep_id is not None: safeprint(ep_id) else: safeprint("No Globus Connect Personal installation found.") click.get_current_context().exit(1)
def local_id(personal): """ Executor for `globus endpoint local-id` """ if personal: try: ep_id = LocalGlobusConnectPersonal().endpoint_id except IOError as e: safeprint(e, write_to_stderr=True) click.get_current_context().exit(1) if ep_id is not None: safeprint(ep_id) else: safeprint('No Globus Connect Personal installation found.') click.get_current_context().exit(1)
def print_table(iterable, headers_and_keys, print_headers=True): # the iterable may not be safe to walk multiple times, so we must walk it # only once -- however, to let us write things naturally, convert it to a # list and we can assume it is safe to walk repeatedly iterable = list(iterable) # extract headers and keys as separate lists headers = [h for (h, k) in headers_and_keys] keys = [k for (h, k) in headers_and_keys] # convert all keys to keyfuncs keyfuncs = [_key_to_keyfunc(key) for key in keys] # use the iterable to find the max width of an element for each column, in # the same order as the headers_and_keys array # use a special function to handle empty iterable def get_max_colwidth(kf): def _safelen(x): try: return len(x) except TypeError: return len(str(x)) lengths = [_safelen(kf(i)) for i in iterable] if not lengths: return 0 else: return max(lengths) widths = [get_max_colwidth(kf) for kf in keyfuncs] # handle the case in which the column header is the widest thing widths = [max(w, len(h)) for w, h in zip(widths, headers)] # create a format string based on column widths format_str = u' | '.join(u'{:' + str(w) + u'}' for w in widths) def none_to_null(val): if val is None: return 'NULL' return val # print headers if print_headers: safeprint(format_str.format(*[h for h in headers])) safeprint(format_str.format(*['-'*w for w in widths])) # print the rows of data for i in iterable: safeprint(format_str.format(*[none_to_null(kf(i)) for kf in keyfuncs]))
def session_hook(exception): """ Expects an exception with an authorization_paramaters field in its raw_json """ safeprint("The resource you are trying to access requires you to " "re-authenticate with specific identities.") params = exception.raw_json["authorization_parameters"] message = params.get("session_message") if message: safeprint("message: {}".format(message)) identities = params.get("session_required_identities") if identities: id_str = " ".join(identities) safeprint("Please run\n\n" " globus session boost {}\n\n" "to re-authenticate with the required identities" .format(id_str)) else: safeprint('Please use "globus session boost" to re-authenticate ' 'with specific identities'.format(id_str)) exit_with_mapped_status(exception.http_status)
def _print_cmd(command): # print commands with short_help indent = 4 min_space = 2 # if the output would be pinched too close together, or if the command # name would overflow, use two separate lines if len(command.name) > _command_length - min_space: safeprint(" " * indent + command.name) safeprint(" " * (indent + _command_length) + command.short_help) # otherwise, it's all cool to cram into one line, just ljust command # names so that they form a nice column else: safeprint( " " * indent + "{}{}".format(command.name.ljust(_command_length), command.short_help) )
def init_command(default_output_format, default_myproxy_username): """ Executor for `globus config init` """ # now handle the output format, requires a little bit more care # first, prompt if it isn't given, but be clear that we have a sensible # default if they don't set it # then, make sure that if it is given, it's a valid format (discard # otherwise) # finally, set it only if given and valid if not default_output_format: safeprint( textwrap.fill( 'This must be one of "json" or "text". Other values will be ' "ignored. ENTER to skip." ) ) default_output_format = ( click.prompt( "Default CLI output format (cli.output_format)", default="text" ) .strip() .lower() ) if default_output_format not in ("json", "text"): default_output_format = None if not default_myproxy_username: safeprint(textwrap.fill("ENTER to skip.")) default_myproxy_username = click.prompt( "Default myproxy username (cli.default_myproxy_username)", default="", show_default=False, ).strip() # write to disk safeprint( "\n\nWriting updated config to {0}".format(os.path.expanduser("~/.globus.cfg")) ) write_option(OUTPUT_FORMAT_OPTNAME, default_output_format) write_option(MYPROXY_USERNAME_OPTNAME, default_myproxy_username)
def do_local_server_login_flow(): """ Starts a local http server, opens a browser to have the user login, and gets the code redirected to the server (no copy and pasting required) """ safeprint( "You are running 'globus login', which should automatically open " "a browser window for you to login.\n" "If this fails or you experience difficulty, try " "'globus login --no-local-server'" "\n---") # start local server and create matching redirect_uri with start_local_server(listen=('127.0.0.1', 0)) as server: _, port = server.socket.getsockname() redirect_uri = 'http://localhost:{}'.format(port) # get the NativeApp client object and start a flow # if available, use the system-name to prefill the grant label = platform.node() or None native_client = internal_auth_client() native_client.oauth2_start_flow(refresh_tokens=True, prefill_named_grant=label, redirect_uri=redirect_uri, requested_scopes=SCOPES) url = native_client.oauth2_get_authorize_url() # open web-browser for user to log in, get auth code webbrowser.open(url, new=1) auth_code = server.wait_for_code() if isinstance(auth_code, LocalServerError): safeprint('Login failed: {}'.format(auth_code), write_to_stderr=True) click.get_current_context().exit(1) elif isinstance(auth_code, Exception): safeprint('Login failed with unexpected error:\n{}'.format(auth_code), write_to_stderr=True) click.get_current_context().exit(1) # finish login flow exchange_code_and_store_config(native_client, auth_code)
def login_command(no_local_server, force): # if not forcing, stop if user already logged in if not force and check_logged_in(): safeprint(_LOGGED_IN_RESPONSE) return # use a link login if remote session or user requested if no_local_server or is_remote_session(): do_link_auth_flow(force_new_client=True) # otherwise default to a local server login flow else: safeprint( "You are running 'globus login', which should automatically open " "a browser window for you to login.\n" "If this fails or you experience difficulty, try " "'globus login --no-local-server'" "\n---" ) do_local_server_auth_flow(force_new_client=True) # print epilog safeprint(_LOGIN_EPILOG)
def _print_cmd_group(command, parent_names): parents = " ".join(parent_names) if parents: parents = parents + " " safeprint("\n=== {}{} ===\n".format(parents, command.name))
def update_command(yes, development, development_version): """ Executor for `globus update` """ # enforce that pip MUST be installed # Why not just include it in the setup.py requirements? Mostly weak # reasons, but it shouldn't matter much. # - if someone has installed the CLI without pip, then they haven't # followed our install instructions, so it's mostly a non-issue # - we don't want to have `pip install -U globus-cli` upgrade pip -- that's # a little bit invasive and easy to do by accident on modern versions of # pip where `--upgrade-strategy` defaults to `eager` # - we may want to do distributions in the future with dependencies baked # into a package, but we'd never want to do that with pip. More changes # would be needed to support that use-case, which we've discussed, but # not depending directly on pip gives us a better escape hatch # - if we depend on pip, we need to start thinking about what versions we # support. In point of fact, that becomes an issue as soon as we add this # command, but not being explicit about it lets us punt for now (maybe # indefinitely) on figuring out version requirements. All of that is to # say: not including it is bad, and from that badness we reap the rewards # of procrastination and non-explicit requirements # - Advanced usage, like `pip install -t` can produce an installed version # of the CLI which can't import its installing `pip`. If we depend on # pip, anyone doing this is forced to get two copies of pip, which seems # kind of nasty (even if "they're asking for it") if not _check_pip_installed(): safeprint("`globus update` requires pip. " "Please install pip and try again") click.get_current_context().exit(1) # --development-version implies --development development = development or (development_version is not None) # if we're running with `--development`, then the target version is a # tarball from GitHub, and we can skip out on the safety checks if development: # default to master development_version = development_version or "master" target_version = ( "https://github.com/globus/globus-cli/archive/{}" ".tar.gz#egg=globus-cli" ).format(development_version) else: # lookup version from PyPi, abort if we can't get it latest, current = get_versions() if latest is None: safeprint("Failed to lookup latest version. Aborting.") click.get_current_context().exit(1) # in the case where we're already up to date, do nothing and exit if current == latest: safeprint("You are already running the latest version: {}".format(current)) return # if we're up to date (or ahead, meaning a dev version was installed) # then prompt before continuing, respecting `--yes` else: safeprint( ( "You are already running version {0}\n" "The latest version is {1}" ).format(current, latest) ) if not yes and ( not click.confirm("Continue with the upgrade?", default=True) ): click.get_current_context().exit(1) # if we make it through to here, it means we didn't hit any safe (or # unsafe) abort conditions, so set the target version for upgrade to # the latest target_version = "globus-cli=={}".format(latest) # print verbose warning/help message, to guide less fortunate souls who hit # Ctrl+C at a foolish time, lose connectivity, or don't invoke with `sudo` # on a global install of the CLI safeprint( ( "The Globus CLI will now update itself.\n" "In the event that an error occurs or the update is interrupted, we " "recommend uninstalling and reinstalling the CLI.\n" "Update Target: {}\n" ).format(target_version) ) # register the upgrade activity as an atexit function # this ensures that most library teardown (other than whatever libs might # jam into atexit themselves...) has already run, and therefore protects us # against most potential bugs resulting from upgrading click while a click # command is running # # NOTE: there is a risk that we will see bugs on upgrade if the act of # doing a pip upgrade install changes state on disk and we (or a lib we # use) rely on that via pkg_resources, lazy/deferred imports, or good # old-fashioned direct inspection of `__file__` and the like DURING an # atexit method. Anything outside of atexit methods remains safe! @atexit.register def do_upgrade(): install_args = ["install", "--upgrade", target_version] if IS_USER_INSTALL: install_args.insert(1, "--user") _call_pip(*install_args)
def print_version(): """ Print out the current version, and at least try to fetch the latest from PyPi to print alongside it. It may seem odd that this isn't in globus_cli.version , but it's done this way to separate concerns over printing the version from looking it up. """ latest, current = get_versions() if latest is None: safeprint( ("Installed Version: {0}\n" "Failed to lookup latest version.").format( current ) ) else: safeprint( ("Installed Version: {0}\n" "Latest Version: {1}\n" "\n{2}").format( current, latest, "You are running the latest version of the Globus CLI" if current == latest else ( "You should update your version of the Globus CLI with\n" " globus update" ) if current < latest else "You are running a preview version of the Globus CLI", ) ) # verbose shows more platform and python info # it also includes versions of some CLI dependencies if is_verbose(): moddata = _get_package_data() safeprint("\nVerbose Data\n---") safeprint("platform:") safeprint(" platform: {}".format(platform.platform())) safeprint(" py_implementation: {}".format(platform.python_implementation())) safeprint(" py_version: {}".format(platform.python_version())) safeprint(" sys.executable: {}".format(sys.executable)) safeprint(" site.USER_BASE: {}".format(site.USER_BASE)) safeprint("modules:") for mod, modversion, modfile, modpath in moddata: safeprint(" {}:".format(mod)) safeprint(" __version__: {}".format(modversion)) safeprint(" __file__: {}".format(modfile)) safeprint(" __path__: {}".format(modpath))
def delete_command( batch, ignore_missing, star_silent, recursive, enable_globs, endpoint_plus_path, label, submission_id, dry_run, deadline, skip_activation_check, notify, ): """ Executor for `globus delete` """ endpoint_id, path = endpoint_plus_path if path is None and (not batch): raise click.UsageError("delete requires either a PATH OR --batch") client = get_client() # attempt to activate unless --skip-activation-check is given if not skip_activation_check: autoactivate(client, endpoint_id, if_expires_in=60) delete_data = DeleteData( client, endpoint_id, label=label, recursive=recursive, ignore_missing=ignore_missing, submission_id=submission_id, deadline=deadline, skip_activation_check=skip_activation_check, interpret_globs=enable_globs, **notify ) if batch: # although this sophisticated structure (like that in transfer) # isn't strictly necessary, it gives us the ability to add options in # the future to these lines with trivial modifications @click.command() @click.argument("path", type=TaskPath(base_dir=path)) def process_batch_line(path): """ Parse a line of batch input and add it to the delete submission item. """ delete_data.add_item(str(path)) shlex_process_stdin(process_batch_line, "Enter paths to delete, line by line.") else: if not star_silent and enable_globs and path.endswith("*"): # not intuitive, but `click.confirm(abort=True)` prints to stdout # unnecessarily, which we don't really want... # only do this check if stderr is a pty if ( err_is_terminal() and term_is_interactive() and not click.confirm( 'Are you sure you want to delete all files matching "{}"?'.format( path ), err=True, ) ): safeprint("Aborted.", write_to_stderr=True) click.get_current_context().exit(1) delete_data.add_item(path) if dry_run: formatted_print(delete_data, response_key="DATA", fields=[("Path", "path")]) # exit safely return res = client.submit_delete(delete_data) formatted_print( res, text_format=FORMAT_TEXT_RECORD, fields=(("Message", "message"), ("Task ID", "task_id")), )
def task_wait_with_io( meow, heartbeat, polling_interval, timeout, task_id, timeout_exit_code, client=None ): """ Options are the core "task wait" options, including the `--meow` easter egg. This does the core "task wait" loop, including all of the IO. It *does exit* on behalf of the caller. (We can enhance with a `noabort=True` param or somesuch in the future if necessary.) """ client = client or get_client() def timed_out(waited_time): if timeout is None: return False else: return waited_time >= timeout def check_completed(): completed = client.task_wait( task_id, timeout=polling_interval, polling_interval=polling_interval ) if completed: if heartbeat: safeprint("", write_to_stderr=True) # meowing tasks wake up! if meow: safeprint( r""" _.. /}_{\ /.-' ( a a )-.___...-'/ ==._.== ; \ i _..._ /, {_;/ {_//""", write_to_stderr=True, ) # TODO: possibly update TransferClient.task_wait so that we don't # need to do an extra fetch to get the task status after completion res = client.get_task(task_id) formatted_print(res, text_format=FORMAT_SILENT) status = res["status"] if status == "SUCCEEDED": click.get_current_context().exit(0) else: click.get_current_context().exit(1) return completed # Tasks start out sleepy if meow: safeprint( r""" |\ _,,,---,,_ /,`.-'`' -. ;-;;,_ |,4- ) )-,_..;\ ( `'-' '---''(_/--' `-'\_)""", write_to_stderr=True, ) waited_time = 0 while not timed_out(waited_time) and not check_completed(): if heartbeat: safeprint(".", write_to_stderr=True, newline=False) sys.stderr.flush() waited_time += polling_interval # add a trailing newline to heartbeats if we fail if heartbeat: safeprint("", write_to_stderr=True) exit_code = 1 if timed_out(waited_time): safeprint( "Task has yet to complete after {} seconds".format(timeout), write_to_stderr=True, ) exit_code = timeout_exit_code # output json if requested, but nothing for text mode res = client.get_task(task_id) formatted_print(res, text_format=FORMAT_SILENT) click.get_current_context().exit(exit_code)
def custom_except_hook(exc_info): """ A custom excepthook to present python errors produced by the CLI. We don't want to show end users big scary stacktraces if they aren't python programmers, so slim it down to some basic info. We keep a "DEBUGMODE" env variable kicking around to let us turn on stacktraces if we ever need them. Additionally, does global suppression of EPIPE errors, which often occur when a python command is piped to a consumer like `head` which closes its input stream before python has sent all of its output. DANGER: There is a (small) risk that this will bite us if there are EPIPE errors produced within the Globus SDK. We should keep an eye on this possibility, as it may demand more sophisticated handling of EPIPE. Possible TODO item to reduce this risk: inspect the exception and only hide EPIPE if it comes from within the globus_cli package. """ exception_type, exception, traceback = exc_info # check if we're in debug mode, and run the real excepthook if we are ctx = click.get_current_context() state = ctx.ensure_object(CommandState) if state.debug: sys.excepthook(exception_type, exception, traceback) # we're not in debug mode, do custom handling else: # if it's a click exception, re-raise as original -- Click's main # execution context will handle pretty-printing if isinstance(exception, click.ClickException): reraise(exception_type, exception, traceback) # catch any session errors to give helpful instructions # on how to use globus session update elif ( isinstance(exception, exc.GlobusAPIError) and exception.raw_json and "authorization_parameters" in exception.raw_json ): session_hook(exception) # handle the Globus-raised errors with our special hooks # these will present the output (on stderr) as JSON elif isinstance(exception, exc.TransferAPIError): if exception.code == "ClientError.AuthenticationFailed": authentication_hook(exception) else: transferapi_hook(exception) elif isinstance(exception, exc.AuthAPIError): if exception.code == "UNAUTHORIZED": authentication_hook(exception) # invalid_grant occurs when the users refresh tokens are not valid elif exception.message == "invalid_grant": invalidrefresh_hook(exception) else: authapi_hook(exception) elif isinstance(exception, exc.GlobusAPIError): globusapi_hook(exception) # specific checks fell through -- now check if it's any kind of # GlobusError elif isinstance(exception, exc.GlobusError): globus_generic_hook(exception) # not a GlobusError, not a ClickException -- something like ValueError # or NotImplementedError bubbled all the way up here: just print it # out, basically else: safeprint(u"{}: {}".format(exception_type.__name__, exception)) sys.exit(1)
def rm_command( ignore_missing, star_silent, recursive, enable_globs, endpoint_plus_path, label, submission_id, dry_run, deadline, skip_activation_check, notify, meow, heartbeat, polling_interval, timeout, timeout_exit_code, ): """ Executor for `globus rm` """ endpoint_id, path = endpoint_plus_path client = get_client() # attempt to activate unless --skip-activation-check is given if not skip_activation_check: autoactivate(client, endpoint_id, if_expires_in=60) delete_data = DeleteData( client, endpoint_id, label=label, recursive=recursive, ignore_missing=ignore_missing, submission_id=submission_id, deadline=deadline, skip_activation_check=skip_activation_check, interpret_globs=enable_globs, **notify ) if not star_silent and enable_globs and path.endswith("*"): # not intuitive, but `click.confirm(abort=True)` prints to stdout # unnecessarily, which we don't really want... # only do this check if stderr is a pty if ( err_is_terminal() and term_is_interactive() and not click.confirm( 'Are you sure you want to delete all files matching "{}"?'.format(path), err=True, ) ): safeprint("Aborted.", write_to_stderr=True) click.get_current_context().exit(1) delete_data.add_item(path) if dry_run: formatted_print(delete_data, response_key="DATA", fields=[("Path", "path")]) # exit safely return # Print task submission to stderr so that `-Fjson` is still correctly # respected, as it will be by `task wait` res = client.submit_delete(delete_data) task_id = res["task_id"] safeprint( 'Delete task submitted under ID "{}"'.format(task_id), write_to_stderr=True ) # do a `task wait` equivalent, including printing and correct exit status task_wait_with_io( meow, heartbeat, polling_interval, timeout, task_id, timeout_exit_code, client=client, )