docs: def login(credentials=None, app_name=None, services=None, client_id=None, make_clients=True,
            clear_old_tokens=False, token_dir=DEFAULT_CRED_PATH, **kwargs):
      """Log in to Globus services
  
      Arguments:
          credentials (str or dict): A string filename, string JSON, or dictionary
                  with credential and config information.
                  By default, looks in ``~/mdf/credentials/globus_login.json``.
                  Contains ``app_name``, ``services``, and ``client_id`` as described below.
          app_name (str): Name of script/client. This will form the name of the token cache file.
                  **Default**: ``'UNKNOWN'``.
          services (list of str): Services to authenticate with.
                  **Default**: ``[]``.
          client_id (str): The ID of the client, given when registered with Globus.
                  **Default**: The MDF Native Clients ID.
          make_clients (bool): If ``True``, will make and return appropriate clients with
                  generated tokens. If ``False``, will only return authorizers.
                  **Default**: ``True``.
          clear_old_tokens (bool): If ``True``, delete old token file if it exists,
                  forcing user to re-login. If ``False``, use existing token file if there is one.
                  **Default**: ``False``.
          token_dir (str): The path to the directory to save tokens in and look for
                  credentials by default. **Default**: ``DEFAULT_CRED_PATH``.
  
      Returns:
          dict: The clients and authorizers requested, indexed by service name.
                  For example, if ``login()`` is told to auth with ``'search'``
                  then the search client will be in the ``'search'`` field.
  
          Note:
              Previously requested tokens (which are cached) will be returned alongside
              explicitly requested ones.
      """
      NATIVE_CLIENT_ID = "98bfc684-977f-4670-8669-71f8337688e4"
      DEFAULT_CRED_FILENAME = "globus_login.json"
  
      def _get_tokens(client, scopes, app_name, force_refresh=False):
          token_path = os.path.join(token_dir, app_name + "_tokens.json")
          if force_refresh:
              if os.path.exists(token_path):
                  os.remove(token_path)
          if os.path.exists(token_path):
              with open(token_path, "r") as tf:
                  try:
                      tokens = json.load(tf)
                      # Check that requested scopes are present
                      # :all scopes should override any scopes with lesser permissions
                      # Some scopes are returned in multiples and should be separated
                      existing_scopes = []
                      for sc in [val["scope"] for val in tokens.values()]:
                          if " " in sc:
                              existing_scopes += sc.split(" ")
                          else:
                              existing_scopes.append(sc)
                      permissive_scopes = [scope.replace(":all", "")
                                           for scope in existing_scopes
                                           if scope.endswith(":all")]
                      missing_scopes = [scope for scope in scopes.split(" ")
                                        if scope not in existing_scopes
                                        and not any([scope.startswith(per_sc)
                                                     for per_sc in permissive_scopes])
                                        and not scope.strip() == ""]
                      # If some scopes are missing, regenerate tokens
                      # Get tokens for existing scopes and new scopes
                      if len(missing_scopes) > 0:
                          scopes = " ".join(existing_scopes + missing_scopes)
                          os.remove(token_path)
                  except ValueError:
                      # Tokens corrupted
                      os.remove(token_path)
          if not os.path.exists(token_path):
              try:
                  os.makedirs(token_dir)
              except (IOError, OSError):
                  pass
              client.oauth2_start_flow(requested_scopes=scopes, refresh_tokens=True)
              authorize_url = client.oauth2_get_authorize_url()
  
              print("It looks like this is the first time you're accessing this service.",
                    "\nPlease log in to Globus at this link:\n", authorize_url)
              auth_code = input("Copy and paste the authorization code here: ").strip()
  
              # Handle 401s
              try:
                  token_response = client.oauth2_exchange_code_for_tokens(auth_code)
              except globus_sdk.GlobusAPIError as e:
                  if e.http_status == 401:
                      raise ValueError("\nSorry, that code isn't valid."
                                       " You can try again, or contact support.")
                  else:
                      raise
              tokens = token_response.by_resource_server
  
              os.umask(0o077)
              with open(token_path, "w") as tf:
                  json.dump(tokens, tf)
              print("Thanks! You're now logged in.")
  
          return tokens
  
      # If creds supplied in 'credentials', process
      if credentials:
          if type(credentials) is str:
              try:
                  with open(credentials) as cred_file:
                      creds = json.load(cred_file)
              except IOError:
                  try:
                      creds = json.loads(credentials)
                  except ValueError:
                      raise ValueError("Credential string unreadable")
          elif type(credentials) is dict:
              creds = credentials
          else:
              try:
                  with open(os.path.join(os.getcwd(), DEFAULT_CRED_FILENAME)) as cred_file:
                      creds = json.load(cred_file)
              except IOError:
                  try:
                      with open(os.path.join(token_dir, DEFAULT_CRED_FILENAME)) as cred_file:
                          creds = json.load(cred_file)
                  except IOError:
                      raise ValueError("Credentials/configuration must be passed as a "
                                       + "filename string, JSON string, or dictionary, "
                                       + "or provided in '"
                                       + DEFAULT_CRED_FILENAME
                                       + "' or '"
                                       + token_dir
                                       + "'.")
          app_name = creds.get("app_name")
          services = creds.get("services", services)
          client_id = creds.get("client_id")
      if not app_name:
          app_name = "UNKNOWN"
      if not services:
          services = []
      elif isinstance(services, str):
          services = [services]
      if not client_id:
          client_id = NATIVE_CLIENT_ID
  
      native_client = globus_sdk.NativeAppAuthClient(client_id, app_name=app_name)
  
      servs = []
      for serv in services:
          serv = serv.lower().strip()
          if type(serv) is str:
              servs += serv.split(" ")
          else:
              servs += list(serv)
      # Translate services into scopes as possible
      scopes = " ".join([KNOWN_SCOPES.get(sc, sc) for sc in servs])
  
      all_tokens = _get_tokens(native_client, scopes, app_name, force_refresh=clear_old_tokens)
  
      # Make authorizers with every returned token
      all_authorizers = {}
      for key, tokens in all_tokens.items():
          # TODO: Allow non-Refresh authorizers
          try:
              all_authorizers[key] = globus_sdk.RefreshTokenAuthorizer(tokens["refresh_token"],
                                                                       native_client)
          except KeyError:
              print("Error: Unable to retrieve tokens for '{}'.\n"
                    "You may need to delete your old tokens and retry.".format(key))
      returnables = {}
      # Populate clients and named services
      # Only translate back services - if user provides scope directly, don't translate back
      # ex. transfer => urn:transfer.globus.org:all => transfer,
      #     but urn:transfer.globus.org:all !=> transfer
      for service in servs:
          token_key = KNOWN_TOKEN_KEYS.get(service)
          # If the .by_resource_server key (token key) for the service was returned
          if token_key in all_authorizers.keys():
              # If there is an applicable client (all clients have known token key)
              # Pop from all_authorizers to remove from final return value
              if make_clients and KNOWN_CLIENTS.get(service):
                  try:
                      returnables[service] = KNOWN_CLIENTS[service](
                                                  authorizer=all_authorizers.pop(token_key),
                                                  http_timeout=STD_TIMEOUT)
                  except globus_sdk.GlobusAPIError as e:
                      print("Error: Unable to create {} client: {}".format(service, e.message))
              # If no applicable client, just translate the key
              else:
                  returnables[service] = all_authorizers.pop(token_key)
      # Add authorizers not associated with service to returnables
      returnables.update(all_authorizers)
  
      return returnables