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.
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/PKG-INFO +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/__main__.py +2 -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.5.0/cwmscli/callbacks/__init__.py +27 -0
- {cwms_cli-0.3.8 → 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.3.8 → cwms_cli-0.5.0}/cwmscli/commands/commands_cwms.py +328 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +3 -3
- cwms_cli-0.5.0/cwmscli/commands/users.py +420 -0
- {cwms_cli-0.3.8 → 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.3.8 → 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.3.8 → cwms_cli-0.5.0}/cwmscli/requirements.py +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_cda.py +4 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/__init__.py +24 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/click_help.py +37 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/colors.py +0 -2
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/deps.py +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/pyproject.toml +6 -2
- cwms_cli-0.3.8/cwmscli/callbacks/__init__.py +0 -18
- cwms_cli-0.3.8/cwmscli/load/timeseries/timeseries.py +0 -59
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/LICENSE +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.5.0}/cwmscli/utils/version.py +0 -0
- {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
|
+
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
|
|
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
|
+
)
|