-
Notifications
You must be signed in to change notification settings - Fork 448
Expand file tree
/
Copy path_shared.py
More file actions
201 lines (172 loc) · 8.14 KB
/
Copy path_shared.py
File metadata and controls
201 lines (172 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
####
# Shared helpers for the sample scripts in this directory.
#
# The most important thing here is `resolve_credentials`, which lets samples
# accept a Tableau server URL, site, and credentials from three sources:
#
# 1. Command-line arguments (useful for CI, but note that these end up in
# shell history and process listings, so avoid them for real secrets).
# 2. Environment variables. If a `.env` file exists next to the sample
# being run, or in the current working directory, we load it first --
# only the standard `KEY=value` lines, no external dependency required.
# 3. Interactive prompts. Missing values are asked for on stdin; secrets
# are read with `getpass.getpass` so they are not echoed.
#
# CLI args take precedence, then environment, then interactive prompt.
# This lets a user set defaults in a `.env` file and override individual
# values on the command line.
####
from __future__ import annotations
import argparse
import getpass
import os
from pathlib import Path
from typing import Iterable
import tableauserverclient as TSC
# Recognized environment variable names, in the order we look them up.
# Older samples used TABLEAU_SERVER etc; keep those working as aliases.
_ENV_ALIASES: dict[str, tuple[str, ...]] = {
"server": ("TABLEAU_SERVER", "SERVER"),
"site": ("TABLEAU_SITE", "SITE"),
"token_name": ("TABLEAU_TOKEN_NAME", "TOKEN_NAME"),
"token_value": ("TABLEAU_TOKEN_VALUE", "TOKEN_VALUE"),
"username": ("TABLEAU_USERNAME", "USERNAME"),
"password": ("TABLEAU_PASSWORD", "PASSWORD"),
}
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
"""Add the sign-in and logging arguments used by every sample.
Kept in sync with the historical inline definitions so no existing
command line breaks. All arguments are optional -- missing values
are pulled from the environment or prompted for interactively.
"""
parser.add_argument("--server", "-s", help="server address (env: TABLEAU_SERVER)")
parser.add_argument("--site", "-t", help="site content URL (env: TABLEAU_SITE)")
parser.add_argument(
"--token-name",
help="name of the personal access token used to sign into the server " "(env: TABLEAU_TOKEN_NAME)",
)
parser.add_argument(
"--token-value",
help="value of the personal access token used to sign into the server "
"(env: TABLEAU_TOKEN_VALUE). Prefer the env var or interactive prompt over the "
"command line so the secret does not land in shell history.",
)
parser.add_argument(
"--username",
"-u",
help="username to sign into the server (env: TABLEAU_USERNAME). Only used if "
"no personal access token is supplied.",
)
parser.add_argument(
"--password",
"-p",
help="password (env: TABLEAU_PASSWORD). Prefer the env var or interactive " "prompt over the command line.",
)
parser.add_argument(
"--env-file",
help="path to a .env-style file with KEY=value lines to load. If omitted, "
".env in the current directory is loaded automatically when present.",
)
parser.add_argument(
"--logging-level",
"-l",
choices=["debug", "info", "error"],
default="error",
help="desired logging level (set to error by default)",
)
def _load_env_file(path: Path) -> None:
"""Very small `.env` loader: `KEY=value` per line, `#` for comments.
We do not want a runtime dependency on python-dotenv for the samples,
so this parses just the common cases. Existing env vars are not
overwritten -- a value already in `os.environ` wins.
"""
try:
text = path.read_text(encoding="utf-8")
except OSError:
return
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip("'\"")
if key and key not in os.environ:
os.environ[key] = value
def _first_env(names: Iterable[str]) -> str | None:
for name in names:
val = os.environ.get(name)
if val:
return val
return None
def resolve_credentials(args: argparse.Namespace, *, allow_prompt: bool = True) -> None:
"""Fill in server/site/credential values on `args` from env or prompt.
Precedence for each field: existing value on `args` > environment variable
> interactive prompt (if allow_prompt and stdin is a terminal).
Pass `allow_prompt=False` in CI environments where blocking on input would
hang the job; the caller should then verify the fields it needs are set.
"""
# Load `.env` file if one is requested or available.
env_file = getattr(args, "env_file", None)
if env_file:
_load_env_file(Path(env_file))
else:
default_env = Path.cwd() / ".env"
if default_env.is_file():
_load_env_file(default_env)
# For each field, prefer the CLI arg, then env, then prompt.
for field, env_names in _ENV_ALIASES.items():
current = getattr(args, field, None)
if current:
continue
env_val = _first_env(env_names)
if env_val:
setattr(args, field, env_val)
if not allow_prompt:
return
# Prompt for what's still missing. We only prompt for the pieces we
# actually need: server URL, and one of token or username/password.
if not getattr(args, "server", None):
args.server = input("Tableau server URL: ").strip()
# Site is optional (empty string is the default site) so we don't prompt.
has_token = getattr(args, "token_name", None) and getattr(args, "token_value", None)
has_user = getattr(args, "username", None) and getattr(args, "password", None)
if has_token or has_user:
return
# Nothing configured yet. Ask which auth method to use.
if getattr(args, "token_name", None) or getattr(args, "username", None):
# Partial info supplied -- fill in the matching missing piece.
if getattr(args, "token_name", None) and not getattr(args, "token_value", None):
args.token_value = getpass.getpass(f"Personal access token value for '{args.token_name}': ")
return
if getattr(args, "username", None) and not getattr(args, "password", None):
args.password = getpass.getpass(f"Password for '{args.username}': ")
return
# Fully unspecified: default to PAT since that's what the docs recommend.
print("No credentials found in args or environment. Sign in with a personal access token.")
print("(Set TABLEAU_TOKEN_NAME / TABLEAU_TOKEN_VALUE in your env or a .env file to skip this prompt.)")
args.token_name = input("Personal access token name: ").strip()
args.token_value = getpass.getpass("Personal access token value: ")
def build_auth(args: argparse.Namespace) -> TSC.TableauAuth | TSC.PersonalAccessTokenAuth:
"""Return the appropriate auth object based on what's set on `args`."""
site = getattr(args, "site", None) or ""
if getattr(args, "token_name", None) and getattr(args, "token_value", None):
return TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=site)
if getattr(args, "username", None) and getattr(args, "password", None):
return TSC.TableauAuth(args.username, args.password, site_id=site)
raise ValueError(
"No usable credentials found. Provide --token-name/--token-value, "
"--username/--password, or set the corresponding env vars."
)
def sign_in(args: argparse.Namespace, *, use_server_version: bool = True) -> TSC.Server:
"""Convenience helper: resolve credentials, build the server, and sign in.
The caller is responsible for calling `server.auth.sign_out()` or using
the `with server.auth.sign_in(...)` context manager pattern themselves
when they need finer control. This helper is intended for the small
samples that just want a signed-in server object to poke at.
"""
resolve_credentials(args)
auth = build_auth(args)
server = TSC.Server(args.server, use_server_version=use_server_version)
server.auth.sign_in(auth)
return server