cwms-cli 0.7.4__tar.gz → 0.8.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.
Files changed (87) hide show
  1. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/PKG-INFO +30 -2
  2. cwms_cli-0.8.0/README.md +66 -0
  3. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/__main__.py +29 -3
  4. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/_generated/ownership_data.py +11 -1
  5. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/blob.py +3 -2
  6. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/clob.py +28 -8
  7. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/commands_cwms.py +60 -11
  8. cwms_cli-0.8.0/cwmscli/commands/env.py +508 -0
  9. cwms_cli-0.8.0/cwmscli/dss/__init__.py +5 -0
  10. cwms_cli-0.8.0/cwmscli/dss/cli.py +352 -0
  11. cwms_cli-0.8.0/cwmscli/dss/naming.py +318 -0
  12. cwms_cli-0.8.0/cwmscli/dss/transfer.py +215 -0
  13. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/location/location_ids.py +8 -0
  14. cwms_cli-0.8.0/cwmscli/load/root.py +335 -0
  15. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/timeseries/timeseries.py +3 -2
  16. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/requirements.py +16 -0
  17. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/__init__.py +1 -0
  18. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/auth.py +3 -6
  19. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/click_help.py +20 -22
  20. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/deps.py +3 -2
  21. cwms_cli-0.8.0/cwmscli/utils/env_store.py +133 -0
  22. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/friendly_errors.py +88 -0
  23. cwms_cli-0.8.0/cwmscli/utils/links.py +5 -0
  24. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/logging/__init__.py +1 -1
  25. cwms_cli-0.8.0/cwmscli/utils/paths.py +23 -0
  26. cwms_cli-0.8.0/cwmscli/utils/update.py +155 -0
  27. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/pyproject.toml +8 -5
  28. cwms_cli-0.7.4/README.md +0 -42
  29. cwms_cli-0.7.4/cwmscli/load/root.py +0 -190
  30. cwms_cli-0.7.4/cwmscli/utils/links.py +0 -1
  31. cwms_cli-0.7.4/cwmscli/utils/update.py +0 -61
  32. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/LICENSE +0 -0
  33. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/__init__.py +0 -0
  34. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/_generated/__init__.py +0 -0
  35. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/callbacks/__init__.py +0 -0
  36. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
  37. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/README.md +0 -0
  38. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
  39. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
  40. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/config.py +0 -0
  41. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
  42. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
  43. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
  44. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
  45. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
  46. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
  47. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
  48. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
  49. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
  50. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
  51. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
  52. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
  53. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
  54. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
  55. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
  56. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
  57. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
  58. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
  59. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
  60. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
  61. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/shef/__init__.py +0 -0
  62. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/shef/import_critfile.py +0 -0
  63. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/shef/import_infile.py +0 -0
  64. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/shef/shef_parameters.csv +0 -0
  65. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/commands/users.py +0 -0
  66. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/README.md +0 -0
  67. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/__init__.py +0 -0
  68. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/__main__.py +0 -0
  69. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/location/location.py +0 -0
  70. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
  71. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/timeseries/timeseries_data.py +0 -0
  72. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
  73. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/ownership.py +0 -0
  74. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/__init__.py +0 -0
  75. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/__main__.py +0 -0
  76. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
  77. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/getusgs_cda.py +0 -0
  78. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
  79. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
  80. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/callback_success.html +0 -0
  81. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/colors.py +0 -0
  82. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/intervals.py +0 -0
  83. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/io.py +0 -0
  84. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/logging/formatters.py +0 -0
  85. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/ssl_errors.py +0 -0
  86. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/version.py +0 -0
  87. {cwms_cli-0.7.4 → cwms_cli-0.8.0}/cwmscli/utils/version_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cwms-cli
3
- Version: 0.7.4
3
+ Version: 0.8.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,8 +16,12 @@ 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
+ Provides-Extra: dss
19
20
  Requires-Dist: click (>=8.1.8,<9.0.0)
