Here’s an updated, drop-in doc page that matches your current function’s behavior (including the “blank password lets you re-enter the username” flow and returning None when login is cancelled). I kept your structure and added the key nuances.
connect_tapis()#
connect_tapis(token_filePath=”~/.tapis_tokens.json”, base_url=”https://designsafe.tapis.io”, username=””, password=””, force_connect=False)
Purpose. Create an authenticated Tapis client (e.g., for DesignSafe) with automatic token caching. It reuses a valid saved token when available; otherwise it securely prompts for credentials, fetches a new token, saves it for next time, and returns a ready-to-use client.
What it does#
Checks token_filePath (default
~/.tapis_tokens.json) for a saved token.Valid token → uses it directly (no login prompts).
Missing/expired or
force_connect=True→ performs a fresh login, saves the token, and continues.Prints when the token expires and how long remains.
Interactive login behavior:
If
usernameargument is empty, you’ll be prompted for it (blank = cancel; returnsNone).You’re then prompted for your password. Press Enter on an empty password to restart the prompts and re-enter the username (useful if you mistyped it).
Parameters#
token_filePath (str, default
"~/.tapis_tokens.json") – Path to the JSON token file.base_url (str, default
"https://designsafe.tapis.io") – Tapis API base URL for your tenancy.username (str, default
"") – Preset username; if empty, you’ll be prompted (blank cancels).password (str, default
"") – Preset password; if empty, you’ll be prompted securely. Blank at the prompt restarts and lets you change the username.force_connect (bool, default
False) – Force a fresh login even if a valid token exists.
Returns#
Tapis client (
tapipy.tapis.Tapis) orNoneAn authenticated client when successful;Noneif login was cancelled (blank username) or ultimately failed.
Token file format (JSON)#
{
"access_token": "…",
"expires_at": "2025-08-31T12:34:56+00:00"
}
Expiry strings are parsed leniently; naive timestamps are treated as UTC. On fresh login the file is saved and (best effort) chmod’ed to 0600.
Examples#
# Use a saved token if valid; otherwise prompt and save a new one
t = connect_tapis()
if t:
jobs = t.jobs.getJobList()
for j in jobs:
print(j.id, j.status)
Force a fresh login (rotate the token):
t = connect_tapis(force_connect=True)
Provide credentials programmatically (no prompts on success):
t = connect_tapis(username="me@example.org", password="********")
Custom token cache path (e.g., project workspace):
t = connect_tapis(token_filePath="./.secrets/tapis_token.json")
Handle a cancelled login gracefully:
t = connect_tapis()
if t is None:
print("Login cancelled by user.")
Fix a mistyped username during prompts:
At the password prompt, press Enter with a blank password → you’ll be re-prompted for the username and can try again.
Notes & tips#
Security: The token file contains only the access token and expiry, not your password; the code attempts to set file permissions to
0600after writing.Portability: You can point token_filePath to a project-local location (e.g., inside a shared workspace) if appropriate.
Diagnostics: On success you’ll see
-- AUTHENTICATED VIA SAVED TOKEN(cache) or-- AUTHENTICATED VIA FRESH LOGIN. Expiry time and remaining duration are printed.
Files#
You can find these files in Community Data.
connect_tapis.py
def connect_tapis(token_filePath: str = "~/.tapis_tokens.json",
base_url: str = "https://designsafe.tapis.io",
username: str = "",
password: str = "",
force_connect: bool = False):
"""
Authenticate to a Tapis (DesignSafe) tenancy with token caching and an interactive fallback.
Behavior
--------
- Attempts to reuse a valid cached access token from ``token_filePath`` (default:
``~/.tapis_tokens.json``). If valid, no prompts are shown.
- If the cache is missing/expired, or ``force_connect=True``, performs a fresh
login and writes a new token back to ``token_filePath``.
- During interactive login:
* If ``username`` is empty, you are prompted for it (blank cancels).
* You are then prompted for the password. **Pressing Enter with a blank
password restarts the prompts and lets you re-enter the username**.
- Prints token expiry details for transparency.
Parameters
----------
token_filePath : str, optional
Path to the JSON file that stores the cached token:
``{"access_token": "...", "expires_at": "...ISO8601..."}``.
Defaults to ``"~/.tapis_tokens.json"``.
base_url : str, optional
Tapis API base URL for your tenancy. Defaults to
``"https://designsafe.tapis.io"``.
username : str, optional
Preset username. If empty, you will be prompted (blank cancels).
password : str, optional
Preset password. If empty, you will be prompted securely. **Blank** at the
prompt restarts the flow so you can change the username.
force_connect : bool, optional
If ``True``, ignores any valid cached token and forces a fresh login.
Returns
-------
tapipy.tapis.Tapis or None
An authenticated client ready to use, or ``None`` if login was cancelled
(blank username) or ultimately failed.
Notes
-----
- Expiry strings in the cache are parsed leniently; naive timestamps are treated
as UTC. On successful login, the cache file is written and (best-effort) set
to file mode ``0600`` for local protection.
- If the saved token cannot be parsed/validated, a fresh login is performed.
Examples
--------
>>> t = connect_tapis() # reuse cached token or prompt as needed
>>> if t:
... print(t.jobs.getJobList())
Force a fresh login:
>>> t = connect_tapis(force_connect=True)
Provide credentials programmatically (no prompts on success):
>>> t = connect_tapis(username="me@example.org", password="••••••••")
Cancel login at prompt:
- Press Enter when asked for username.
Restart prompts to fix username:
- At the password prompt, press Enter to restart and re-enter the username.
Author
------
Silvia Mazzoni, DesignSafe (silviamazzoni@yahoo.com)
Date
----
2025-09-22
Version
-------
1.2
"""
from tapipy.tapis import Tapis
from getpass import getpass
from datetime import datetime, timezone
import json
import os
from typing import Optional
def _parse_expires_at(s: str) -> Optional[datetime]:
"""Parse ISO8601 expiry, accepting 'Z' and naive strings; return aware UTC dt or None."""
if not s:
return None
try:
s_norm = s.replace("Z", "+00:00")
dt = datetime.fromisoformat(s_norm)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
return None
def getTokensLoop(u=None):
"""Prompt repeatedly until tokens are obtained.
Blank password restarts and re-prompts username. Blank username cancels.
"""
while True:
if not u:
u = getpass("Username (leave blank to cancel): ")
if u == "":
print(" Blank username entered → cancelling!!")
return None
p = getpass(f"Password for {u} (leave blank to re-enter username): ")
if p == "":
print(" Blank password entered → restarting (you can correct the username).")
u=None
continue
t_local = Tapis(base_url=base_url, username=u, password=p)
try:
t_local.get_tokens()
return t_local
except Exception as e:
print(f" ** Warning ** could NOT get token : {e}\n TRY AGAIN!")
print(" -- Checking Tapis token --")
token_path = os.path.expanduser(token_filePath)
now = datetime.now(timezone.utc)
t = None
saved_expires_at = None
used_saved_token = False
# Try to load a saved token unless forcing fresh login
if force_connect:
print(" Forcing a connection to Tapis (fresh login).")
else:
if os.path.exists(token_path):
try:
with open(token_path, "r") as f:
tokens = json.load(f)
saved_expires_at = _parse_expires_at(tokens.get("expires_at"))
if tokens.get("access_token") and saved_expires_at and saved_expires_at > now:
print(" Token loaded from file. Token is still valid!")
t = Tapis(base_url=base_url, access_token=tokens["access_token"])
used_saved_token = True
else:
print(" Token file found but token is missing/expired.")
if saved_expires_at:
print(" Token expired at:", saved_expires_at.isoformat())
except Exception as e:
print(f" Could not read/parse token file ({token_path}): {e}")
else:
print(" No saved tokens found.")
# If no valid token, perform login
if t is None:
print("-- Connect to Tapis --")
print(" Leave username blank to cancel.")
if not username:
username = getpass("Username: ")
if username == "":
print(" Login aborted: Blank Username!")
return None
if not password:
password = getpass(f"Password for {username} (leave blank to re-enter username): ")
if password == "":
print(" Blank password entered → restarting full login prompts.")
t = getTokensLoop()
if t is None:
print(" Login aborted.")
return None
else:
t = Tapis(base_url=base_url, username=username, password=password)
try:
t.get_tokens()
except Exception as e:
print(f" ** Warning ** could NOT get token : {e}\n TRY AGAIN!")
t = getTokensLoop(username)
if t is None:
print(" Login aborted.")
return None
else:
t = Tapis(base_url=base_url, username=username, password=password)
try:
t.get_tokens()
except Exception as e:
print(f" ** Warning ** could NOT get token : {e}\n TRY AGAIN!")
t = getTokensLoop(username)
if t is None:
print(" Login aborted.")
return None
# Save the new token back to the chosen path
try:
tokens = {
"access_token": t.access_token.access_token,
"expires_at": t.access_token.expires_at.isoformat(),
}
parent = os.path.dirname(token_path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(token_path, "w") as f:
json.dump(tokens, f)
try:
os.chmod(token_path, 0o600) # best-effort tighten perms
except Exception:
pass
print(f" Token saved to {token_path}")
saved_expires_at = _parse_expires_at(tokens["expires_at"])
except Exception as e:
print(f" Warning: could not save token to {token_path}: {e}")
# Print expiry info
exp_to_show = saved_expires_at
try:
if getattr(t, "access_token", None) and getattr(t.access_token, "expires_at", None):
exp_to_show = _parse_expires_at(str(t.access_token.expires_at)) or exp_to_show
except Exception:
pass
if exp_to_show:
print(" Token expires at:", exp_to_show.isoformat())
print(" Token expires in:", str(exp_to_show - now))
else:
print(" Token expiry time unavailable.")
print("-- AUTHENTICATED VIA {} --".format("SAVED TOKEN" if used_saved_token else "FRESH LOGIN"))
return t