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.
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/PKG-INFO +2 -1
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/__main__.py +3 -1
- cwms_cli-0.6.0/cwmscli/_generated/__init__.py +1 -0
- cwms_cli-0.6.0/cwmscli/_generated/ownership_data.py +57 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/blob.py +70 -20
- cwms_cli-0.6.0/cwmscli/commands/clob.py +340 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/commands_cwms.py +456 -3
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/__main__.py +4 -2
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +3 -3
- cwms_cli-0.4.0/cwmscli/commands/shef_critfile_import.py → cwms_cli-0.6.0/cwmscli/commands/shef/import_critfile.py +27 -3
- cwms_cli-0.6.0/cwmscli/commands/shef/import_infile.py +714 -0
- cwms_cli-0.6.0/cwmscli/commands/shef/shef_parameters.csv +272 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/users.py +2 -3
- cwms_cli-0.6.0/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location_ids.py +5 -4
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location_ids_bygroup.py +4 -2
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/root.py +1 -1
- cwms_cli-0.6.0/cwmscli/load/timeseries/timeseries.py +150 -0
- cwms_cli-0.6.0/cwmscli/load/timeseries/timeseries_data.py +157 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/timeseries/timeseries_ids.py +4 -5
- cwms_cli-0.6.0/cwmscli/ownership.py +26 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/requirements.py +2 -3
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/__init__.py +10 -2
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getUSGS_ratings_cda.py +3 -2
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getusgs_cda.py +6 -4
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/getusgs_measurements_cda.py +3 -2
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/rating_ini_file_import.py +40 -17
- cwms_cli-0.6.0/cwmscli/utils/__init__.py +243 -0
- cwms_cli-0.6.0/cwmscli/utils/auth.py +693 -0
- cwms_cli-0.6.0/cwmscli/utils/callback_success.html +160 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/click_help.py +55 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/deps.py +1 -1
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/pyproject.toml +17 -1
- cwms_cli-0.4.0/cwmscli/load/timeseries/timeseries.py +0 -59
- cwms_cli-0.4.0/cwmscli/utils/__init__.py +0 -139
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/LICENSE +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.4.0/cwmscli/load → cwms_cli-0.6.0/cwmscli/commands/shef}/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.6.0}/cwmscli/utils/version.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
567
|
-
cwms
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
659
|
+
bid,
|
|
620
660
|
office,
|
|
621
661
|
)
|
|
622
662
|
return
|
|
623
663
|
raise
|
|
624
|
-
logging.info(f"Deleted blob: {
|
|
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
|
|
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
|
-
|
|
691
|
-
cwms
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|