20
21
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
22
+ Requires-Dist: cwms-python (>=1.0.7) ; extra == "dss"
23
+ Requires-Dist: hec-python-library (>=0.9.5) ; extra == "dss"
24
+ Requires-Dist: hecdss (>=0.1.24) ; extra == "dss"
21
25
  Requires-Dist: requests (>=2.32.0,<3.0.0) ; python_version == "3.9"
22
26
  Requires-Dist: requests (>=2.33.0,<3.0.0) ; python_version >= "3.10"
23
27
  Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-cli
@@ -39,7 +43,7 @@ Note: You may need to run `python -m pip install cwms-cli` if PIP is not in your
39
43
 
40
44
  ### Update
41
45
  ```sh
42
- pip install cwms-cli --upgrade
46
+ python -m pip install --upgrade cwms-cli
43
47
  ```
44
48
 
45
49
  Or as of version `0.3.0+`
@@ -47,6 +51,12 @@ Or as of version `0.3.0+`
47
51
  cwms-cli update
48
52
  ```
49
53
 
54
+ `cwms-cli update` displays the Python executable, environment, package metadata
55
+ location, and editable project location when applicable before asking for
56
+ confirmation. It runs pip through the same Python interpreter that is running
57
+ `cwms-cli`, avoiding accidental updates to a different Python installation or
58
+ virtual environment.
59
+
50
60
  To install a specific version:
51
61
  ```sh
52
62
  cwms-cli update --target-version 0.7.1 --yes
@@ -66,3 +76,21 @@ from cwmscli.usgs.getusgs_measurements_cda import getusgs_measurements_cda
66
76
  from cwmscli.usgs.getUSGS_ratings_cda import getusgs_rating_cda
