cwms-cli 0.3.8__tar.gz → 0.5.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 (73) hide show
  1. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/PKG-INFO +1 -1
  2. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/__main__.py +2 -0
  3. cwms_cli-0.5.0/cwmscli/_generated/__init__.py +1 -0
  4. cwms_cli-0.5.0/cwmscli/_generated/ownership_data.py +57 -0
  5. cwms_cli-0.5.0/cwmscli/callbacks/__init__.py +27 -0
  6. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/blob.py +22 -5
  7. cwms_cli-0.5.0/cwmscli/commands/clob.py +340 -0
  8. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/commands_cwms.py +328 -1
  9. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +3 -3
  10. cwms_cli-0.5.0/cwmscli/commands/users.py +420 -0
  11. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/location/location_ids.py +2 -2
  12. cwms_cli-0.5.0/cwmscli/load/timeseries/timeseries.py +150 -0
  13. cwms_cli-0.5.0/cwmscli/load/timeseries/timeseries_data.py +157 -0
  14. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/timeseries/timeseries_ids.py +1 -4
  15. cwms_cli-0.5.0/cwmscli/ownership.py +26 -0
  16. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/requirements.py +1 -1
  17. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_cda.py +4 -1
  18. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/__init__.py +24 -1
  19. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/click_help.py +37 -0
  20. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/colors.py +0 -2
  21. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/deps.py +1 -1
  22. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/pyproject.toml +6 -2
  23. cwms_cli-0.3.8/cwmscli/callbacks/__init__.py +0 -18
  24. cwms_cli-0.3.8/cwmscli/load/timeseries/timeseries.py +0 -59
  25. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/LICENSE +0 -0
  26. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/README.md +0 -0
  27. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/__init__.py +0 -0
  28. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
  29. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/README.md +0 -0
  30. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
  31. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
  32. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/config.py +0 -0
  33. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
  34. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
  35. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
  36. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
  37. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
  38. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
  39. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
  40. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
  41. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
  42. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
  43. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
  44. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
  45. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
  46. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
  47. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
  48. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
  49. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
  50. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
  51. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
  52. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/shef_critfile_import.py +0 -0
  53. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/README.md +0 -0
  54. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/__init__.py +0 -0
  55. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/__main__.py +0 -0
  56. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/location/location.py +0 -0
  57. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
  58. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/root.py +0 -0
  59. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/__init__.py +0 -0
  60. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/__main__.py +0 -0
  61. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
  62. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
  63. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
  64. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/friendly_errors.py +0 -0
  65. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/intervals.py +0 -0
  66. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/io.py +0 -0
  67. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/links.py +0 -0
  68. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/logging/__init__.py +0 -0
  69. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/logging/formatters.py +0 -0
  70. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/ssl_errors.py +0 -0
  71. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/update.py +0 -0
  72. {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/version.py +0 -0
  73. {cwms_cli-0.3.8 → cwms_cli-0.5.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.3.8
3
+ Version: 0.5.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
@@ -85,6 +85,8 @@ cli.add_command(commands_cwms.shefcritimport)
85
85
  cli.add_command(commands_cwms.csv2cwms_cmd)
86
86
  cli.add_command(commands_cwms.update_cli_cmd)
87
87
  cli.add_command(commands_cwms.blob_group)
88
+ cli.add_command(commands_cwms.clob_group)
89
+ cli.add_command(commands_cwms.users_group)
88
90
  cli.add_command(load.load_group)
89
91
  add_version_to_help_tree(cli)
90
92
 
@@ -0,0 +1 @@
1
+ # Generated files used at runtime and in docs.
@@ -0,0 +1,57 @@
1
+ # DO NOT EDIT THIS FILE MANUALLY.
2
+ # Generated from maintainers.toml by scripts/sync_ownership.py.
3
+
4
+ OWNERSHIP_DATA = {
5
+ "commands": {
6
+ "cwms-cli blob": [
7
+ {
8
+ "email": "charles.r.graham@usace.army.mil",
9
+ "name": "Charles Graham"
10
+ }
11
+ ],
12
+ "cwms-cli csv2cwms": [
13
+ {
14
+ "email": "charles.r.graham@usace.army.mil",
15
+ "name": "Charles Graham"
16
+ }
17
+ ],
18
+ "cwms-cli load": [
19
+ {
20
+ "email": "charles.r.graham@usace.army.mil",
21
+ "name": "Charles Graham"
22
+ },
23
+ {
24
+ "email": "eric.v.novotny@usace.army.mil",
25
+ "name": "Eric Novotny"
26
+ }
27
+ ],
28
+ "cwms-cli shefcritimport": [
29
+ {
30
+ "email": "eric.v.novotny@usace.army.mil",
31
+ "name": "Eric Novotny"
32
+ }
33
+ ],
34
+ "cwms-cli update": [
35
+ {
36
+ "email": "charles.r.graham@usace.army.mil",
37
+ "name": "Charles Graham"
38
+ }
39
+ ],
40
+ "cwms-cli usgs": [
41
+ {
42
+ "email": "eric.v.novotny@usace.army.mil",
43
+ "name": "Eric Novotny"
44
+ }
45
+ ]
46
+ },
47
+ "default": [
48
+ {
49
+ "email": "charles.r.graham@usace.army.mil",
50
+ "name": "Charles Graham"
51
+ },
52
+ {
53
+ "email": "eric.v.novotny@usace.army.mil",
54
+ "name": "Eric Novotny"
55
+ }
56
+ ]
57
+ }
@@ -0,0 +1,27 @@
1
+ # Click callbacks for click
2
+
3
+ import re
4
+
5
+
6
+ def csv_to_list(ctx, param, value):
7
+ """Accept multiple values either via repeated flags or a single delimiter-separated string.
8
+
9
+ Supported delimiters are comma (,) and pipe (|) to make CLI usage easier and avoid
10
+ shell pipe interpretation issues when users type role shortcuts.
11
+ """
12
+ if value is None:
13
+ return None
14
+ if isinstance(value, (list, tuple)):
15
+ out = []
16
+ for v in value:
17
+ if isinstance(v, str) and ("," in v or "|" in v):
18
+ for part in re.split(r"[,|]", v):
19
+ part = part.strip()
20
+ if part:
21
+ out.append(part)
22
+ else:
23
+ out.append(v)
24
+ return tuple(out)
25
+ if isinstance(value, str):
26
+ return tuple(p.strip() for p in re.split(r"[,|]", value) if p.strip())
27
+ return value
@@ -7,7 +7,7 @@ import os
7
7
  import re
8
8
  import sys
9
9
  from collections import defaultdict
10
- from typing import Optional, Sequence, Tuple
10
+ from typing import Optional, Sequence, Tuple, Union
11
11
 
12
12
  from cwmscli.utils import colors, get_api_key, log_scoped_read_hint
13
13
  from cwmscli.utils.click_help import DOCS_BASE_URL
@@ -64,12 +64,12 @@ def _looks_like_base64(raw: str) -> bool:
64
64
 
65
65
 
66
66
  def _save_blob_content(
67
- content: bytes | str,
67
+ content: Union[bytes, str],
68
68
  dest: str,
69
69
  media_type_hint: Optional[str] = None,
70
70
  ) -> str:
71
71
  media_type = media_type_hint
72
- data: bytes | str = content
72
+ data: Union[bytes, str] = content
73
73
 
74
74
  if isinstance(content, str):
75
75
  m = DATA_URL_RE.match(content.strip())
@@ -260,12 +260,19 @@ def list_blobs(
260
260
  sort_by: Optional[Sequence[str]] = None,
261
261
  ascending: bool = True,
262
262
  limit: Optional[int] = None,
263
+ page_size: Optional[int] = None,
263
264
  ):
264
265
  logging.info(f"Listing blobs for office: {office!r}...")
265
266
  import cwms
266
267
  import pandas as pd
267
268
 
268
- result = cwms.get_blobs(office_id=office, blob_id_like=blob_id_like)
269
+ # Use page size if it's provided per #184
270
+ fetch_page_size = page_size if page_size is not None else limit
271
+ result = cwms.get_blobs(
272
+ office_id=office,
273
+ blob_id_like=blob_id_like,
274
+ page_size=fetch_page_size,
275
+ )
269
276
 
270
277
  # Accept either a DataFrame or a JSON/dict-like response
271
278
  if isinstance(result, pd.DataFrame):
@@ -678,6 +685,7 @@ def list_cmd(
678
685
  sort_by: list[str],
679
686
  desc: bool,
680
687
  limit: int,
688
+ page_size: int,
681
689
  to_csv: str,
682
690
  office: str,
683
691
  api_root: str,
@@ -697,6 +705,7 @@ def list_cmd(
697
705
  sort_by=sort_by,
698
706
  ascending=not desc,
699
707
  limit=limit,
708
+ page_size=page_size,
700
709
  )
701
710
  except Exception:
702
711
  log_scoped_read_hint(
@@ -713,4 +722,12 @@ def list_cmd(
713
722
  else:
714
723
  # Friendly console preview
715
724
  with pd.option_context("display.max_rows", 500, "display.max_columns", None):
716
- logging.info(df.to_string(index=False))
725
+ # Left-align all columns
726
+ logging.info(
727
+ "\n"
728
+ + df.apply(
729
+ lambda s: (s := s.astype(str).str.strip()).str.ljust(
730
+ s.str.len().max()
731
+ )
732
+ ).to_string(index=False, justify="left")
733
+ )
@@ -0,0 +1,340 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ from typing import Optional, Sequence
6
+
7
+ import cwms
8
+ import pandas as pd
9
+ import requests
10
+ from cwms import api as cwms_api
11
+
12
+ from cwmscli.utils import get_api_key, has_invalid_chars, log_scoped_read_hint
13
+
14
+
15
+ def _join_api_url(api_root: str, path: str) -> str:
16
+ return f"{api_root.rstrip('/')}/{path.lstrip('/')}"
17
+
18
+
19
+ def _resolve_optional_api_key(api_key: Optional[str], anonymous: bool) -> Optional[str]:
20
+ if anonymous or not api_key:
21
+ return None
22
+ return get_api_key(api_key, None)
23
+
24
+
25
+ def _write_clob_content(content: str, dest: str) -> str:
26
+ os.makedirs(os.path.dirname(dest) or ".", exist_ok=True)
27
+ with open(dest, "w", encoding="utf-8", newline="") as f:
28
+ f.write(content)
29
+ return dest
30
+
31
+
32
+ def _clob_endpoint_id(clob_id: str) -> tuple[str, Optional[str]]:
33
+ normalized = clob_id.upper()
34
+ if has_invalid_chars(normalized):
35
+ return "ignored", normalized
36
+ return normalized, None
37
+
38
+
39
+ def _get_special_clob_text(*, office: str, clob_id: str) -> str:
40
+ with cwms_api.SESSION.get(
41
+ "clobs/ignored",
42
+ params={"office": office, "clob-id": clob_id},
43
+ headers={"Accept": "text/plain"},
44
+ ) as response:
45
+ response.raise_for_status()
46
+ return response.text
47
+
48
+
49
+ def list_clobs(
50
+ office: Optional[str] = None,
51
+ clob_id_like: Optional[str] = None,
52
+ columns: Optional[Sequence[str]] = None,
53
+ sort_by: Optional[Sequence[str]] = None,
54
+ ascending: bool = True,
55
+ limit: Optional[int] = None,
56
+ page_size: Optional[int] = None,
57
+ ) -> pd.DataFrame:
58
+ logging.info(f"Listing clobs for office: {office!r}...")
59
+ fetch_page_size = page_size if page_size is not None else limit
60
+ result = cwms.get_clobs(
61
+ office_id=office,
62
+ clob_id_like=clob_id_like,
63
+ page_size=fetch_page_size,
64
+ )
65
+
66
+ # Accept either a DataFrame or a JSON/dict-like response
67
+ if isinstance(result, pd.DataFrame):
68
+ df = result.copy()
69
+ else:
70
+ # Expecting normal clob return structure
71
+ data = getattr(result, "json", None)
72
+ if callable(data):
73
+ data = result.json()
74
+ df = pd.DataFrame((data or {}).get("clobs", []))
75
+
76
+ # Allow column filtering
77
+ if columns:
78
+ keep = [c for c in columns if c in df.columns]
79
+ if keep:
80
+ df = df[keep]
81
+
82
+ # Sort by option
83
+ if sort_by:
84
+ by = [c for c in sort_by if c in df.columns]
85
+ if by:
86
+ df = df.sort_values(by=by, ascending=ascending, kind="stable")
87
+
88
+ # Optional limit
89
+ if limit is not None:
90
+ df = df.head(limit)
91
+
92
+ logging.info(f"Found {len(df):,} clob(s)")
93
+ # List the clobs in the logger
94
+ for _, row in df.iterrows():
95
+ logging.info(f"clob ID: {row['id']}, Description: {row.get('description')}")
96
+ return df
97
+
98
+
99
+ def upload_cmd(
100
+ input_file: str,
101
+ clob_id: str,
102
+ description: str,
103
+ overwrite: bool,
104
+ dry_run: bool,
105
+ office: str,
106
+ api_root: str,
107
+ api_key: str,
108
+ ):
109
+ cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
110
+ try:
111
+ file_size = os.path.getsize(input_file)
112
+ with open(input_file, "r", encoding="utf-8") as f:
113
+ file_data = f.read()
114
+ logging.info(f"Read file: {input_file} ({file_size} bytes)")
115
+ except Exception as e:
116
+ logging.error(f"Failed to read file: {e}")
117
+ sys.exit(1)
118
+
119
+ clob_id_up = clob_id.upper()
120
+ logging.debug(f"Office={office} clobID={clob_id_up}")
121
+
122
+ clob = {
123
+ "office-id": office,
124
+ "id": clob_id_up,
125
+ "description": (
126
+ json.dumps(description)
127
+ if isinstance(description, (dict, list))
128
+ else description
129
+ ),
130
+ "value": file_data,
131
+ }
132
+ params = {"fail-if-exists": not overwrite}
133
+ view_url = _join_api_url(api_root, f"clobs/{clob_id_up}?office={office}")
134
+
135
+ if dry_run:
136
+ logging.info(
137
+ f"DRY RUN: would POST {_join_api_url(api_root, 'clobs')} with params={params}"
138
+ )
139
+ logging.info(
140
+ json.dumps(
141
+ {
142
+ "url": _join_api_url(api_root, "clobs"),
143
+ "params": params,
144
+ "clob": {**clob, "value": f'<{len(clob["value"])} chars>'},
145
+ },
146
+ indent=2,
147
+ )
148
+ )
149
+ return
150
+
151
+ try:
152
+ cwms.store_clobs(clob, fail_if_exists=not overwrite)
153
+ logging.info(f"Uploaded clob: {clob_id_up}")
154
+ if has_invalid_chars(clob_id_up):
155
+ logging.info(
156
+ f"View: {_join_api_url(api_root, f'clobs/ignored?clob-id={clob_id_up}&office={office}')}"
157
+ )
158
+ else:
159
+ logging.info(f"View: {view_url}")
160
+ except requests.HTTPError as e:
161
+ detail = getattr(e.response, "text", "") or str(e)
162
+ logging.error(f"Failed to upload (HTTP): {detail}")
163
+ sys.exit(1)
164
+ except Exception as e:
165
+ logging.error(f"Failed to upload: {e}")
166
+ sys.exit(1)
167
+
168
+
169
+ def download_cmd(
170
+ clob_id: str,
171
+ dest: str,
172
+ office: str,
173
+ api_root: str,
174
+ api_key: str,
175
+ dry_run: bool,
176
+ anonymous: bool = False,
177
+ ):
178
+ if dry_run:
179
+ logging.info(
180
+ f"DRY RUN: would GET {api_root} clob with clob-id={clob_id} office={office}."
181
+ )
182
+ return
183
+ resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
184
+ cwms.init_session(api_root=api_root, api_key=resolved_api_key)
185
+ bid = clob_id.upper()
186
+ logging.debug(f"Office={office} clobID={bid}")
187
+
188
+ try:
189
+ path_id, query_id = _clob_endpoint_id(bid)
190
+ if query_id is None:
191
+ clob = cwms.get_clob(office_id=office, clob_id=path_id)
192
+ payload = getattr(clob, "json", clob)
193
+ if callable(payload):
194
+ payload = payload()
195
+ if isinstance(payload, dict):
196
+ content = payload.get("value", "")
197
+ else:
198
+ content = str(payload)
199
+ else:
200
+ content = _get_special_clob_text(office=office, clob_id=query_id)
201
+ target = dest or bid
202
+ _write_clob_content(content, target)
203
+ logging.info(f"Downloaded clob to: {target}")
204
+ except requests.HTTPError as e:
205
+ detail = getattr(e.response, "text", "") or str(e)
206
+ logging.error(f"Failed to download (HTTP): {detail}")
207
+ log_scoped_read_hint(
208
+ api_key=resolved_api_key,
209
+ anonymous=anonymous,
210
+ office=office,
211
+ action="download",
212
+ resource="clob content",
213
+ )
214
+ sys.exit(1)
215
+ except Exception as e:
216
+ logging.error(f"Failed to download: {e}")
217
+ log_scoped_read_hint(
218
+ api_key=resolved_api_key,
219
+ anonymous=anonymous,
220
+ office=office,
221
+ action="download",
222
+ resource="clob content",
223
+ )
224
+ sys.exit(1)
225
+
226
+
227
+ def delete_cmd(clob_id: str, office: str, api_root: str, api_key: str, dry_run: bool):
228
+
229
+ if dry_run:
230
+ logging.info(
231
+ f"DRY RUN: would DELETE {api_root} clob with clob-id={clob_id} office={office}"
232
+ )
233
+ return
234
+ cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
235
+ cid = clob_id.upper()
236
+ path_id, query_id = _clob_endpoint_id(cid)
237
+ if query_id is None:
238
+ cwms.delete_clob(office_id=office, clob_id=cid)
239
+ else:
240
+ cwms_api.delete(
241
+ f"clobs/{path_id}", params={"office": office, "clob-id": query_id}
242
+ )
243
+ logging.info(f"Deleted clob: {clob_id} for office: {office}")
244
+
245
+
246
+ def update_cmd(
247
+ input_file: str,
248
+ clob_id: str,
249
+ description: str,
250
+ ignore_nulls: bool,
251
+ dry_run: bool,
252
+ office: str,
253
+ api_root: str,
254
+ api_key: str,
255
+ ):
256
+ if dry_run:
257
+ logging.info(
258
+ f"DRY RUN: would PATCH {api_root} clob with clob-id={clob_id} office={office}"
259
+ )
260
+ return
261
+ file_data = None
262
+ if input_file:
263
+ try:
264
+ file_size = os.path.getsize(input_file)
265
+ with open(input_file, "r", encoding="utf-8") as f:
266
+ file_data = f.read()
267
+ logging.info(f"Read file: {input_file} ({file_size} bytes)")
268
+ except Exception as e:
269
+ logging.error(f"Failed to read file: {e}")
270
+ sys.exit(1)
271
+ # Setup minimum required payload
272
+ clob = {"office-id": office, "id": clob_id.upper()}
273
+ if description:
274
+ clob["description"] = description
275
+
276
+ if file_data:
277
+ clob["value"] = file_data
278
+ cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
279
+ cid = clob_id.upper()
280
+ path_id, query_id = _clob_endpoint_id(cid)
281
+ if query_id is None:
282
+ cwms.update_clob(clob, cid, ignore_nulls=ignore_nulls)
283
+ else:
284
+ cwms_api.patch(
285
+ f"clobs/{path_id}",
286
+ data=clob,
287
+ params={"clob-id": query_id, "ignore-nulls": ignore_nulls},
288
+ )
289
+ logging.info(f"Updated clob: {clob_id} for office: {office}")
290
+
291
+
292
+ def list_cmd(
293
+ clob_id_like: str,
294
+ columns: list[str],
295
+ sort_by: list[str],
296
+ desc: bool,
297
+ limit: int,
298
+ page_size: int,
299
+ to_csv: str,
300
+ office: str,
301
+ api_root: str,
302
+ api_key: str,
303
+ anonymous: bool = False,
304
+ ):
305
+ resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
306
+ cwms.init_session(api_root=api_root, api_key=resolved_api_key)
307
+ try:
308
+ df = list_clobs(
309
+ office=office,
310
+ clob_id_like=clob_id_like,
311
+ columns=columns,
312
+ sort_by=sort_by,
313
+ ascending=not desc,
314
+ limit=limit,
315
+ page_size=page_size,
316
+ )
317
+ except Exception:
318
+ log_scoped_read_hint(
319
+ api_key=resolved_api_key,
320
+ anonymous=anonymous,
321
+ office=office,
322
+ action="list",
323
+ resource="clob content",
324
+ )
325
+ raise
326
+ if to_csv:
327
+ df.to_csv(to_csv, index=False)
328
+ logging.info(f"Wrote {len(df)} rows to {to_csv}")
329
+ else:
330
+ # Friendly console preview
331
+ with pd.option_context("display.max_rows", 500, "display.max_columns", None):
332
+ # Left-align all columns
333
+ logging.info(
334
+ "\n"
335
+ + df.apply(
336
+ lambda s: (s := s.astype(str).str.strip()).str.ljust(
337
+ s.str.len().max()
338
+ )
339
+ ).to_string(index=False, justify="left")
340
+ )