-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy path__init__.py
More file actions
305 lines (268 loc) · 8.87 KB
/
Copy path__init__.py
File metadata and controls
305 lines (268 loc) · 8.87 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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import logging as py_logging
import re
import time
from pathlib import Path
from typing import Optional, Union
import click
from click.core import ParameterSource
from cwmscli.utils import colors
from cwmscli.utils.click_help import DOCS_BASE_URL
from cwmscli.utils.logging import apply_logging_policies, current_environment
def to_uppercase(ctx, param, value):
if value is None:
return None
return value.upper()
def has_invalid_chars(id: str) -> bool:
"""
Checks if ID contains any invalid web path characters.
"""
INVALID_PATH_CHARS = ["/", "\\", "&", "?", "="]
for char in INVALID_PATH_CHARS:
if char in id:
return True
return False
def _set_log_level(ctx, param, value):
if value is None:
return
level = getattr(py_logging, value.upper(), None)
if level is None:
raise click.BadParameter(f"Invalid log level: {value}")
quiet = bool(ctx.find_root().params.get("quiet", False))
level = apply_logging_policies(
level,
quiet=quiet,
environment=current_environment(),
explicit_log_level=ctx.get_parameter_source(param.name)
== ParameterSource.COMMANDLINE,
)
py_logging.getLogger().setLevel(level)
return value
office_option = click.option(
"-o",
"--office",
required=True,
envvar="OFFICE",
type=str,
callback=to_uppercase,
help="Office to grab data for",
)
office_option_notrequired = click.option(
"-o",
"--office",
default=None,
required=False,
envvar="OFFICE",
type=str,
callback=to_uppercase,
help="Office to grab data for",
)
api_root_option = click.option(
"-a",
"--api-root",
required=True,
envvar="CDA_API_ROOT",
type=str,
help="Api Root for CDA. Can be user defined or placed in a env variable CDA_API_ROOT",
)
api_coop_root_option = click.option(
"--coop",
is_flag=True,
envvar="CDA_API_COOP_ROOT",
type=str,
help="Use CDA_API_COOP_ROOT from env",
)
api_key_option = click.option(
"-k",
"--api-key",
default=None,
type=str,
envvar="CDA_API_KEY",
help="API key for CDA. Optional when a saved cwms-cli login token is available. Can also be provided by CDA_API_KEY.",
)
api_key_loc_option = click.option(
"-kl",
"--api-key-loc",
default=None,
type=str,
help="File storing an API key. Optional when a saved cwms-cli login token is available.",
)
log_level_option = click.option(
"--log-level",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
),
default="INFO",
envvar="LOG_LEVEL",
callback=_set_log_level,
expose_value=False, # Callback will set the log level of all methods
# Run before other commands (to cover any logging statements)
is_eager=True,
help="Set logging verbosity (overrides default INFO).",
)
def get_api_key(api_key: str, api_key_loc: str) -> str:
if api_key_loc is not None:
with open(api_key_loc, "r") as f:
return f.readline().strip()
elif api_key is not None:
return api_key
else:
raise Exception(
"must add a value to either --api-key(-k) or --api-key-loc(-kl)"
)
def get_saved_login_token(
token_file: Optional[Union[str, Path]] = None,
provider: str = "federation-eams",
) -> Optional[str]:
from cwmscli.utils.auth import (
AuthError,
default_token_file,
load_saved_login,
refresh_saved_login,
save_login,
)
candidate = Path(token_file) if token_file else default_token_file(provider)
try:
saved = load_saved_login(candidate)
except AuthError as error:
if candidate.exists():
py_logging.warning("Ignoring saved login at %s: %s", candidate, error)
return None
token = saved.get("token", {})
access_token = token.get("access_token")
if not access_token:
py_logging.warning(
"Ignoring saved login at %s: no access token found", candidate
)
return None
expires_at = token.get("expires_at")
if expires_at is not None:
try:
if float(expires_at) <= time.time():
py_logging.info("Refreshing expired saved login token at %s", candidate)
try:
refreshed = refresh_saved_login(token_file=candidate)
save_login(
token_file=candidate,
config=refreshed["config"],
token=refreshed["token"],
)
except AuthError as error:
py_logging.warning(
"Could not refresh saved login at %s: %s. Falling back to API key if available.",
candidate,
error,
)
return None
return refreshed["token"].get("access_token")
except (TypeError, ValueError, OSError):
py_logging.warning(
"Ignoring saved login at %s: invalid token expiration value %r",
candidate,
expires_at,
)
return None
return access_token
def init_cwms_session(
cwms_module,
*,
api_root: str,
api_key: Optional[str] = None,
api_key_loc: Optional[str] = None,
anonymous: bool = False,
token_file: Optional[Union[str, Path]] = None,
provider: str = "federation-eams",
):
init_fn = getattr(cwms_module, "init_session", None)
if init_fn is None:
init_fn = cwms_module.api.init_session
if anonymous:
return init_fn(api_root=api_root, api_key=None)
token = get_saved_login_token(token_file=token_file, provider=provider)
if token:
return init_fn(api_root=api_root, token=token)
resolved_api_key = None
if api_key_loc is not None or api_key is not None:
resolved_api_key = get_api_key(api_key, api_key_loc)
return init_fn(api_root=api_root, api_key=resolved_api_key)
def log_scoped_read_hint(
*,
credential_kind: Optional[str],
anonymous: bool,
office: str,
action: str,
resource: str = "content",
) -> None:
if anonymous or not credential_kind:
return
credential_text = (
"a saved login token was sent"
if credential_kind == "token"
else "an API key was sent"
)
py_logging.warning(
colors.c(
f"Access scope hint: {credential_text} for this {action} request in office {office}. "
f"If you need to view {resource} outside that credential's access scope, retry with "
f"--anonymous or remove the configured credential. Docs: {DOCS_BASE_URL}/cli/blob.html#blob-auth-scope",
"yellow",
bright=True,
)
)
def format_local_download_error(error: Exception, docs_url: str) -> str:
if isinstance(error, (OSError, ValueError)):
message = (
f"{colors.c('Failed to download:', 'red', bright=True)} {error}. "
f"If this is a local destination/path issue, pass "
f"{colors.c('--dest', 'cyan', bright=True)} explicitly."
)
if docs_url:
message = (
f"{message} {colors.c('Docs:', 'blue', bright=True)} "
f"{colors.c(docs_url, 'blue', bright=True)}"
)
return message
return f"{colors.c('Failed to download:', 'red', bright=True)} {error}"
def validate_default_download_dest(
raw_id: str,
*,
resource_name: str,
docs_url: str = "",
) -> str:
if raw_id is None:
raise ValueError(
f"{resource_name} ID must include a non-root destination name. "
f"Pass --dest explicitly if needed."
)
if raw_id.startswith("//") or raw_id.startswith("\\\\"):
raise ValueError(
f"{resource_name} ID must resolve to a relative local path. "
f"Pass --dest explicitly if needed."
)
# Leading separators can be part of a CDA ID; default downloads stay relative.
target = raw_id.lstrip("/\\")
if not target:
message = (
f"{resource_name} ID must include a non-root destination name. "
f"Pass --dest explicitly if needed."
)
if docs_url:
message = f"{message} Docs: {docs_url}"
raise ValueError(message)
if re.match(r"^[A-Za-z]:", target):
raise ValueError(
f"{resource_name} ID must resolve to a relative local path. "
f"Pass --dest explicitly if needed."
)
parts = re.split(r"[\\/]", target)
if any(part in {"", ".", ".."} for part in parts):
raise ValueError(
f"{resource_name} ID must resolve to a relative local path. "
f"Pass --dest explicitly if needed."
)
return target
def common_api_options(f):
f = log_level_option(f)
f = office_option(f)
f = api_root_option(f)
f = api_key_option(f)
return f