cwms-cli 0.4.0__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.
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/PKG-INFO +1 -1
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/__main__.py +1 -0
- cwms_cli-0.5.0/cwmscli/_generated/__init__.py +1 -0
- cwms_cli-0.5.0/cwmscli/_generated/ownership_data.py +57 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/blob.py +22 -5
- cwms_cli-0.5.0/cwmscli/commands/clob.py +340 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/commands_cwms.py +174 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +3 -3
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/location/location_ids.py +2 -2
- cwms_cli-0.5.0/cwmscli/load/timeseries/timeseries.py +150 -0
- cwms_cli-0.5.0/cwmscli/load/timeseries/timeseries_data.py +157 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/timeseries/timeseries_ids.py +1 -4
- cwms_cli-0.5.0/cwmscli/ownership.py +26 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_cda.py +4 -1
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/__init__.py +12 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/click_help.py +34 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/deps.py +1 -1
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/pyproject.toml +1 -1
- cwms_cli-0.4.0/cwmscli/load/timeseries/timeseries.py +0 -59
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/LICENSE +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/commands/users.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.4.0 → cwms_cli-0.5.0}/cwmscli/utils/version.py +0 -0
- {cwms_cli-0.4.0 → 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
|
+
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,7 @@ 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)
|
|
88
89
|
cli.add_command(commands_cwms.users_group)
|
|
89
90
|
cli.add_command(load.load_group)
|
|
90
91
|
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,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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
@@ -387,6 +387,12 @@ def update_cmd(**kwargs):
|
|
|
387
387
|
help="Sort descending instead of ascending.",
|
|
388
388
|
)
|
|
389
389
|
@click.option("--limit", type=int, default=None, help="Max rows to show.")
|
|
390
|
+
@click.option(
|
|
391
|
+
"--page-size",
|
|
392
|
+
type=int,
|
|
393
|
+
default=None,
|
|
394
|
+
help="Max rows to request from the blob endpoint. Defaults to --limit if set, otherwise no pagination (all results in one page).",
|
|
395
|
+
)
|
|
390
396
|
@click.option(
|
|
391
397
|
"--to-csv",
|
|
392
398
|
type=click.Path(dir_okay=False, writable=True, path_type=str),
|
|
@@ -405,6 +411,174 @@ def list_cmd(**kwargs):
|
|
|
405
411
|
list_cmd(**kwargs)
|
|
406
412
|
|
|
407
413
|
|
|
414
|
+
# endregion
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# region Clob
|
|
418
|
+
# ================================================================================
|
|
419
|
+
# CLOB
|
|
420
|
+
# ================================================================================
|
|
421
|
+
@click.group(
|
|
422
|
+
"clob",
|
|
423
|
+
help="Manage CWMS Clobs (upload, download, delete, update, list)",
|
|
424
|
+
epilog=textwrap.dedent(
|
|
425
|
+
"""
|
|
426
|
+
Example Usage:\n
|
|
427
|
+
- Download a clob by id to your local filesystem\n
|
|
428
|
+
- Update a clob's name/description/mime-type\n
|
|
429
|
+
- Bulk list clobs for an office
|
|
430
|
+
"""
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
@requires(reqs.cwms)
|
|
434
|
+
def clob_group():
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ================================================================================
|
|
439
|
+
# Upload
|
|
440
|
+
# ================================================================================
|
|
441
|
+
@clob_group.command("upload", help="Upload a file as a clob")
|
|
442
|
+
@click.option(
|
|
443
|
+
"--input-file",
|
|
444
|
+
required=True,
|
|
445
|
+
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str),
|
|
446
|
+
help="Path to the file to upload.",
|
|
447
|
+
)
|
|
448
|
+
@click.option("--clob-id", required=True, type=str, help="Clob ID to create.")
|
|
449
|
+
@click.option("--description", default=None, help="Optional description JSON or text.")
|
|
450
|
+
@click.option(
|
|
451
|
+
"--overwrite/--no-overwrite",
|
|
452
|
+
default=False,
|
|
453
|
+
show_default=True,
|
|
454
|
+
help="If true, replace existing clob.",
|
|
455
|
+
)
|
|
456
|
+
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
457
|
+
@common_api_options
|
|
458
|
+
def clob_upload(**kwargs):
|
|
459
|
+
from cwmscli.commands.clob import upload_cmd
|
|
460
|
+
|
|
461
|
+
upload_cmd(**kwargs)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ================================================================================
|
|
465
|
+
# Download
|
|
466
|
+
# ================================================================================
|
|
467
|
+
@clob_group.command("download", help="Download a clob by ID")
|
|
468
|
+
# TODO: test XML
|
|
469
|
+
@click.option("--clob-id", required=True, type=str, help="Clob ID to download.")
|
|
470
|
+
@click.option(
|
|
471
|
+
"--dest",
|
|
472
|
+
default=None,
|
|
473
|
+
help="Destination file path. Defaults to clob-id.",
|
|
474
|
+
)
|
|
475
|
+
@click.option(
|
|
476
|
+
"--anonymous",
|
|
477
|
+
is_flag=True,
|
|
478
|
+
help="Do not send credentials for this read request, even if they are configured.",
|
|
479
|
+
)
|
|
480
|
+
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
481
|
+
@common_api_options
|
|
482
|
+
def clob_download(**kwargs):
|
|
483
|
+
from cwmscli.commands.clob import download_cmd
|
|
484
|
+
|
|
485
|
+
download_cmd(**kwargs)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ================================================================================
|
|
489
|
+
# Delete
|
|
490
|
+
# ================================================================================
|
|
491
|
+
@clob_group.command("delete", help="Delete a clob by ID")
|
|
492
|
+
@click.option("--clob-id", required=True, type=str, help="Clob ID to delete.")
|
|
493
|
+
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
494
|
+
@common_api_options
|
|
495
|
+
def delete_cmd(**kwargs):
|
|
496
|
+
from cwmscli.commands.clob import delete_cmd
|
|
497
|
+
|
|
498
|
+
delete_cmd(**kwargs)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ================================================================================
|
|
502
|
+
# Update
|
|
503
|
+
# ================================================================================
|
|
504
|
+
@clob_group.command("update", help="Update/patch a clob by ID")
|
|
505
|
+
@click.option("--clob-id", required=True, type=str, help="Clob ID to update.")
|
|
506
|
+
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
507
|
+
@click.option(
|
|
508
|
+
"--description",
|
|
509
|
+
default=None,
|
|
510
|
+
help="New description JSON or text.",
|
|
511
|
+
)
|
|
512
|
+
@click.option(
|
|
513
|
+
"--input-file",
|
|
514
|
+
required=False,
|
|
515
|
+
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str),
|
|
516
|
+
help="Optional file content to upload with update.",
|
|
517
|
+
)
|
|
518
|
+
@click.option(
|
|
519
|
+
"--ignore-nulls/--no-ignore-nulls",
|
|
520
|
+
default=True,
|
|
521
|
+
show_default=True,
|
|
522
|
+
help="If true, null and empty fields in the provided clob will be ignored and the existing value of those fields left in place.",
|
|
523
|
+
)
|
|
524
|
+
@common_api_options
|
|
525
|
+
def update_cmd(**kwargs):
|
|
526
|
+
from cwmscli.commands.clob import update_cmd
|
|
527
|
+
|
|
528
|
+
update_cmd(**kwargs)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ================================================================================
|
|
532
|
+
# List
|
|
533
|
+
# ================================================================================
|
|
534
|
+
@clob_group.command("list", help="List clobs with optional filters and sorting")
|
|
535
|
+
# TODO: Add link to regex docs when new CWMS-DATA site is deployed to PROD
|
|
536
|
+
@click.option(
|
|
537
|
+
"--clob-id-like", help="LIKE filter for clob ID (e.g., ``*PNG``)."
|
|
538
|
+
) # Escape the wildcard/asterisk for RTD generation with double backticks
|
|
539
|
+
@click.option(
|
|
540
|
+
"--columns",
|
|
541
|
+
multiple=True,
|
|
542
|
+
callback=csv_to_list,
|
|
543
|
+
help="Columns to show (repeat or comma-separate).",
|
|
544
|
+
)
|
|
545
|
+
@click.option(
|
|
546
|
+
"--sort-by",
|
|
547
|
+
multiple=True,
|
|
548
|
+
callback=csv_to_list,
|
|
549
|
+
help="Columns to sort by (repeat or comma-separate).",
|
|
550
|
+
)
|
|
551
|
+
@click.option(
|
|
552
|
+
"--desc/--asc",
|
|
553
|
+
default=False,
|
|
554
|
+
show_default=True,
|
|
555
|
+
help="Sort descending instead of ascending.",
|
|
556
|
+
)
|
|
557
|
+
@click.option("--limit", type=int, default=None, help="Max rows to show.")
|
|
558
|
+
@click.option(
|
|
559
|
+
"--page-size",
|
|
560
|
+
type=int,
|
|
561
|
+
default=None,
|
|
562
|
+
help="Max rows to request from the clob endpoint. Defaults to --limit when set.",
|
|
563
|
+
)
|
|
564
|
+
@click.option(
|
|
565
|
+
"--to-csv",
|
|
566
|
+
type=click.Path(dir_okay=False, writable=True, path_type=str),
|
|
567
|
+
help="If set, write results to this CSV file.",
|
|
568
|
+
)
|
|
569
|
+
@click.option(
|
|
570
|
+
"--anonymous",
|
|
571
|
+
is_flag=True,
|
|
572
|
+
help="Do not send credentials for this read request, even if they are configured.",
|
|
573
|
+
)
|
|
574
|
+
@common_api_options
|
|
575
|
+
def list_cmd(**kwargs):
|
|
576
|
+
from cwmscli.commands.clob import list_cmd
|
|
577
|
+
|
|
578
|
+
list_cmd(**kwargs)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# endregion
|
|
408
582
|
# ================================================================================
|
|
409
583
|
# USERS
|
|
410
584
|
# ================================================================================
|