cwms-cli 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/PKG-INFO +8 -5
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/README.md +6 -4
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/__main__.py +20 -5
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/blob.py +34 -18
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/clob.py +16 -8
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/commands_cwms.py +12 -9
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/writer.py +4 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/env.py +39 -4
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/shef/import_critfile.py +3 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/shef/import_infile.py +8 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/users.py +49 -8
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/dss/transfer.py +4 -1
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/location/location_ids.py +3 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/location/location_ids_bygroup.py +3 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/timeseries/timeseries_data.py +161 -157
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/timeseries/timeseries_ids.py +3 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/getUSGS_ratings_cda.py +9 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/getusgs_cda.py +117 -1
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/getusgs_measurements_cda.py +44 -36
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/rating_ini_file_import.py +4 -1
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/auth.py +4 -2
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/click_help.py +9 -2
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/deps.py +36 -6
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/friendly_errors.py +136 -15
- cwms_cli-0.9.0/cwmscli/utils/interaction.py +45 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/ssl_errors.py +14 -2
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/version_cli.py +7 -1
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/pyproject.toml +1 -1
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/LICENSE +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/_generated/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/_generated/ownership_data.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/shef/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/commands/shef/shef_parameters.csv +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/dss/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/dss/cli.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/dss/naming.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/ownership.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/callback_success.html +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/env_store.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/paths.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.8.0 → cwms_cli-0.9.0}/cwmscli/utils/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Command line utilities for Corps Water Management Systems (CWMS) python scripts. This is a collection of shared scripts across the enterprise Water Management Enterprise System (WMES) teams.
|
|
5
5
|
License: LICENSE
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
20
|
Provides-Extra: dss
|
|
20
21
|
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
21
22
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
@@ -89,8 +90,10 @@ devcontainer exec --workspace-folder . poetry run pytest -q
|
|
|
89
90
|
|
|
90
91
|
The container installs project dependencies with Poetry and uses
|
|
91
92
|
`/home/vscode/.venv`, so it does not reuse a host operating system's `.venv`.
|
|
92
|
-
Poetry itself is kept in a separate `/opt/poetry` environment.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
Poetry itself is kept in a separate `/opt/poetry` environment. The workspace
|
|
94
|
+
root is also added to `PYTHONPATH`, so the checked-out source and local test
|
|
95
|
+
modules are available throughout a Codespace without copying them into the
|
|
96
|
+
image. Use the container for changes involving time zones, paths, native
|
|
97
|
+
libraries, or other operating-system-dependent behavior. Standard Python 3.9
|
|
98
|
+
and Python 3.12 installations remain covered by the existing CI matrix.
|
|
96
99
|
|
|
@@ -60,7 +60,9 @@ devcontainer exec --workspace-folder . poetry run pytest -q
|
|
|
60
60
|
|
|
61
61
|
The container installs project dependencies with Poetry and uses
|
|
62
62
|
`/home/vscode/.venv`, so it does not reuse a host operating system's `.venv`.
|
|
63
|
-
Poetry itself is kept in a separate `/opt/poetry` environment.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
Poetry itself is kept in a separate `/opt/poetry` environment. The workspace
|
|
64
|
+
root is also added to `PYTHONPATH`, so the checked-out source and local test
|
|
65
|
+
modules are available throughout a Codespace without copying them into the
|
|
66
|
+
image. Use the container for changes involving time zones, paths, native
|
|
67
|
+
libraries, or other operating-system-dependent behavior. Standard Python 3.9
|
|
68
|
+
and Python 3.12 installations remain covered by the existing CI matrix.
|
|
@@ -64,6 +64,15 @@ from cwmscli.utils.version_cli import show_version_and_exit
|
|
|
64
64
|
default=False,
|
|
65
65
|
help="Suppress routine output; warnings and errors still print.",
|
|
66
66
|
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--non-interactive/--interactive",
|
|
69
|
+
default=None,
|
|
70
|
+
envvar="CWMS_CLI_NON_INTERACTIVE",
|
|
71
|
+
help=(
|
|
72
|
+
"Disable prompts. Automatically enabled when standard input is not a "
|
|
73
|
+
"terminal or a CI environment is detected."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
67
76
|
@click.pass_context
|
|
68
77
|
def cli(
|
|
69
78
|
ctx: click.Context,
|
|
@@ -71,6 +80,7 @@ def cli(
|
|
|
71
80
|
no_color: bool,
|
|
72
81
|
log_level: str,
|
|
73
82
|
quiet: bool,
|
|
83
|
+
non_interactive: Optional[bool],
|
|
74
84
|
) -> None:
|
|
75
85
|
level = getattr(logging, log_level.upper(), logging.INFO)
|
|
76
86
|
level = apply_logging_policies(
|
|
@@ -83,7 +93,7 @@ def cli(
|
|
|
83
93
|
|
|
84
94
|
# Disable colors if stdout isn't a TTY (piped/redirected)
|
|
85
95
|
tty = sys.stdout.isatty()
|
|
86
|
-
color = (not no_color) and tty
|
|
96
|
+
color = (not no_color) and ("NO_COLOR" not in os.environ) and tty
|
|
87
97
|
setup_logging(LoggingConfig(level=level, log_file=log_file, color=color))
|
|
88
98
|
|
|
89
99
|
|
|
@@ -126,6 +136,15 @@ def main() -> None:
|
|
|
126
136
|
if server_stack_trace is not None:
|
|
127
137
|
click.echo(format_cda_stack_trace(server_stack_trace), err=True)
|
|
128
138
|
raise SystemExit(e.exit_code)
|
|
139
|
+
else:
|
|
140
|
+
if is_cert_verify_error(e):
|
|
141
|
+
click.echo(ssl_help_text(), err=True)
|
|
142
|
+
raise SystemExit(2)
|
|
143
|
+
friendly_error = to_user_facing_error(e)
|
|
144
|
+
if friendly_error is not None:
|
|
145
|
+
logging.debug("Suppressed traceback for CLI exception", exc_info=e)
|
|
146
|
+
friendly_error.show()
|
|
147
|
+
raise SystemExit(friendly_error.exit_code)
|
|
129
148
|
e.show()
|
|
130
149
|
raise SystemExit(e.exit_code)
|
|
131
150
|
except Exception as e:
|
|
@@ -134,10 +153,6 @@ def main() -> None:
|
|
|
134
153
|
debug = debug or logging.getLogger().isEnabledFor(logging.DEBUG)
|
|
135
154
|
|
|
136
155
|
if is_cert_verify_error(e) and not debug:
|
|
137
|
-
# Keep this short, no stack trace.
|
|
138
|
-
logging.error(
|
|
139
|
-
"SSL certificate verification failed while connecting to the server."
|
|
140
|
-
)
|
|
141
156
|
click.echo(ssl_help_text(), err=True)
|
|
142
157
|
raise SystemExit(2)
|
|
143
158
|
|
|
@@ -20,6 +20,10 @@ from cwmscli.utils import (
|
|
|
20
20
|
)
|
|
21
21
|
from cwmscli.utils.click_help import DOCS_BASE_URL
|
|
22
22
|
from cwmscli.utils.deps import requires
|
|
23
|
+
from cwmscli.utils.friendly_errors import (
|
|
24
|
+
is_actionable_service_error,
|
|
25
|
+
is_fatal_service_error,
|
|
26
|
+
)
|
|
23
27
|
|
|
24
28
|
# used to rebuild data URL for images
|
|
25
29
|
DATA_URL_RE = re.compile(r"^data:(?P<mime>[^;]+);base64,(?P<data>.+)$", re.I | re.S)
|
|
@@ -122,7 +126,9 @@ def _default_download_dest(blob_id: str) -> str:
|
|
|
122
126
|
def _blob_media_type(cwms_module, office: str, blob_id: str) -> Optional[str]:
|
|
123
127
|
try:
|
|
124
128
|
result = cwms_module.get_blobs(office_id=office, blob_id_like=blob_id)
|
|
125
|
-
except Exception:
|
|
129
|
+
except Exception as error:
|
|
130
|
+
if is_fatal_service_error(error):
|
|
131
|
+
raise
|
|
126
132
|
return None
|
|
127
133
|
|
|
128
134
|
df = getattr(result, "df", result)
|
|
@@ -222,12 +228,11 @@ def store_blob(**kwargs):
|
|
|
222
228
|
cwms.store_blobs(blob, fail_if_exists=kwargs.get("overwrite"))
|
|
223
229
|
logging.info(f"Successfully stored blob with ID: {blob_id}")
|
|
224
230
|
logging.info(f"View: {view_url}")
|
|
225
|
-
except requests.HTTPError
|
|
226
|
-
|
|
227
|
-
detail = getattr(e.response, "text", "") or str(e)
|
|
228
|
-
logging.error(f"Failed to store blob (HTTP): {detail}")
|
|
229
|
-
sys.exit(1)
|
|
231
|
+
except requests.HTTPError:
|
|
232
|
+
raise
|
|
230
233
|
except Exception as e:
|
|
234
|
+
if is_actionable_service_error(e):
|
|
235
|
+
raise
|
|
231
236
|
logging.error(f"Failed to store blob: {e}")
|
|
232
237
|
sys.exit(1)
|
|
233
238
|
|
|
@@ -257,11 +262,11 @@ def retrieve_blob(**kwargs):
|
|
|
257
262
|
media_type_hint=_blob_media_type(cwms, kwargs.get("office"), blob_id),
|
|
258
263
|
)
|
|
259
264
|
logging.info(f"Downloaded blob to: {blob_id}")
|
|
260
|
-
except requests.HTTPError
|
|
261
|
-
|
|
262
|
-
logging.error(f"Failed to retrieve blob (HTTP): {detail}")
|
|
263
|
-
sys.exit(1)
|
|
265
|
+
except requests.HTTPError:
|
|
266
|
+
raise
|
|
264
267
|
except Exception as e:
|
|
268
|
+
if is_actionable_service_error(e):
|
|
269
|
+
raise
|
|
265
270
|
logging.error(f"Failed to retrieve blob: {e}")
|
|
266
271
|
sys.exit(1)
|
|
267
272
|
|
|
@@ -279,11 +284,11 @@ def delete_blob(**kwargs):
|
|
|
279
284
|
blob_id=kwargs.get("blob_id").upper(),
|
|
280
285
|
)
|
|
281
286
|
logging.info(f"Successfully deleted blob with ID: {blob_id}")
|
|
282
|
-
except requests.HTTPError
|
|
283
|
-
|
|
284
|
-
logging.error(f"Failed to delete blob (HTTP): {details}")
|
|
285
|
-
sys.exit(1)
|
|
287
|
+
except requests.HTTPError:
|
|
288
|
+
raise
|
|
286
289
|
except Exception as e:
|
|
290
|
+
if is_actionable_service_error(e):
|
|
291
|
+
raise
|
|
287
292
|
logging.error(f"Failed to delete blob: {e}")
|
|
288
293
|
sys.exit(1)
|
|
289
294
|
|
|
@@ -550,6 +555,8 @@ def upload_cmd(
|
|
|
550
555
|
)
|
|
551
556
|
logging.info(f"View: {view_url}")
|
|
552
557
|
except requests.HTTPError as e:
|
|
558
|
+
if is_fatal_service_error(e):
|
|
559
|
+
raise
|
|
553
560
|
failures += 1
|
|
554
561
|
detail = getattr(e.response, "text", "") or str(e)
|
|
555
562
|
logging.error(
|
|
@@ -560,6 +567,8 @@ def upload_cmd(
|
|
|
560
567
|
)
|
|
561
568
|
)
|
|
562
569
|
except Exception as e:
|
|
570
|
+
if is_fatal_service_error(e):
|
|
571
|
+
raise
|
|
563
572
|
failures += 1
|
|
564
573
|
logging.error(
|
|
565
574
|
colors.c(
|
|
@@ -619,9 +628,7 @@ def download_cmd(
|
|
|
619
628
|
media_type_hint=_blob_media_type(cwms, office, bid),
|
|
620
629
|
)
|
|
621
630
|
logging.info(f"Downloaded blob to: {saved_target}")
|
|
622
|
-
except requests.HTTPError
|
|
623
|
-
detail = getattr(e.response, "text", "") or str(e)
|
|
624
|
-
logging.error(f"Failed to download (HTTP): {detail}")
|
|
631
|
+
except requests.HTTPError:
|
|
625
632
|
log_scoped_read_hint(
|
|
626
633
|
credential_kind=credential_kind,
|
|
627
634
|
anonymous=anonymous,
|
|
@@ -629,8 +636,17 @@ def download_cmd(
|
|
|
629
636
|
action="download",
|
|
630
637
|
resource="blob content",
|
|
631
638
|
)
|
|
632
|
-
|
|
639
|
+
raise
|
|
633
640
|
except Exception as e:
|
|
641
|
+
if is_actionable_service_error(e):
|
|
642
|
+
log_scoped_read_hint(
|
|
643
|
+
credential_kind=credential_kind,
|
|
644
|
+
anonymous=anonymous,
|
|
645
|
+
office=office,
|
|
646
|
+
action="download",
|
|
647
|
+
resource="blob content",
|
|
648
|
+
)
|
|
649
|
+
raise
|
|
634
650
|
logging.error(format_local_download_error(e, BLOB_DOCS_URL))
|
|
635
651
|
# Local write/path failures are not CDA credential scope problems.
|
|
636
652
|
if not isinstance(e, (OSError, ValueError)):
|
|
@@ -19,6 +19,7 @@ from cwmscli.utils import (
|
|
|
19
19
|
validate_default_download_dest,
|
|
20
20
|
)
|
|
21
21
|
from cwmscli.utils.click_help import DOCS_BASE_URL
|
|
22
|
+
from cwmscli.utils.friendly_errors import is_actionable_service_error
|
|
22
23
|
|
|
23
24
|
CLOB_DOCS_URL = f"{DOCS_BASE_URL}/cli/clob.html"
|
|
24
25
|
|
|
@@ -187,11 +188,11 @@ def upload_cmd(
|
|
|
187
188
|
)
|
|
188
189
|
else:
|
|
189
190
|
logging.info(f"View: {view_url}")
|
|
190
|
-
except requests.HTTPError
|
|
191
|
-
|
|
192
|
-
logging.error(f"Failed to upload (HTTP): {detail}")
|
|
193
|
-
sys.exit(1)
|
|
191
|
+
except requests.HTTPError:
|
|
192
|
+
raise
|
|
194
193
|
except Exception as e:
|
|
194
|
+
if is_actionable_service_error(e):
|
|
195
|
+
raise
|
|
195
196
|
logging.error(f"Failed to upload: {e}")
|
|
196
197
|
sys.exit(1)
|
|
197
198
|
|
|
@@ -231,9 +232,7 @@ def download_cmd(
|
|
|
231
232
|
target = dest or _default_download_dest(bid)
|
|
232
233
|
_write_clob_content(content, target)
|
|
233
234
|
logging.info(f"Downloaded clob to: {target}")
|
|
234
|
-
except requests.HTTPError
|
|
235
|
-
detail = getattr(e.response, "text", "") or str(e)
|
|
236
|
-
logging.error(f"Failed to download (HTTP): {detail}")
|
|
235
|
+
except requests.HTTPError:
|
|
237
236
|
log_scoped_read_hint(
|
|
238
237
|
credential_kind=credential_kind,
|
|
239
238
|
anonymous=anonymous,
|
|
@@ -241,8 +240,17 @@ def download_cmd(
|
|
|
241
240
|
action="download",
|
|
242
241
|
resource="clob content",
|
|
243
242
|
)
|
|
244
|
-
|
|
243
|
+
raise
|
|
245
244
|
except Exception as e:
|
|
245
|
+
if is_actionable_service_error(e):
|
|
246
|
+
log_scoped_read_hint(
|
|
247
|
+
credential_kind=credential_kind,
|
|
248
|
+
anonymous=anonymous,
|
|
249
|
+
office=office,
|
|
250
|
+
action="download",
|
|
251
|
+
resource="clob content",
|
|
252
|
+
)
|
|
253
|
+
raise
|
|
246
254
|
logging.error(format_local_download_error(e, CLOB_DOCS_URL))
|
|
247
255
|
sys.exit(1)
|
|
248
256
|
|
|
@@ -23,6 +23,7 @@ from cwmscli.utils import (
|
|
|
23
23
|
)
|
|
24
24
|
from cwmscli.utils.auth import DEFAULT_REDIRECT_HOST, DEFAULT_REDIRECT_PORT
|
|
25
25
|
from cwmscli.utils.deps import requires
|
|
26
|
+
from cwmscli.utils.interaction import is_non_interactive
|
|
26
27
|
from cwmscli.utils.update import (
|
|
27
28
|
build_update_package_spec,
|
|
28
29
|
get_update_environment,
|
|
@@ -455,6 +456,12 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
|
|
|
455
456
|
cmd.append("--pre")
|
|
456
457
|
|
|
457
458
|
if not yes:
|
|
459
|
+
if is_non_interactive():
|
|
460
|
+
raise click.ClickException(
|
|
461
|
+
"The update command requires confirmation, but prompting is "
|
|
462
|
+
"disabled in non-interactive mode. Re-run with --yes to authorize "
|
|
463
|
+
"the update."
|
|
464
|
+
)
|
|
458
465
|
proceed = click.confirm(
|
|
459
466
|
"Proceed with updating cwms-cli in this environment via pip?",
|
|
460
467
|
default=True,
|
|
@@ -527,15 +534,13 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
|
|
|
527
534
|
@click.group(
|
|
528
535
|
"blob",
|
|
529
536
|
help="Manage CWMS Blobs (upload, download, delete, update, list)",
|
|
530
|
-
epilog=textwrap.dedent(
|
|
531
|
-
"""
|
|
537
|
+
epilog=textwrap.dedent("""
|
|
532
538
|
Example Usage:\n
|
|
533
539
|
- Store a PDF/image as a CWMS blob with optional description\n
|
|
534
540
|
- Download a blob by id to your local filesystem\n
|
|
535
541
|
- Update a blob's name/description/mime-type\n
|
|
536
542
|
- Bulk list blobs for an office
|
|
537
|
-
"""
|
|
538
|
-
),
|
|
543
|
+
"""),
|
|
539
544
|
)
|
|
540
545
|
def blob_group():
|
|
541
546
|
pass
|
|
@@ -748,14 +753,12 @@ def list_cmd(**kwargs):
|
|
|
748
753
|
@click.group(
|
|
749
754
|
"clob",
|
|
750
755
|
help="Manage CWMS Clobs (upload, download, delete, update, list)",
|
|
751
|
-
epilog=textwrap.dedent(
|
|
752
|
-
"""
|
|
756
|
+
epilog=textwrap.dedent("""
|
|
753
757
|
Example Usage:\n
|
|
754
758
|
- Download a clob by id to your local filesystem\n
|
|
755
759
|
- Update a clob's name/description/mime-type\n
|
|
756
760
|
- Bulk list clobs for an office
|
|
757
|
-
"""
|
|
758
|
-
),
|
|
761
|
+
"""),
|
|
759
762
|
)
|
|
760
763
|
@requires(reqs.cwms)
|
|
761
764
|
def clob_group():
|
|
@@ -1010,7 +1013,7 @@ def users_roles_list_user(user_name, office, api_root, api_key, api_key_loc):
|
|
|
1010
1013
|
multiple=True,
|
|
1011
1014
|
default=None,
|
|
1012
1015
|
callback=csv_to_list,
|
|
1013
|
-
help="enter admin, readonly, readwrite, or individual role name(s) to add. Repeat the option or pass a comma/pipe-separated list.",
|
|
1016
|
+
help="enter admin, readonly, readwrite, batchadmin, or individual role name(s) to add. Repeat the option or pass a comma/pipe-separated list.",
|
|
1014
1017
|
)
|
|
1015
1018
|
@requires(reqs.cwms)
|
|
1016
1019
|
def users_roles_add(office, api_root, api_key, api_key_loc, user_name, roles):
|
|
@@ -2,6 +2,8 @@ import traceback
|
|
|
2
2
|
|
|
3
3
|
import cwms
|
|
4
4
|
|
|
5
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def write_timeseries(
|
|
7
9
|
file_name, ts_data, config_item, office, dry_run, config_path, logger
|
|
@@ -26,6 +28,8 @@ def write_timeseries(
|
|
|
26
28
|
)
|
|
27
29
|
logger.info(f"Stored {ts_object['name']} values")
|
|
28
30
|
except Exception as e:
|
|
31
|
+
if is_fatal_service_error(e):
|
|
32
|
+
raise
|
|
29
33
|
logger.error(
|
|
30
34
|
f"Error posting data for {file_name}: {e}\n{traceback.format_exc()}"
|
|
31
35
|
)
|
|
@@ -16,6 +16,9 @@ from cwmscli.utils.env_store import (
|
|
|
16
16
|
load_env,
|
|
17
17
|
save_env,
|
|
18
18
|
)
|
|
19
|
+
from cwmscli.utils.friendly_errors import to_user_facing_error
|
|
20
|
+
from cwmscli.utils.interaction import is_non_interactive
|
|
21
|
+
from cwmscli.utils.ssl_errors import is_cert_verify_error, ssl_help_text
|
|
19
22
|
|
|
20
23
|
SENSITIVE_KEYS = {"CDA_API_KEY"}
|
|
21
24
|
|
|
@@ -43,11 +46,16 @@ def _check_env(env_config: Dict[str, str]) -> Dict:
|
|
|
43
46
|
resp = requests.get(url, timeout=5)
|
|
44
47
|
latency_ms = int((time.monotonic() - t0) * 1000)
|
|
45
48
|
except requests.RequestException as e:
|
|
49
|
+
if is_cert_verify_error(e):
|
|
50
|
+
error = ssl_help_text().strip()
|
|
51
|
+
else:
|
|
52
|
+
friendly = to_user_facing_error(e)
|
|
53
|
+
error = friendly.format_message() if friendly is not None else str(e)
|
|
46
54
|
return {
|
|
47
55
|
"reachable": False,
|
|
48
56
|
"latency_ms": None,
|
|
49
57
|
"auth": "skipped",
|
|
50
|
-
"error":
|
|
58
|
+
"error": error,
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
if resp.status_code >= 400:
|
|
@@ -69,12 +77,17 @@ def _check_env(env_config: Dict[str, str]) -> Dict:
|
|
|
69
77
|
|
|
70
78
|
try:
|
|
71
79
|
auth_resp = requests.get(url, headers={"Authorization": api_key}, timeout=5)
|
|
72
|
-
except requests.RequestException:
|
|
80
|
+
except requests.RequestException as e:
|
|
81
|
+
if is_cert_verify_error(e):
|
|
82
|
+
error = ssl_help_text().strip()
|
|
83
|
+
else:
|
|
84
|
+
friendly = to_user_facing_error(e)
|
|
85
|
+
error = friendly.format_message() if friendly is not None else str(e)
|
|
73
86
|
return {
|
|
74
87
|
"reachable": True,
|
|
75
88
|
"latency_ms": latency_ms,
|
|
76
89
|
"auth": "failed",
|
|
77
|
-
"error":
|
|
90
|
+
"error": error,
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
if auth_resp.status_code == 401:
|
|
@@ -82,7 +95,21 @@ def _check_env(env_config: Dict[str, str]) -> Dict:
|
|
|
82
95
|
"reachable": True,
|
|
83
96
|
"latency_ms": latency_ms,
|
|
84
97
|
"auth": "failed",
|
|
85
|
-
"error":
|
|
98
|
+
"error": (
|
|
99
|
+
"CDA rejected the API key (HTTP 401). Update CDA_API_KEY in this "
|
|
100
|
+
"environment, then retry."
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if auth_resp.status_code == 403:
|
|
105
|
+
return {
|
|
106
|
+
"reachable": True,
|
|
107
|
+
"latency_ms": latency_ms,
|
|
108
|
+
"auth": "failed",
|
|
109
|
+
"error": (
|
|
110
|
+
"CDA recognized the credentials but denied access (HTTP 403). "
|
|
111
|
+
"Confirm the account's roles and office access."
|
|
112
|
+
),
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
return {"reachable": True, "latency_ms": latency_ms, "auth": "ok", "error": None}
|
|
@@ -210,6 +237,8 @@ def show_cmd(check: bool):
|
|
|
210
237
|
auth_str = click.style("authenticated", fg="green")
|
|
211
238
|
elif result["auth"] == "failed":
|
|
212
239
|
auth_str = click.style("auth failed", fg="red")
|
|
240
|
+
if result["error"]:
|
|
241
|
+
auth_str += f" — {result['error']}"
|
|
213
242
|
|
|
214
243
|
click.echo(f" Connect: {reach_str}")
|
|
215
244
|
if auth_str:
|
|
@@ -227,6 +256,12 @@ def delete_cmd(env_name: str, yes: bool):
|
|
|
227
256
|
cwms-cli env delete myenv
|
|
228
257
|
cwms-cli env delete myenv --yes
|
|
229
258
|
"""
|
|
259
|
+
if not yes and is_non_interactive():
|
|
260
|
+
raise click.ClickException(
|
|
261
|
+
"Deleting an environment requires confirmation, but prompting is "
|
|
262
|
+
"disabled in non-interactive mode. Re-run with --yes to authorize "
|
|
263
|
+
"the deletion."
|
|
264
|
+
)
|
|
230
265
|
if not yes and not click.confirm(f"Delete environment '{env_name}'?"):
|
|
231
266
|
click.echo("Cancelled")
|
|
232
267
|
return
|
|
@@ -6,6 +6,7 @@ import cwms
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
8
|
from cwmscli.utils import init_cwms_session
|
|
9
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def import_shef_critfile(
|
|
@@ -97,6 +98,8 @@ def import_shef_critfile(
|
|
|
97
98
|
f'SUCCESS Stored timeseries ID {data["Timeseries ID"]} to {group_id}'
|
|
98
99
|
)
|
|
99
100
|
except Exception as error:
|
|
101
|
+
if is_fatal_service_error(error):
|
|
102
|
+
raise
|
|
100
103
|
logging.error(
|
|
101
104
|
f'FAIL Data could not be stored to CWMS database for --> {data["Timeseries ID"]},{data["Alias"]} error = {error}'
|
|
102
105
|
)
|
|
@@ -50,6 +50,8 @@ from typing import Optional
|
|
|
50
50
|
|
|
51
51
|
import pandas as pd
|
|
52
52
|
|
|
53
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
54
|
+
|
|
53
55
|
try:
|
|
54
56
|
import cwms
|
|
55
57
|
import cwms.api as cwms_api
|
|
@@ -599,6 +601,8 @@ def _filter_existing_tsids(
|
|
|
599
601
|
cwms.get_timeseries_identifier(ts_id=e["tsid"], office_id=office_id)
|
|
600
602
|
valid.append(e)
|
|
601
603
|
except Exception as exc:
|
|
604
|
+
if is_fatal_service_error(exc):
|
|
605
|
+
raise
|
|
602
606
|
log.warning(
|
|
603
607
|
"Skipping missing TSID '%s' (%s)",
|
|
604
608
|
e["tsid"],
|
|
@@ -619,6 +623,8 @@ def _save(
|
|
|
619
623
|
log.info("SUCCESS — group stored via store_timeseries_groups.")
|
|
620
624
|
return
|
|
621
625
|
except Exception as exc:
|
|
626
|
+
if is_fatal_service_error(exc):
|
|
627
|
+
raise
|
|
622
628
|
log.warning(
|
|
623
629
|
"store_timeseries_groups raised %s: %s — retrying with update ...",
|
|
624
630
|
type(exc).__name__,
|
|
@@ -651,6 +657,8 @@ def store_group(
|
|
|
651
657
|
_save(group_json, group_id, office_id, fail_if_exists)
|
|
652
658
|
return
|
|
653
659
|
except Exception as exc:
|
|
660
|
+
if is_fatal_service_error(exc):
|
|
661
|
+
raise
|
|
654
662
|
if entries is None:
|
|
655
663
|
raise
|
|
656
664
|
log.warning(
|
|
@@ -5,6 +5,7 @@ from typing import Optional, Union
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
7
|
from cwmscli.utils import colors, init_cwms_session
|
|
8
|
+
from cwmscli.utils.interaction import is_non_interactive
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _format_table(headers: list[str], rows: list[list[str]]) -> str:
|
|
@@ -57,11 +58,45 @@ def _fetch_roles(cwms_module) -> list[str]:
|
|
|
57
58
|
_handle_api_error(error, cwms_module)
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
# Role shortcut expansions used by `_expand_role_shortcuts` and
|
|
62
|
+
# referenced during validation so expanded roles are accepted even when the
|
|
63
|
+
# CWMS role catalog returned by the API does not include all shortcut values.
|
|
64
|
+
ROLE_SHORTCUTS: dict[str, list[str]] = {
|
|
65
|
+
"admin": [
|
|
66
|
+
"All Users",
|
|
67
|
+
"CWMS Users",
|
|
68
|
+
"TS ID Creator",
|
|
69
|
+
"CWMS User Admins",
|
|
70
|
+
"CWMS PD Users",
|
|
71
|
+
"Data Acquisition Mgr",
|
|
72
|
+
],
|
|
73
|
+
"readonly": ["All Users", "CWMS Users"],
|
|
74
|
+
"readwrite": ["All Users", "CWMS Users", "TS ID Creator"],
|
|
75
|
+
"batchadmin": [
|
|
76
|
+
"All Users",
|
|
77
|
+
"CWMS Users",
|
|
78
|
+
"TS ID Creator",
|
|
79
|
+
"Data Acquisition Mgr",
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
60
84
|
def _fetch_users(
|
|
61
85
|
cwms_module, office=None, username_like=None, page_size: int = 5000
|
|
62
86
|
) -> list[dict]:
|
|
63
87
|
users: list[dict] = []
|
|
64
88
|
|
|
89
|
+
# If a username_like filter was provided, ensure it is wrapped with
|
|
90
|
+
# leading and trailing wildcards so CDA receives a contains-style query.
|
|
91
|
+
if username_like:
|
|
92
|
+
_ul = str(username_like).strip()
|
|
93
|
+
if _ul:
|
|
94
|
+
if not _ul.startswith("*"):
|
|
95
|
+
_ul = "*" + _ul
|
|
96
|
+
if not _ul.endswith("*"):
|
|
97
|
+
_ul = _ul + "*"
|
|
98
|
+
username_like = _ul
|
|
99
|
+
|
|
65
100
|
try:
|
|
66
101
|
response = cwms_module.get_users(
|
|
67
102
|
office_id=office, username_like=username_like, page_size=page_size
|
|
@@ -134,19 +169,13 @@ def _split_roles(
|
|
|
134
169
|
|
|
135
170
|
|
|
136
171
|
def _expand_role_shortcuts(roles: list[str]) -> list[str]:
|
|
137
|
-
emap = {
|
|
138
|
-
"admin": ["All Users", "CWMS Users", "TS ID Creator", "CWMS User Admins"],
|
|
139
|
-
"readonly": ["All Users", "CWMS Users"],
|
|
140
|
-
"readwrite": ["All Users", "CWMS Users", "TS ID Creator"],
|
|
141
|
-
}
|
|
142
|
-
|
|
143
172
|
expanded_roles: list[str] = []
|
|
144
173
|
for role in roles:
|
|
145
174
|
key = role.strip().casefold()
|
|
146
175
|
if key == "all":
|
|
147
176
|
expanded_roles.append(role) # Keep "all" as is, handled elsewhere
|
|
148
|
-
elif key in
|
|
149
|
-
expanded_roles.extend(
|
|
177
|
+
elif key in ROLE_SHORTCUTS:
|
|
178
|
+
expanded_roles.extend(ROLE_SHORTCUTS[key])
|
|
150
179
|
else:
|
|
151
180
|
expanded_roles.append(role)
|
|
152
181
|
return expanded_roles
|
|
@@ -325,6 +354,12 @@ def add_roles(
|
|
|
325
354
|
f"{_cmd('cwms-cli users roles add')} interactively with no add-specific args."
|
|
326
355
|
)
|
|
327
356
|
|
|
357
|
+
if not provided_user_name and not provided_roles and is_non_interactive():
|
|
358
|
+
raise click.ClickException(
|
|
359
|
+
"User role changes cannot prompt in non-interactive mode. Specify "
|
|
360
|
+
"both --user-name and --roles."
|
|
361
|
+
)
|
|
362
|
+
|
|
328
363
|
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
329
364
|
users = _fetch_users(cwms)
|
|
330
365
|
available_roles = _fetch_roles(cwms)
|
|
@@ -373,6 +408,12 @@ def delete_roles(
|
|
|
373
408
|
f"{_cmd('cwms-cli users roles delete')} interactively with no delete-specific args."
|
|
374
409
|
)
|
|
375
410
|
|
|
411
|
+
if not provided_user_name and not provided_roles and is_non_interactive():
|
|
412
|
+
raise click.ClickException(
|
|
413
|
+
"User role changes cannot prompt in non-interactive mode. Specify "
|
|
414
|
+
"both --user-name and --roles."
|
|
415
|
+
)
|
|
416
|
+
|
|
376
417
|
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
377
418
|
users = _fetch_users(cwms, office)
|
|
378
419
|
available_roles = _fetch_roles(cwms)
|
|
@@ -7,6 +7,7 @@ from typing import Callable, Iterable, Optional, Protocol
|
|
|
7
7
|
|
|
8
8
|
from cwmscli.dss.naming import ExportRule, ImportRule
|
|
9
9
|
from cwmscli.utils import colors, init_cwms_session
|
|
10
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
@@ -71,7 +72,9 @@ def transfer_all(
|
|
|
71
72
|
colors.c(str(timeseries.name), "cyan", bright=True),
|
|
72
73
|
)
|
|
73
74
|
summary.transferred += 1
|
|
74
|
-
except Exception:
|
|
75
|
+
except Exception as error:
|
|
76
|
+
if is_fatal_service_error(error):
|
|
77
|
+
raise
|
|
75
78
|
summary.failed += 1
|
|
76
79
|
logger.exception("Failed to transfer %s", colors.err(str(identifier)))
|
|
77
80
|
return summary
|
|
@@ -8,6 +8,7 @@ import cwms
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
10
|
from cwmscli.utils import init_cwms_session
|
|
11
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
11
12
|
from cwmscli.utils.links import CDA_REGEXP_GUIDE_URL
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -89,6 +90,8 @@ def load_locations(
|
|
|
89
90
|
if verbose:
|
|
90
91
|
logger.info("%s", result)
|
|
91
92
|
except Exception as e:
|
|
93
|
+
if is_fatal_service_error(e):
|
|
94
|
+
raise
|
|
92
95
|
errors += 1
|
|
93
96
|
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
94
97
|
|
|
@@ -8,6 +8,7 @@ import cwms
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
10
|
from cwmscli.utils import init_cwms_session
|
|
11
|
+
from cwmscli.utils.friendly_errors import is_fatal_service_error
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -129,6 +130,8 @@ def copy_from_group(
|
|
|
129
130
|
if verbose:
|
|
130
131
|
logger.info("\tStored successfully.")
|
|
131
132
|
except Exception as e:
|
|
133
|
+
if is_fatal_service_error(e):
|
|
134
|
+
raise
|
|
132
135
|
errors += 1
|
|
133
136
|
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
134
137
|
|