cwms-cli 0.4.0__tar.gz → 0.6.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 (78) hide show
  1. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/PKG-INFO +2 -1
  2. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/__main__.py +3 -1
  3. cwms_cli-0.6.0/cwmscli/_generated/__init__.py +1 -0
  4. cwms_cli-0.6.0/cwmscli/_generated/ownership_data.py +57 -0
  5. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/blob.py +70 -20
  6. cwms_cli-0.6.0/cwmscli/commands/clob.py +340 -0
  7. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/commands_cwms.py +456 -3
  8. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/__main__.py +4 -2
  9. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +3 -3
  10. cwms_cli-0.4.0/cwmscli/commands/shef_critfile_import.py → cwms_cli-0.6.0/cwmscli/commands/shef/import_critfile.py +27 -3
  11. cwms_cli-0.6.0/cwmscli/commands/shef/import_infile.py +714 -0
  12. cwms_cli-0.6.0/cwmscli/commands/shef/shef_parameters.csv +272 -0
  13. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/users.py +2 -3
  14. cwms_cli-0.6.0/cwmscli/load/__init__.py +0 -0
  15. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location_ids.py +5 -4
  16. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location_ids_bygroup.py +4 -2
  17. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/root.py +1 -1
  18. cwms_cli-0.6.0/cwmscli/load/timeseries/timeseries.py +150 -0
  19. cwms_cli-0.6.0/cwmscli/load/timeseries/timeseries_data.py +157 -0
  20. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/timeseries/timeseries_ids.py +4 -5
  21. cwms_cli-0.6.0/cwmscli/ownership.py +26 -0
  22. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/requirements.py +2 -3
  23. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/__init__.py +10 -2
  24. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getUSGS_ratings_cda.py +3 -2
  25. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getusgs_cda.py +6 -4
  26. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getusgs_measurements_cda.py +3 -2
  27. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/rating_ini_file_import.py +40 -17
  28. cwms_cli-0.6.0/cwmscli/utils/__init__.py +243 -0
  29. cwms_cli-0.6.0/cwmscli/utils/auth.py +693 -0
  30. cwms_cli-0.6.0/cwmscli/utils/callback_success.html +160 -0
  31. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/click_help.py +55 -0
  32. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/deps.py +1 -1
  33. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/pyproject.toml +17 -1
  34. cwms_cli-0.4.0/cwmscli/load/timeseries/timeseries.py +0 -59
  35. cwms_cli-0.4.0/cwmscli/utils/__init__.py +0 -139
  36. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/LICENSE +0 -0
  37. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/README.md +0 -0
  38. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/__init__.py +0 -0
  39. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/callbacks/__init__.py +0 -0
  40. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
  41. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/README.md +0 -0
  42. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
  43. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/config.py +0 -0
  44. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
  45. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
  46. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
  47. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
  48. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
  49. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
  50. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
  51. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
  52. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
  53. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
  54. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
  55. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
  56. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
  57. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
  58. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
  59. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
  60. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
  61. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
  62. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
  63. {cwms_cli-0.4.0/cwmscli/load → cwms_cli-0.6.0/cwmscli/commands/shef}/__init__.py +0 -0
  64. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/README.md +0 -0
  65. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/__main__.py +0 -0
  66. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location.py +0 -0
  67. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/__main__.py +0 -0
  68. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/colors.py +0 -0
  69. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/friendly_errors.py +0 -0
  70. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/intervals.py +0 -0
  71. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/io.py +0 -0
  72. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/links.py +0 -0
  73. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/logging/__init__.py +0 -0
  74. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/logging/formatters.py +0 -0
  75. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/ssl_errors.py +0 -0
  76. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/update.py +0 -0
  77. {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/version.py +0 -0
  78. {cwms_cli-0.4.0 → cwms_cli-0.6.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.4.0
3
+ Version: 0.6.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
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Requires-Dist: click (>=8.1.8,<9.0.0)
20
20
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
21
+ Requires-Dist: requests (>=2.30.0,<3.0.0)
21
22
  Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-cli
22
23
  Description-Content-Type: text/markdown
23
24
 
@@ -81,10 +81,12 @@ def cli(
81
81
 
82
82
 
83
83
  cli.add_command(usgs_group, name="usgs")
84
- cli.add_command(commands_cwms.shefcritimport)
84
+ cli.add_command(commands_cwms.shef_group)
85
85
  cli.add_command(commands_cwms.csv2cwms_cmd)
86
+ cli.add_command(commands_cwms.login_cmd)
86
87
  cli.add_command(commands_cwms.update_cli_cmd)
87
88
  cli.add_command(commands_cwms.blob_group)
89
+ cli.add_command(commands_cwms.clob_group)
88
90
  cli.add_command(commands_cwms.users_group)
89
91
  cli.add_command(load.load_group)
90
92
  add_version_to_help_tree(cli)
@@ -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
+ }
@@ -7,9 +7,15 @@ import os
7
7
  import re
8
8
  import sys
9
9
  from collections import defaultdict
10
- from typing import Optional, Sequence, Tuple
11
-
12
- from cwmscli.utils import colors, get_api_key, log_scoped_read_hint
10
+ from typing import Optional, Sequence, Tuple, Union
11
+
12
+ from cwmscli.utils import (
13
+ colors,
14
+ get_api_key,
15
+ has_invalid_chars,
16
+ init_cwms_session,
17
+ log_scoped_read_hint,
18
+ )
13
19
  from cwmscli.utils.click_help import DOCS_BASE_URL
14
20
  from cwmscli.utils.deps import requires
15
21
 
@@ -64,12 +70,12 @@ def _looks_like_base64(raw: str) -> bool:
64
70
 
65
71
 
66
72
  def _save_blob_content(
67
- content: bytes | str,
73
+ content: Union[bytes, str],
68
74
  dest: str,
69
75
  media_type_hint: Optional[str] = None,
70
76
  ) -> str:
71
77
  media_type = media_type_hint
72
- data: bytes | str = content
78
+ data: Union[bytes, str] = content
73
79
 
74
80
  if isinstance(content, str):
75
81
  m = DATA_URL_RE.match(content.strip())
@@ -133,11 +139,30 @@ def _resolve_optional_api_key(api_key: Optional[str], anonymous: bool) -> Option
133
139
  return get_api_key(api_key, None)
134
140
 
135
141
 
142
+ def _resolve_credential_kind(api_key: Optional[str], anonymous: bool) -> Optional[str]:
143
+ if anonymous:
144
+ return None
145
+ from cwmscli.utils import get_saved_login_token
146
+
147
+ if get_saved_login_token():
148
+ return "token"
149
+ if _resolve_optional_api_key(api_key, anonymous):
150
+ return "api_key"
151
+ return None
152
+
153
+
136
154
  def _response_status_code(exc: BaseException) -> Optional[int]:
137
155
  response = getattr(exc, "response", None)
138
156
  return getattr(response, "status_code", None)
139
157
 
140
158
 
159
+ def _blob_endpoint_id(blob_id: str) -> tuple[str, Optional[str]]:
160
+ normalized = blob_id.upper()
161
+ if has_invalid_chars(normalized):
162
+ return "ignored", normalized
163
+ return normalized, None
164
+
165
+
141
166
  def store_blob(**kwargs):
142
167
  import cwms
143
168
  import requests
@@ -260,12 +285,19 @@ def list_blobs(
260
285
  sort_by: Optional[Sequence[str]] = None,
261
286
  ascending: bool = True,
262
287
  limit: Optional[int] = None,
288
+ page_size: Optional[int] = None,
263
289
  ):
264
290
  logging.info(f"Listing blobs for office: {office!r}...")
265
291
  import cwms
266
292
  import pandas as pd
267
293
 
268
- result = cwms.get_blobs(office_id=office, blob_id_like=blob_id_like)
294
+ # Use page size if it's provided per #184
295
+ fetch_page_size = page_size if page_size is not None else limit
296
+ result = cwms.get_blobs(
297
+ office_id=office,
298
+ blob_id_like=blob_id_like,
299
+ page_size=fetch_page_size,
300
+ )
269
301
 
270
302
  # Accept either a DataFrame or a JSON/dict-like response
271
303
  if isinstance(result, pd.DataFrame):
@@ -404,7 +436,7 @@ def upload_cmd(
404
436
  import cwms
405
437
  import requests
406
438
 
407
- cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
439
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key)
408
440
 
409
441
  using_single = bool(input_file)
410
442
  using_multi = bool(input_dir)
@@ -563,8 +595,8 @@ def download_cmd(
563
595
  f"DRY RUN: would GET {api_root} blob with blob-id={blob_id} office={office}."
564
596
  )
565
597
  return
566
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
567
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
598
+ credential_kind = _resolve_credential_kind(api_key, anonymous)
599
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
568
600
  bid = blob_id.upper()
569
601
  logging.debug(f"Office={office} BlobID={bid}")
570
602
 
@@ -581,7 +613,7 @@ def download_cmd(
581
613
  detail = getattr(e.response, "text", "") or str(e)
582
614
  logging.error(f"Failed to download (HTTP): {detail}")
583
615
  log_scoped_read_hint(
584
- api_key=resolved_api_key,
616
+ credential_kind=credential_kind,
585
617
  anonymous=anonymous,
586
618
  office=office,
587
619
  action="download",
@@ -591,7 +623,7 @@ def download_cmd(
591
623
  except Exception as e:
592
624
  logging.error(f"Failed to download: {e}")
593
625
  log_scoped_read_hint(
594
- api_key=resolved_api_key,
626
+ credential_kind=credential_kind,
595
627
  anonymous=anonymous,
596
628
  office=office,
597
629
  action="download",
@@ -609,19 +641,27 @@ def delete_cmd(blob_id: str, office: str, api_root: str, api_key: str, dry_run:
609
641
  f"DRY RUN: would DELETE {api_root} blob with blob-id={blob_id} office={office}"
610
642
  )
611
643
  return
612
- cwms.init_session(api_root=api_root, api_key=api_key)
644
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key)
645
+ bid = blob_id.upper()
646
+ path_id, query_id = _blob_endpoint_id(bid)
613
647
  try:
614
- cwms.delete_blob(office_id=office, blob_id=blob_id)
648
+ if query_id is None:
649
+ cwms.delete_blob(office_id=office, blob_id=bid)
650
+ else:
651
+ cwms.api.delete(
652
+ f"blobs/{path_id}",
653
+ params={"office": office, "blob-id": query_id},
654
+ )
615
655
  except requests.HTTPError as e:
616
656
  if _response_status_code(e) == 404:
617
657
  logging.info(
618
658
  "Blob %s was already absent in office %s. Nothing to delete.",
619
- blob_id,
659
+ bid,
620
660
  office,
621
661
  )
622
662
  return
623
663
  raise
624
- logging.info(f"Deleted blob: {blob_id} for office: {office}")
664
+ logging.info(f"Deleted blob: {bid} for office: {office}")
625
665
 
626
666
 
627
667
  def update_cmd(
@@ -642,7 +682,7 @@ def update_cmd(
642
682
  f"DRY RUN: would PATCH {api_root} blob with blob-id={blob_id} office={office}"
643
683
  )
644
684
  return
645
- cwms.init_session(api_root=api_root, api_key=api_key)
685
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key)
646
686
  file_data = None
647
687
  if input_file:
648
688
  try:
@@ -678,6 +718,7 @@ def list_cmd(
678
718
  sort_by: list[str],
679
719
  desc: bool,
680
720
  limit: int,
721
+ page_size: int,
681
722
  to_csv: str,
682
723
  office: str,
683
724
  api_root: str,
@@ -687,8 +728,8 @@ def list_cmd(
687
728
  import cwms
688
729
  import pandas as pd
689
730
 
690
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
691
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
731
+ credential_kind = _resolve_credential_kind(api_key, anonymous)
732
+ init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
692
733
  try:
693
734
  df = list_blobs(
694
735
  office=office,
@@ -697,10 +738,11 @@ def list_cmd(
697
738
  sort_by=sort_by,
698
739
  ascending=not desc,
699
740
  limit=limit,
741
+ page_size=page_size,
700
742
  )
701
743
  except Exception:
702
744
  log_scoped_read_hint(
703
- api_key=resolved_api_key,
745
+ credential_kind=credential_kind,
704
746
  anonymous=anonymous,
705
747
  office=office,
706
748
  action="list",
@@ -713,4 +755,12 @@ def list_cmd(
713
755
  else:
714
756
  # Friendly console preview
715
757
  with pd.option_context("display.max_rows", 500, "display.max_columns", None):
716
- logging.info(df.to_string(index=False))
758
+ # Left-align all columns
759
+ logging.info(
760
+ "\n"
761
+ + df.apply(
762
+ lambda s: (s := s.astype(str).str.strip()).str.ljust(
763
+ s.str.len().max()
764
+ )
765
+ ).to_string(index=False, justify="left")
766
+ )
@@ -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
+ )