Skip to content

Commit 5d50c15

Browse files
jacalataclaude
andcommitted
docs: consolidate auth samples into a single samples/login.py
Merge the previously-separate samples/auth_from_env.py into samples/login.py so there is one canonical demo of how to sign in to Tableau Server. The consolidated sample now supports three credential sources composed in precedence order: CLI args, TABLEAU_* env vars, and (with --interactive) a getpass password prompt. Env-var names are TABLEAU_-prefixed to avoid collision with generic shell vars like USERNAME (which Windows sets automatically for the current OS user). Public helpers on samples/login.py: - sample_define_common_options(parser) -- unchanged name; adds --interactive and --api-version. - get_env(key, default=None) -- unchanged name. - resolve_credentials(args) -- new; CLI -> env -> prompt. - build_server_and_auth(args) -- new; returns (Server, Auth) without signing in, replacing load_from_env's return shape. - sample_connect_to_server(args) -- unchanged name; now calls resolve_credentials + build_server_and_auth then signs in. - set_up_and_log_in() -- unchanged main entry. Removes the "Personal Access Token:" getpass fallback in sample_connect_to_server -- nobody wants to type a 40-character random string; a missing PAT half now raises the partial-credentials error. The password getpass prompt is likewise gated on --interactive rather than triggering silently on a missing --password. Env-var rename (breaks anyone relying on the pre-change bare names in login.py::get_env calls, though no other sample called those helpers): SERVER -> TABLEAU_SERVER SITE -> TABLEAU_SITE TOKEN_NAME -> TABLEAU_TOKEN_NAME TOKEN_VALUE -> TABLEAU_TOKEN samples/auth_from_env.py is deleted; its load_from_env() shape is now available as resolve_credentials(args) + build_server_and_auth(args). The README subsection is retained but points at samples/login.py and documents the three-source precedence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6820191 commit 5d50c15

2 files changed

Lines changed: 160 additions & 52 deletions