67
77
  ```
68
78
 
79
+ ## Development environment
80
+
81
+ The repository includes a Linux/Python 3.12 development container exercised by
82
+ a dedicated CI job. Open the repository with the VS Code Dev Containers
83
+ extension, or use the Dev Container CLI:
84
+
85
+ ```sh
86
+ devcontainer up --workspace-folder .
87
+ devcontainer exec --workspace-folder . poetry run pytest -q
88
+ ```
89
+
90
+ The container installs project dependencies with Poetry and uses
91
+ `/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. Use the container
93
+ for changes involving time zones, paths, native libraries, or other
94
+ operating-system-dependent behavior. Standard Python 3.9 and Python 3.12
95
+ installations remain covered by the existing CI matrix.
96
+
@@ -0,0 +1,66 @@
1
+ # cwms-cli
2
+
3
+ A collection of scripts to create, read, update, list, and delete data through CWMS Data API (CDA) and other commonly used API in the US Army Corps of Engineers water management. CWMS-CLI wraps these API in a friendly to use terminal based interface.
4
+
5
+ [![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](https://cwms-cli.readthedocs.io/en/latest/cli.html#cwms-cli) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install cwms-cli
11
+ ```
12
+ Note: You may need to run `python -m pip install cwms-cli` if PIP is not in your path.
13
+
14
+
15
+ ### Update
16
+ ```sh
17
+ python -m pip install --upgrade cwms-cli
18
+ ```
19
+
20
+ Or as of version `0.3.0+`
21
+ ```sh
22
+ cwms-cli update
23
+ ```
24
+
25
+ `cwms-cli update` displays the Python executable, environment, package metadata
26
+ location, and editable project location when applicable before asking for
27
+ confirmation. It runs pip through the same Python interpreter that is running
28
+ `cwms-cli`, avoiding accidental updates to a different Python installation or
29
+ virtual environment.
30
+
31
+ To install a specific version:
32
+ ```sh
33
+ cwms-cli update --target-version 0.7.1 --yes
34
+ ```
35
+
36
+ ## Command line implementation
37
+
38
+ View the help in terminal:
39
+ ```sh
40
+ cwms-cli --help
41
+ ```
42
+
43
+ ## run from within python
44
+ ```python
45
+ from cwmscli.usgs.getusgs_cda import getusgs_cda
46
+ from cwmscli.usgs.getusgs_measurements_cda import getusgs_measurements_cda
47
+ from cwmscli.usgs.getUSGS_ratings_cda import getusgs_rating_cda
48
+ ```
49
+
50
+ ## Development environment
51
+
52
+ The repository includes a Linux/Python 3.12 development container exercised by
53
+ a dedicated CI job. Open the repository with the VS Code Dev Containers
54
+ extension, or use the Dev Container CLI:
55
+
56
+ ```sh
57
+ devcontainer up --workspace-folder .
58
+ devcontainer exec --workspace-folder . poetry run pytest -q
59
+ ```
60
+
61
+ The container installs project dependencies with Poetry and uses
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. Use the container
64
+ for changes involving time zones, paths, native libraries, or other
65
+ operating-system-dependent behavior. Standard Python 3.9 and Python 3.12
66
+ installations remain covered by the existing CI matrix.
@@ -7,10 +7,17 @@ import click
7
7
  from click.core import ParameterSource
8
8
 
9
9
  from cwmscli.commands import commands_cwms
10
+ from cwmscli.commands.env import env_group
11
+ from cwmscli.dss import dss_group
10
12
  from cwmscli.load import __main__ as load
11
13
  from cwmscli.usgs import usgs_group
12
14
  from cwmscli.utils.click_help import add_version_to_help_tree
13
- from cwmscli.utils.friendly_errors import to_user_facing_error
15
+ from cwmscli.utils.friendly_errors import (
16
+ cda_stack_trace,
17
+ format_cda_stack_trace,
18
+ to_user_facing_error,
19
+ )
20
+ from cwmscli.utils.links import BUG_REPORT_URL
14
21
  from cwmscli.utils.logging import (
15
22
  LoggingConfig,
16
23
  apply_logging_policies,
@@ -88,7 +95,9 @@ cli.add_command(commands_cwms.update_cli_cmd)
88
95
  cli.add_command(commands_cwms.blob_group)
89
96
  cli.add_command(commands_cwms.clob_group)
90
97
  cli.add_command(commands_cwms.users_group)
98
+ cli.add_command(env_group)
91
99
  cli.add_command(load.load_group)
100
+ cli.add_command(dss_group)
92
101
  add_version_to_help_tree(cli)
93
102
 
94
103
 
@@ -111,9 +120,19 @@ def main() -> None:
111
120
  except SystemExit:
112
121
  raise
113
122
  except click.ClickException as e:
123
+ debug = debug or logging.getLogger().isEnabledFor(logging.DEBUG)
124
+ if debug:
125
+ server_stack_trace = cda_stack_trace(e)
126
+ if server_stack_trace is not None:
127
+ click.echo(format_cda_stack_trace(server_stack_trace), err=True)
128
+ raise SystemExit(e.exit_code)
114
129
  e.show()
115
130
  raise SystemExit(e.exit_code)
116
131
  except Exception as e:
132
+ # The environment switch supports failures before Click configures logging.
133
+ # Once CLI setup has run, --log-level DEBUG enables the same behavior.
134
+ debug = debug or logging.getLogger().isEnabledFor(logging.DEBUG)
135
+
117
136
  if is_cert_verify_error(e) and not debug:
118
137
  # Keep this short, no stack trace.
119
138
  logging.error(
@@ -122,14 +141,21 @@ def main() -> None:
122
141
  click.echo(ssl_help_text(), err=True)
123
142
  raise SystemExit(2)
124
143
 
125
- if not debug:
144
+ if debug:
145
+ server_stack_trace = cda_stack_trace(e)
146
+ if server_stack_trace is not None:
147
+ click.echo(format_cda_stack_trace(server_stack_trace), err=True)
148
+ raise SystemExit(1)
149
+ else:
126
150
  friendly_error = to_user_facing_error(e)
127
151
  if friendly_error is not None:
128
152
  logging.debug("Suppressed traceback for CLI exception", exc_info=e)
129
153
  friendly_error.show()
130
154
  raise SystemExit(friendly_error.exit_code)
131
155
 
132
- # If debug is enabled (or it's not a cert verify error), keep the normal failure behavior.
156
+ click.echo(f"Unexpected error. Report it at {BUG_REPORT_URL}", err=True)
157
+ # Preserve raw exception behavior when CDA did not provide a server stack
158
+ # trace so an issue report includes useful diagnostic details.
133
159
  raise
134
160
 
135
161
 
@@ -15,6 +15,16 @@ OWNERSHIP_DATA = {
15
15
  "name": "Charles Graham"
16
16
  }
17
17
  ],
18
+ "cwms-cli env": [
19
+ {
20
+ "email": "mitchell.s.weier@usace.army.mil",
21
+ "name": "Mitch Weier"
22
+ },
23
+ {
24
+ "email": "charles.r.graham@usace.army.mil",
25
+ "name": "Charles Graham"
26
+ }
27
+ ],
18
28
  "cwms-cli load": [
19
29
  {
20
30
  "email": "charles.r.graham@usace.army.mil",
@@ -25,7 +35,7 @@ OWNERSHIP_DATA = {
25
35
  "name": "Eric Novotny"
26
36
  }
27
37
  ],
28
- "cwms-cli shefcritimport": [
38
+ "cwms-cli shef": [
29
39
  {
30
40
  "email": "eric.v.novotny@usace.army.mil",
31
41
  "name": "Eric Novotny"
@@ -613,12 +613,12 @@ def download_cmd(
613
613
  try:
614
614
  blob_content = cwms.get_blob(office_id=office, blob_id=bid)
615
615
  target = dest or _default_download_dest(bid)
616
- _save_blob_content(
616
+ saved_target = _save_blob_content(
617
617
  blob_content,
618
618
  dest=target,
619
619
  media_type_hint=_blob_media_type(cwms, office, bid),
620
620
  )
621
- logging.info(f"Downloaded blob to: {target}")
621
+ logging.info(f"Downloaded blob to: {saved_target}")
622
622
  except requests.HTTPError as e:
623
623
  detail = getattr(e.response, "text", "") or str(e)
624
624
  logging.error(f"Failed to download (HTTP): {detail}")
@@ -632,6 +632,7 @@ def download_cmd(
632
632
  sys.exit(1)
633
633
  except Exception as e:
634
634
  logging.error(format_local_download_error(e, BLOB_DOCS_URL))
635
+ # Local write/path failures are not CDA credential scope problems.
635
636
  if not isinstance(e, (OSError, ValueError)):
636
637
  log_scoped_read_hint(
637
638
  credential_kind=credential_kind,
@@ -12,10 +12,15 @@ from cwms import api as cwms_api
12
12
  from cwmscli.utils import (
13
13
  format_local_download_error,
14
14
  get_api_key,
15
+ get_saved_login_token,
15
16
  has_invalid_chars,
17
+ init_cwms_session,
16
18
  log_scoped_read_hint,
17
19
  validate_default_download_dest,
18
20
  )
21
+ from cwmscli.utils.click_help import DOCS_BASE_URL
22
+
23
+ CLOB_DOCS_URL = f"{DOCS_BASE_URL}/cli/clob.html"
19
24
 
20
25
 
21
26
  def _join_api_url(api_root: str, path: str) -> str:
@@ -28,6 +33,16 @@ def _resolve_optional_api_key(api_key: Optional[str], anonymous: bool) -> Option
28
33
  return get_api_key(api_key, None)
29
34
 
30
35
 
36
+ def _resolve_credential_kind(api_key: Optional[str], anonymous: bool) -> Optional[str]:
37
+ if anonymous:
38
+ return None
39
+ if get_saved_login_token():
40
+ return "token"
41
+ if _resolve_optional_api_key(api_key, anonymous):
42
+ return "api_key"
43
+ return None
44
+
45
+
31
46
  def _write_clob_content(content: str, dest: str) -> str:
32
47
  os.makedirs(os.path.dirname(dest) or ".", exist_ok=True)
33
48
  with open(dest, "w", encoding="utf-8", newline="") as f:
@@ -36,12 +51,17 @@ def _write_clob_content(content: str, dest: str) -> str:
36
51
 
37
52
 
38
53
  def _default_download_dest(clob_id: str) -> str:
39
- return validate_default_download_dest(clob_id, resource_name="Clob")
54
+ return validate_default_download_dest(
55
+ clob_id,
56
+ resource_name="Clob",
57
+ docs_url=CLOB_DOCS_URL,
58
+ )
40
59
 
41
60
 
42
61
  def _clob_endpoint_id(clob_id: str) -> tuple[str, Optional[str]]:
43
62
  normalized = clob_id.upper()
44
63
  if has_invalid_chars(normalized):
64
+ # Path-like IDs use the placeholder route and keep the real ID in query params.
45
65
  return "ignored", normalized
46
66
  return normalized, None
47
67
 
@@ -190,8 +210,8 @@ def download_cmd(
190
210
  f"DRY RUN: would GET {api_root} clob with clob-id={clob_id} office={office}."
191
211
  )
192
212
  return
193
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
194
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
213
+ credential_kind = _resolve_credential_kind(api_key, anonymous)
214
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
195
215
  bid = clob_id.upper()
196
216
  logging.debug(f"Office={office} clobID={bid}")
197
217
 
@@ -215,7 +235,7 @@ def download_cmd(
215
235
  detail = getattr(e.response, "text", "") or str(e)
216
236
  logging.error(f"Failed to download (HTTP): {detail}")
217
237
  log_scoped_read_hint(
218
- api_key=resolved_api_key,
238
+ credential_kind=credential_kind,
219
239
  anonymous=anonymous,
220
240
  office=office,
221
241
  action="download",
@@ -223,7 +243,7 @@ def download_cmd(
223
243
  )
224
244
  sys.exit(1)
225
245
  except Exception as e:
226
- logging.error(format_local_download_error(e, ""))
246
+ logging.error(format_local_download_error(e, CLOB_DOCS_URL))
227
247
  sys.exit(1)
228
248
 
229
249
 
@@ -305,8 +325,8 @@ def list_cmd(
305
325
  api_key: str,
306
326
  anonymous: bool = False,
307
327
  ):
308
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
309
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
328
+ credential_kind = _resolve_credential_kind(api_key, anonymous)
329
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
310
330
  try:
311
331
  df = list_clobs(
312
332
  office=office,
@@ -319,7 +339,7 @@ def list_cmd(
319
339
  )
320
340
  except Exception:
321
341
  log_scoped_read_hint(
322
- api_key=resolved_api_key,
342
+ credential_kind=credential_kind,
323
343
  anonymous=anonymous,
324
344
  office=office,
325
345
  action="list",
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  import os
3
3
  import subprocess
4
- import sys
5
4
  import textwrap
6
5
  from pathlib import Path
7
6
  from typing import Optional
@@ -26,7 +25,9 @@ from cwmscli.utils.auth import DEFAULT_REDIRECT_HOST, DEFAULT_REDIRECT_PORT
26
25
  from cwmscli.utils.deps import requires
27
26
  from cwmscli.utils.update import (
28
27
  build_update_package_spec,
28
+ get_update_environment,
29
29
  launch_windows_update,
30
+ looks_like_externally_managed_environment,
30
31
  looks_like_missing_version,
31
32
  )
32
33
  from cwmscli.utils.version import get_cwms_cli_version
@@ -416,6 +417,7 @@ def csv2cwms_cmd(**kwargs):
416
417
  def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
417
418
  current_version = get_cwms_cli_version()
418
419
  package_spec = build_update_package_spec(target_version)
420
+ update_environment = get_update_environment()
419
421
 
420
422
  click.echo(
421
423
  "Current cwms-cli version: " f"{colors.c(current_version, 'cyan', bright=True)}"
@@ -428,12 +430,35 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
428
430
  else:
429
431
  click.echo("Requested cwms-cli version: latest available release")
430
432
 
431
- cmd = [sys.executable, "-m", "pip", "install", "--upgrade", package_spec]
433
+ click.echo("Update environment:")
434
+ click.echo(f" Python executable: {update_environment.python_executable}")
435
+ click.echo(
436
+ f" Environment: {update_environment.environment_prefix} "
437
+ f"({update_environment.environment_type})"
438
+ )
439
+ click.echo(f" Package metadata location: {update_environment.package_location}")
440
+ if update_environment.editable_project_location:
441
+ click.echo(
442
+ " Editable project location: "
443
+ f"{update_environment.editable_project_location}"
444
+ )
445
+
446
+ cmd = [
447
+ update_environment.python_executable,
448
+ "-m",
449
+ "pip",
450
+ "install",
451
+ "--upgrade",
452
+ package_spec,
453
+ ]
432
454
  if pre:
433
455
  cmd.append("--pre")
434
456
 
435
457
  if not yes:
436
- proceed = click.confirm("Proceed with updating cwms-cli via pip?", default=True)
458
+ proceed = click.confirm(
459
+ "Proceed with updating cwms-cli in this environment via pip?",
460
+ default=True,
461
+ )
437
462
  if not proceed:
438
463
  click.echo(colors.warn("Update canceled."))
439
464
  return
@@ -472,6 +497,16 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
472
497
 
473
498
  if result.returncode != 0:
474
499
  pip_output = "\n".join(part for part in [result.stdout, result.stderr] if part)
500
+ if looks_like_externally_managed_environment(pip_output):
501
+ raise click.ClickException(
502
+ colors.err(
503
+ "The selected Python installation is externally managed, so pip "
504
+ "refused to update cwms-cli. Install cwms-cli in a virtual "
505
+ "environment or with pipx, then run that installation's "
506
+ "cwms-cli update command. cwms-cli will not use "
507
+ "--break-system-packages automatically."
508
+ )
509
+ )
475
510
  if target_version and looks_like_missing_version(pip_output, package_spec):
476
511
  raise click.ClickException(
477
512
  colors.err(
@@ -492,13 +527,15 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
492
527
  @click.group(
493
528
  "blob",
494
529
  help="Manage CWMS Blobs (upload, download, delete, update, list)",
495
- epilog=textwrap.dedent("""
530
+ epilog=textwrap.dedent(
531
+ """
496
532
  Example Usage:\n
497
533
  - Store a PDF/image as a CWMS blob with optional description\n
498
534
  - Download a blob by id to your local filesystem\n
499
535
  - Update a blob's name/description/mime-type\n
500
536
  - Bulk list blobs for an office
501
- """),
537
+ """
538
+ ),
502
539
  )
503
540
  def blob_group():
504
541
  pass
@@ -671,10 +708,15 @@ def update_cmd(**kwargs):
671
708
  show_default=True,
672
709
  help="Sort descending instead of ascending.",
673
710
  )
674
- @click.option("--limit", type=int, default=None, help="Max rows to show.")
711
+ @click.option(
712
+ "--limit",
713
+ type=click.IntRange(min=1),
714
+ default=None,
715
+ help="Max rows to show.",
716
+ )
675
717
  @click.option(
676
718
  "--page-size",
677
- type=int,
719
+ type=click.IntRange(min=1),
678
720
  default=None,
679
721
  help="Max rows to request from the blob endpoint. Defaults to --limit if set, otherwise no pagination (all results in one page).",
680
722
  )
@@ -706,12 +748,14 @@ def list_cmd(**kwargs):
706
748
  @click.group(
707
749
  "clob",
708
750
  help="Manage CWMS Clobs (upload, download, delete, update, list)",
709
- epilog=textwrap.dedent("""
751
+ epilog=textwrap.dedent(
752
+ """
710
753
  Example Usage:\n
711
754
  - Download a clob by id to your local filesystem\n
712
755
  - Update a clob's name/description/mime-type\n
713
756
  - Bulk list clobs for an office
714
- """),
757
+ """
758
+ ),
715
759
  )
716
760
  @requires(reqs.cwms)
717
761
  def clob_group():
@@ -837,10 +881,15 @@ def update_cmd(**kwargs):
837
881
  show_default=True,
838
882
  help="Sort descending instead of ascending.",
839
883
  )
840
- @click.option("--limit", type=int, default=None, help="Max rows to show.")
884
+ @click.option(
885
+ "--limit",
886
+ type=click.IntRange(min=1),
887
+ default=None,
888
+ help="Max rows to show.",
889
+ )
841
890
  @click.option(
842
891
  "--page-size",
843
- type=int,
892
+ type=click.IntRange(min=1),
844
893
  default=None,
845
894
  help="Max rows to request from the clob endpoint. Defaults to --limit when set.",
846
895
  )