File tree

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,28 @@ To see sample code that works directly with the REST API (in Java, Python, or Po
1515

1616
For more information on installing and using TSC, see the documentation:
1717
<https://tableau.github.io/server-client-python/docs/>
18-
18+
19+
### Authenticating from environment variables
20+
21+
The [`samples/login.py`](samples/login.py) sample shows three ways to
22+
supply Tableau credentials, in precedence order:
23+
24+
1. Command-line arguments (`--server`, `--username`, `--password`,
25+
`--token-name`, `--token-value`, `--site`, `--api-version`)
26+
2. `TABLEAU_*` environment variables — one way to keep credentials out
27+
of your source code:
28+
* `TABLEAU_SERVER` (required) — server URL
29+
* `TABLEAU_SITE` (optional) — site content URL; `""` for the default site
30+
* `TABLEAU_TOKEN_NAME` + `TABLEAU_TOKEN` — personal access token (preferred)
31+
* `TABLEAU_USERNAME` + `TABLEAU_PASSWORD` — basic auth (fallback)
32+
* `TABLEAU_API_VERSION` (optional) — pin REST API version; otherwise
33+
the sample negotiates with the server
34+
3. Pass `--interactive` to prompt for a missing password on a terminal
35+
via `getpass` instead of exporting it. PATs are never prompted for.
36+
37+
Names are `TABLEAU_`-prefixed to avoid collision with generic shell
38+
variables like `USERNAME` (which Windows sets automatically).
39+
1940
To contribute, see our [Developer Guide](https://tableau.github.io/server-client-python/docs/dev-guide). A list of all our contributors to date is in [CONTRIBUTORS.md].
2041

2142
## License

samples/login.py

Lines changed: 138 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,64 @@
11
####
2-
# This script demonstrates how to log in to Tableau Server Client.
2+
# This sample demonstrates three ways to supply Tableau credentials --
3+
# they compose in this precedence order:
34
#
4-
# To run the script, you must have installed Python 3.7 or later.
5+
# 1. Command-line arguments (highest precedence)
6+
# 2. TABLEAU_* environment variables
7+
# 3. Interactive password prompt via --interactive (password only)
8+
#
9+
# Any credential missing from the CLI is looked up in the environment;
10+
# with --interactive, a missing password is prompted for on the terminal
11+
# via getpass. PATs are never prompted for; nobody wants to type a
12+
# 40-character random string.
13+
#
14+
# Environment variables are TABLEAU_-prefixed to avoid collision with
15+
# generic shell vars like USERNAME (which Windows sets automatically):
16+
# TABLEAU_SERVER (required) Server URL, e.g. https://10ax.online.tableau.com
17+
# TABLEAU_SITE (optional) Site content URL; "" for the default site
18+
# TABLEAU_TOKEN_NAME PAT name (preferred if both PAT vars are set)
19+
# TABLEAU_TOKEN PAT value (preferred if both PAT vars are set)
20+
# TABLEAU_USERNAME username (fallback if both basic vars are set)
21+
# TABLEAU_PASSWORD password (fallback if both basic vars are set)
22+
# TABLEAU_API_VERSION (optional) Pin REST API version; if absent, the
23+
# sample negotiates with the server.
24+
#
25+
# To run this sample, you must have installed Python 3.10 or later.
526
####
627

728
import argparse
829
import getpass
930
import logging
1031
import os
32+
import sys
1133

1234
import tableauserverclient as TSC
1335

36+
logger = logging.getLogger(__name__)
1437

15-
def get_env(key):
16-
if key in os.environ:
17-
return os.environ[key]
18-
return None
19-
20-
21-
# If a sample has additional arguments, then it should copy this code and insert them after the call to
22-
# sample_define_common_options
23-
# If it has no additional arguments, it can just call this method
24-
def set_up_and_log_in():
25-
parser = argparse.ArgumentParser(description="Logs in to the server.")
26-
sample_define_common_options(parser)
27-
args = parser.parse_args()
28-
if not args.server:
29-
args.server = get_env("SERVER")
30-
if not args.site:
31-
args.site = get_env("SITE")
32-
if not args.token_name:
33-
args.token_name = get_env("TOKEN_NAME")
34-
if not args.token_value:
35-
args.token_value = get_env("TOKEN_VALUE")
36-
args.logging_level = "debug"
3738

38-
server = sample_connect_to_server(args)
39-
print(server.server_info.get())
40-
print(server.server_address, "site:", server.site_id, "user:", server.user_id)
39+
def get_env(key: str, default: str | None = None) -> str | None:
40+
"""Return the value of environment variable ``key``, or ``default`` if unset."""
41+
return os.environ.get(key, default)
4142

4243

43-
def sample_define_common_options(parser):
44-
# Common options; please keep these in sync across all samples by copying or calling this method directly
44+
# If a sample has additional arguments, it should call this method and then add its
45+
# own; otherwise it can just call set_up_and_log_in().
46+
def sample_define_common_options(parser: argparse.ArgumentParser) -> None:
47+
"""Add the standard credential/logging arguments to an argparse parser."""
4548
parser.add_argument("--server", "-s", help="server address")
46-
parser.add_argument("--site", "-t", help="site name")
49+
parser.add_argument("--site", "-t", help="site content URL; '' for the default site")
4750
auth = parser.add_mutually_exclusive_group(required=False)
4851
auth.add_argument("--token-name", "-tn", help="name of the personal access token used to sign into the server")
4952
auth.add_argument("--username", "-u", help="username to sign into the server")
5053

5154
parser.add_argument("--token-value", "-tv", help="value of the personal access token used to sign into the server")
52-
parser.add_argument("--password", "-p", help="value of the password used to sign into the server")
55+
parser.add_argument("--password", "-p", help="password used to sign into the server")
56+
parser.add_argument("--api-version", help="pin a REST API version; otherwise auto-negotiate with the server")
57+
parser.add_argument(
58+
"--interactive",
59+
action="store_true",
60+
help="prompt for a missing password via getpass (requires a TTY)",
61+
)
5362
parser.add_argument(
5463
"--logging-level",
5564
"-l",
@@ -59,36 +68,114 @@ def sample_define_common_options(parser):
5968
)
6069

6170

62-
def sample_connect_to_server(args):
63-
if args.username:
64-
# Trying to authenticate using username and password.
65-
password = args.password or getpass.getpass("Password: ")
71+
def resolve_credentials(args: argparse.Namespace) -> argparse.Namespace:
72+
"""Populate credential fields on ``args`` from TABLEAU_* env vars, and
73+
(with ``--interactive``) an interactive password prompt.
74+
75+
Precedence per field: CLI arg > TABLEAU_* env var > (password only,
76+
when ``--interactive`` is set, ``--username`` is set, and stdin is a
77+
TTY) getpass prompt.
78+
79+
Raises ``ValueError`` on a partial credential pair (username without
80+
password, or token-name without token-value), or if ``--interactive``
81+
was requested but stdin is not a TTY so the prompt would hang.
82+
"""
83+
field_env = {
84+
"server": "TABLEAU_SERVER",
85+
"site": "TABLEAU_SITE",
86+
"token_name": "TABLEAU_TOKEN_NAME",
87+
"token_value": "TABLEAU_TOKEN",
88+
"username": "TABLEAU_USERNAME",
89+
"password": "TABLEAU_PASSWORD",
90+
"api_version": "TABLEAU_API_VERSION",
91+
}
92+
for field, env_var in field_env.items():
93+
if getattr(args, field, None) is None:
94+
setattr(args, field, get_env(env_var))
95+
96+
if args.interactive and args.username and not args.password:
97+
if not sys.stdin.isatty():
98+
raise ValueError(
99+
"--interactive requires a TTY; set TABLEAU_PASSWORD/--password " "or run from a real terminal"
100+
)
101+
args.password = getpass.getpass(f"Password for {args.username}: ")
102+
103+
if bool(args.token_name) ^ bool(args.token_value):
104+
missing = "TABLEAU_TOKEN/--token-value" if args.token_name else "TABLEAU_TOKEN_NAME/--token-name"
105+
raise ValueError(f"Partial PAT credentials: {missing} is not set")
106+
if bool(args.username) ^ bool(args.password):
107+
missing = "TABLEAU_PASSWORD/--password" if args.username else "TABLEAU_USERNAME/--username"
108+
raise ValueError(f"Partial basic credentials: {missing} is not set")
109+
110+
return args
111+
112+
113+
def build_server_and_auth(
114+
args: argparse.Namespace,
115+
) -> tuple[TSC.Server, TSC.TableauAuth | TSC.PersonalAccessTokenAuth]:
116+
"""Build the Server and Auth objects from resolved args. Does NOT sign in.
117+
118+
Callers who want to control the sign-in scope themselves (e.g. wrap it
119+
in ``with server.auth.sign_in(auth): ...``) should use this instead of
120+
:func:`sample_connect_to_server`.
121+
"""
122+
if not args.server:
123+
raise ValueError("Server URL is required: pass --server or set TABLEAU_SERVER")
66124

67-
tableau_auth = TSC.TableauAuth(args.username, password, site_id=args.site)
68-
print(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nUsername: {args.username}")
125+
site = args.site or ""
69126

127+
if args.token_name and args.token_value:
128+
auth: TSC.TableauAuth | TSC.PersonalAccessTokenAuth = TSC.PersonalAccessTokenAuth(
129+
token_name=args.token_name, personal_access_token=args.token_value, site_id=site
130+
)
131+
logger.info("Using PAT authentication")
132+
elif args.username and args.password:
133+
auth = TSC.TableauAuth(username=args.username, password=args.password, site_id=site)
134+
logger.info("Using username/password authentication")
70135
else:
71-
# Trying to authenticate using personal access tokens.
72-
token = args.token_value or getpass.getpass("Personal Access Token: ")
73-
74-
tableau_auth = TSC.PersonalAccessTokenAuth(
75-
token_name=args.token_name, personal_access_token=token, site_id=args.site
136+
raise ValueError(
137+
"No credentials found: set --token-name/--token-value "
138+
"(or TABLEAU_TOKEN_NAME/TABLEAU_TOKEN) or --username/--password "
139+
"(or TABLEAU_USERNAME/TABLEAU_PASSWORD)"
76140
)
77-
print(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nToken name: {args.token_name}")
78141

79-
if not tableau_auth:
80-
raise TabError("Did not create authentication object. Check arguments.")
142+
if args.api_version:
143+
server = TSC.Server(args.server, use_server_version=False)
144+
server.version = args.api_version
145+
else:
146+
server = TSC.Server(args.server, use_server_version=True)
147+
148+
return server, auth
81149

82-
# Only set this to False if you are running against a server you trust AND you know why the cert is broken
83-
check_ssl_certificate = True
84150

85-
# Make sure we use an updated version of the rest apis, and pass in our cert handling choice
86-
server = TSC.Server(args.server, use_server_version=True, http_options={"verify": check_ssl_certificate})
87-
server.auth.sign_in(tableau_auth)
88-
server.version = "3.19"
151+
def sample_connect_to_server(args: argparse.Namespace) -> TSC.Server:
152+
"""Resolve credentials, build the Server and Auth objects, and sign in.
89153
154+
Returns a Server that has an active session. The caller is responsible
155+
for signing out (or use :func:`build_server_and_auth` and wrap the
156+
sign-in yourself with a ``with`` block).
157+
"""
158+
resolve_credentials(args)
159+
server, auth = build_server_and_auth(args)
160+
identity = args.token_name or args.username
161+
print(f"\nSigning in...\nServer: {args.server}\nSite: {args.site or '(default)'}\nAs: {identity}")
162+
server.auth.sign_in(auth)
90163
return server
91164

92165

166+
def set_up_and_log_in() -> None:
167+
parser = argparse.ArgumentParser(description="Log in to Tableau Server.")
168+
sample_define_common_options(parser)
169+
args = parser.parse_args()
170+
171+
logging.basicConfig(level=getattr(logging, args.logging_level.upper()))
172+
173+
server = sample_connect_to_server(args)
174+
info = server.server_info.get()
175+
print(f"Product version: {info.product_version}")
176+
print(f"REST API version: {server.version}")
177+
print(f"Site: {server.site_id} User: {server.user_id}")
178+
179+
93180
if __name__ == "__main__":
94181
set_up_and_log_in()

0 commit comments

Comments
 (0)