cwms-cli 0.3.5__tar.gz → 0.3.7__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.5 → cwms_cli-0.3.7}/PKG-INFO +6 -1
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/README.md +5 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/__main__.py +15 -4
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/blob.py +17 -1
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/commands_cwms.py +69 -9
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/test_main.py +35 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/transform.py +3 -2
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/__init__.py +7 -3
- cwms_cli-0.3.7/cwmscli/utils/friendly_errors.py +147 -0
- cwms_cli-0.3.7/cwmscli/utils/update.py +61 -0
- cwms_cli-0.3.7/cwmscli/utils/version.py +68 -0
- cwms_cli-0.3.7/cwmscli/utils/version_cli.py +47 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/pyproject.toml +1 -1
- cwms_cli-0.3.5/cwmscli/utils/version.py +0 -35
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/LICENSE +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/location/location_ids.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/location/location_ids_bygroup.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/getusgs_cda.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/click_help.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/deps.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/utils/ssl_errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
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
|
|
@@ -45,6 +45,11 @@ Or as of version `0.3.0+`
|
|
|
45
45
|
cwms-cli update
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
To install a specific version:
|
|
49
|
+
```sh
|
|
50
|
+
cwms-cli update --target-version 0.7.1 --yes
|
|
51
|
+
```
|
|
52
|
+
|
|
48
53
|
## Command line implementation
|
|
49
54
|
|
|
50
55
|
View the help in terminal:
|
|
@@ -10,6 +10,7 @@ from cwmscli.commands import commands_cwms
|
|
|
10
10
|
from cwmscli.load import __main__ as load
|
|
11
11
|
from cwmscli.usgs import usgs_group
|
|
12
12
|
from cwmscli.utils.click_help import add_version_to_help_tree
|
|
13
|
+
from cwmscli.utils.friendly_errors import to_user_facing_error
|
|
13
14
|
from cwmscli.utils.logging import (
|
|
14
15
|
LoggingConfig,
|
|
15
16
|
apply_logging_policies,
|
|
@@ -17,15 +18,18 @@ from cwmscli.utils.logging import (
|
|
|
17
18
|
setup_logging,
|
|
18
19
|
)
|
|
19
20
|
from cwmscli.utils.ssl_errors import is_cert_verify_error, ssl_help_text
|
|
20
|
-
from cwmscli.utils.
|
|
21
|
+
from cwmscli.utils.version_cli import show_version_and_exit
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
24
|
-
@click.
|
|
25
|
-
get_cwms_cli_version(),
|
|
25
|
+
@click.option(
|
|
26
26
|
"--version",
|
|
27
27
|
"-V",
|
|
28
|
-
|
|
28
|
+
is_flag=True,
|
|
29
|
+
is_eager=True,
|
|
30
|
+
expose_value=False,
|
|
31
|
+
callback=show_version_and_exit,
|
|
32
|
+
help="Show the cwms-cli version and exit.",
|
|
29
33
|
)
|
|
30
34
|
@click.option(
|
|
31
35
|
"--log-file",
|
|
@@ -115,6 +119,13 @@ def main() -> None:
|
|
|
115
119
|
click.echo(ssl_help_text(), err=True)
|
|
116
120
|
raise SystemExit(2)
|
|
117
121
|
|
|
122
|
+
if not debug:
|
|
123
|
+
friendly_error = to_user_facing_error(e)
|
|
124
|
+
if friendly_error is not None:
|
|
125
|
+
logging.debug("Suppressed traceback for CLI exception", exc_info=e)
|
|
126
|
+
friendly_error.show()
|
|
127
|
+
raise SystemExit(friendly_error.exit_code)
|
|
128
|
+
|
|
118
129
|
# If debug is enabled (or it's not a cert verify error), keep the normal failure behavior.
|
|
119
130
|
raise
|
|
120
131
|
|
|
@@ -133,6 +133,11 @@ def _resolve_optional_api_key(api_key: Optional[str], anonymous: bool) -> Option
|
|
|
133
133
|
return get_api_key(api_key, None)
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def _response_status_code(exc: BaseException) -> Optional[int]:
|
|
137
|
+
response = getattr(exc, "response", None)
|
|
138
|
+
return getattr(response, "status_code", None)
|
|
139
|
+
|
|
140
|
+
|
|
136
141
|
def store_blob(**kwargs):
|
|
137
142
|
import cwms
|
|
138
143
|
import requests
|
|
@@ -597,6 +602,7 @@ def download_cmd(
|
|
|
597
602
|
|
|
598
603
|
def delete_cmd(blob_id: str, office: str, api_root: str, api_key: str, dry_run: bool):
|
|
599
604
|
import cwms
|
|
605
|
+
import requests
|
|
600
606
|
|
|
601
607
|
if dry_run:
|
|
602
608
|
logging.info(
|
|
@@ -604,7 +610,17 @@ def delete_cmd(blob_id: str, office: str, api_root: str, api_key: str, dry_run:
|
|
|
604
610
|
)
|
|
605
611
|
return
|
|
606
612
|
cwms.init_session(api_root=api_root, api_key=api_key)
|
|
607
|
-
|
|
613
|
+
try:
|
|
614
|
+
cwms.delete_blob(office_id=office, blob_id=blob_id)
|
|
615
|
+
except requests.HTTPError as e:
|
|
616
|
+
if _response_status_code(e) == 404:
|
|
617
|
+
logging.info(
|
|
618
|
+
"Blob %s was already absent in office %s. Nothing to delete.",
|
|
619
|
+
blob_id,
|
|
620
|
+
office,
|
|
621
|
+
)
|
|
622
|
+
return
|
|
623
|
+
raise
|
|
608
624
|
logging.info(f"Deleted blob: {blob_id} for office: {office}")
|
|
609
625
|
|
|
610
626
|
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import subprocess
|
|
2
3
|
import sys
|
|
3
4
|
import textwrap
|
|
5
|
+
from typing import Optional
|
|
4
6
|
|
|
5
7
|
import click
|
|
6
8
|
|
|
7
9
|
from cwmscli import requirements as reqs
|
|
8
10
|
from cwmscli.callbacks import csv_to_list
|
|
9
11
|
from cwmscli.commands import csv2cwms
|
|
10
|
-
from cwmscli.utils import api_key_loc_option, common_api_options, to_uppercase
|
|
12
|
+
from cwmscli.utils import api_key_loc_option, colors, common_api_options, to_uppercase
|
|
11
13
|
from cwmscli.utils.deps import requires
|
|
14
|
+
from cwmscli.utils.update import (
|
|
15
|
+
build_update_package_spec,
|
|
16
|
+
launch_windows_update,
|
|
17
|
+
looks_like_missing_version,
|
|
18
|
+
)
|
|
12
19
|
from cwmscli.utils.version import get_cwms_cli_version
|
|
13
20
|
|
|
14
21
|
|
|
@@ -86,7 +93,16 @@ def csv2cwms_cmd(**kwargs):
|
|
|
86
93
|
csv2_main(**kwargs)
|
|
87
94
|
|
|
88
95
|
|
|
89
|
-
@click.command(
|
|
96
|
+
@click.command(
|
|
97
|
+
"update",
|
|
98
|
+
help="Update cwms-cli with pip, optionally targeting a specific version.",
|
|
99
|
+
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--target-version",
|
|
102
|
+
"target_version",
|
|
103
|
+
metavar="VERSION",
|
|
104
|
+
help="Install a specific cwms-cli version instead of the latest release.",
|
|
105
|
+
)
|
|
90
106
|
@click.option(
|
|
91
107
|
"--pre",
|
|
92
108
|
is_flag=True,
|
|
@@ -100,32 +116,76 @@ def csv2cwms_cmd(**kwargs):
|
|
|
100
116
|
default=False,
|
|
101
117
|
help="Skip confirmation prompt and run update immediately.",
|
|
102
118
|
)
|
|
103
|
-
def update_cli_cmd(pre: bool, yes: bool) -> None:
|
|
119
|
+
def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
|
|
104
120
|
current_version = get_cwms_cli_version()
|
|
105
|
-
|
|
121
|
+
package_spec = build_update_package_spec(target_version)
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
click.echo(
|
|
124
|
+
"Current cwms-cli version: " f"{colors.c(current_version, 'cyan', bright=True)}"
|
|
125
|
+
)
|
|
126
|
+
if target_version:
|
|
127
|
+
click.echo(
|
|
128
|
+
"Requested cwms-cli version: "
|
|
129
|
+
f"{colors.c(target_version, 'cyan', bright=True)}"
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
click.echo("Requested cwms-cli version: latest available release")
|
|
133
|
+
|
|
134
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", package_spec]
|
|
108
135
|
if pre:
|
|
109
136
|
cmd.append("--pre")
|
|
110
137
|
|
|
111
138
|
if not yes:
|
|
112
139
|
proceed = click.confirm("Proceed with updating cwms-cli via pip?", default=True)
|
|
113
140
|
if not proceed:
|
|
114
|
-
click.echo("Update canceled.")
|
|
141
|
+
click.echo(colors.warn("Update canceled."))
|
|
115
142
|
return
|
|
116
143
|
|
|
117
144
|
click.echo(f"Running: {' '.join(cmd)}")
|
|
145
|
+
if os.name == "nt":
|
|
146
|
+
try:
|
|
147
|
+
script_path = launch_windows_update(cmd)
|
|
148
|
+
except OSError as e:
|
|
149
|
+
raise click.ClickException(
|
|
150
|
+
f"Unable to launch Windows update process: {e}"
|
|
151
|
+
) from e
|
|
152
|
+
click.echo(
|
|
153
|
+
colors.ok(
|
|
154
|
+
"Opened a separate command window to complete the update after "
|
|
155
|
+
"cwms-cli exits."
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
click.echo(f"Update helper script: {script_path}")
|
|
159
|
+
return
|
|
160
|
+
|
|
118
161
|
try:
|
|
119
|
-
result = subprocess.run(
|
|
162
|
+
result = subprocess.run(
|
|
163
|
+
cmd,
|
|
164
|
+
check=False,
|
|
165
|
+
capture_output=True,
|
|
166
|
+
text=True,
|
|
167
|
+
)
|
|
120
168
|
except OSError as e:
|
|
121
169
|
raise click.ClickException(f"Unable to run pip update command: {e}") from e
|
|
122
170
|
|
|
171
|
+
if result.stdout:
|
|
172
|
+
click.echo(result.stdout, nl=False)
|
|
173
|
+
if result.stderr:
|
|
174
|
+
click.echo(result.stderr, err=True, nl=False)
|
|
175
|
+
|
|
123
176
|
if result.returncode != 0:
|
|
177
|
+
pip_output = "\n".join(part for part in [result.stdout, result.stderr] if part)
|
|
178
|
+
if target_version and looks_like_missing_version(pip_output, package_spec):
|
|
179
|
+
raise click.ClickException(
|
|
180
|
+
colors.err(
|
|
181
|
+
f"Requested cwms-cli version '{target_version}' was not found."
|
|
182
|
+
)
|
|
183
|
+
)
|
|
124
184
|
raise click.ClickException(
|
|
125
|
-
"cwms-cli update failed. Please review pip output above."
|
|
185
|
+
colors.err("cwms-cli update failed. Please review pip output above.")
|
|
126
186
|
)
|
|
127
187
|
|
|
128
|
-
click.echo("Update complete. Run `cwms-cli --version` to verify.")
|
|
188
|
+
click.echo(colors.ok("Update complete. Run `cwms-cli --version` to verify."))
|
|
129
189
|
|
|
130
190
|
|
|
131
191
|
# region Blob
|
|
@@ -133,6 +133,41 @@ def test_load_timeseries_uses_raw_timestamps_when_rounding_disabled(monkeypatch)
|
|
|
133
133
|
]
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def test_load_timeseries_preserves_raw_value_when_precision_omitted(monkeypatch):
|
|
137
|
+
monkeypatch.setenv("CDA_API_KEY", "test-key")
|
|
138
|
+
monkeypatch.setenv("CDA_OFFICE", "SWT")
|
|
139
|
+
monkeypatch.setenv("CDA_HOST", "https://example.test")
|
|
140
|
+
|
|
141
|
+
csv2_main = importlib.import_module("cwmscli.commands.csv2cwms.__main__")
|
|
142
|
+
tz = csv2_main.safe_zoneinfo("UTC")
|
|
143
|
+
epoch = int(datetime(2025, 3, 25, 12, 7, tzinfo=tz).timestamp())
|
|
144
|
+
|
|
145
|
+
file_data = {
|
|
146
|
+
"header": ["Time", "Headwater"],
|
|
147
|
+
"data": {
|
|
148
|
+
epoch: ["2025-03-25 12:07", "10.12345"],
|
|
149
|
+
},
|
|
150
|
+
"source_timezone": tz,
|
|
151
|
+
}
|
|
152
|
+
config = {
|
|
153
|
+
"interval": 900,
|
|
154
|
+
"input_files": {
|
|
155
|
+
"BROK": {
|
|
156
|
+
"timeseries": {
|
|
157
|
+
"BROK.Elev.Inst.15Minutes.0.Rev-SCADA-cda": {
|
|
158
|
+
"columns": "Headwater",
|
|
159
|
+
"units": "ft",
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result = csv2_main.load_timeseries(file_data, "BROK", config)
|
|
167
|
+
|
|
168
|
+
assert result[0]["values"] == [[epoch * 1000, 10.12345, 3]]
|
|
169
|
+
|
|
170
|
+
|
|
136
171
|
def test_load_timeseries_rounds_to_configured_interval_when_present(monkeypatch):
|
|
137
172
|
monkeypatch.setenv("CDA_API_KEY", "test-key")
|
|
138
173
|
monkeypatch.setenv("CDA_OFFICE", "SWT")
|
|
@@ -72,7 +72,8 @@ def select_value(
|
|
|
72
72
|
else:
|
|
73
73
|
value = raw_values[-1]
|
|
74
74
|
|
|
75
|
-
value
|
|
75
|
+
if value is not None and precision is not None:
|
|
76
|
+
value = round(value, precision)
|
|
76
77
|
quality = 3 if value is not None else 5
|
|
77
78
|
return value, quality
|
|
78
79
|
|
|
@@ -154,7 +155,7 @@ def load_timeseries(file_data, file_key, config, logger):
|
|
|
154
155
|
for name, meta in ts_config.items():
|
|
155
156
|
expr = meta["columns"]
|
|
156
157
|
units = meta.get("units", "")
|
|
157
|
-
precision = meta.get("precision"
|
|
158
|
+
precision = meta.get("precision")
|
|
158
159
|
ts_data = data
|
|
159
160
|
ts_interval = interval
|
|
160
161
|
|
|
@@ -46,7 +46,7 @@ def getusgs_timeseries(office, days_back, api_root, api_key, api_key_loc, backfi
|
|
|
46
46
|
from cwmscli.usgs.getusgs_cda import getusgs_cda
|
|
47
47
|
|
|
48
48
|
if backfill is not None:
|
|
49
|
-
backfill_list =
|
|
49
|
+
backfill_list = [item.strip() for item in backfill.split(",") if item.strip()]
|
|
50
50
|
else:
|
|
51
51
|
backfill_list = None
|
|
52
52
|
|
|
@@ -78,7 +78,9 @@ def getusgs_ratings(office, days_back, api_root, api_key, api_key_loc, rating_su
|
|
|
78
78
|
from cwmscli.usgs.getUSGS_ratings_cda import getusgs_rating_cda
|
|
79
79
|
|
|
80
80
|
if rating_subset is not None:
|
|
81
|
-
rating_list =
|
|
81
|
+
rating_list = [
|
|
82
|
+
item.strip() for item in rating_subset.split(",") if item.strip()
|
|
83
|
+
]
|
|
82
84
|
else:
|
|
83
85
|
rating_list = None
|
|
84
86
|
|
|
@@ -158,7 +160,9 @@ def getusgs_measurements(
|
|
|
158
160
|
if "group" in backfill:
|
|
159
161
|
backfill_group = True
|
|
160
162
|
elif type(backfill) == str:
|
|
161
|
-
backfill_list =
|
|
163
|
+
backfill_list = [
|
|
164
|
+
item.strip() for item in backfill.split(",") if item.strip()
|
|
165
|
+
]
|
|
162
166
|
api_key = get_api_key(api_key, api_key_loc)
|
|
163
167
|
getusgs_measurement_cda(
|
|
164
168
|
api_root=api_root,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Iterable, Optional, Set
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserFacingError(click.ClickException):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
message: str,
|
|
13
|
+
hint: Optional[str] = None,
|
|
14
|
+
*,
|
|
15
|
+
exit_code: int = 1,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.hint = hint
|
|
18
|
+
self.exit_code = exit_code
|
|
19
|
+
full_message = message
|
|
20
|
+
if hint:
|
|
21
|
+
full_message = f"{full_message}\nHint: {hint}"
|
|
22
|
+
super().__init__(full_message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _walk_exception_chain(exc: BaseException) -> Iterable[BaseException]:
|
|
26
|
+
seen: Set[int] = set()
|
|
27
|
+
cur: Optional[BaseException] = exc
|
|
28
|
+
while cur is not None and id(cur) not in seen:
|
|
29
|
+
seen.add(id(cur))
|
|
30
|
+
yield cur
|
|
31
|
+
cur = cur.__cause__ or cur.__context__
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _response_text(response) -> str:
|
|
35
|
+
text = getattr(response, "text", None)
|
|
36
|
+
if text:
|
|
37
|
+
return str(text).strip()
|
|
38
|
+
|
|
39
|
+
content = getattr(response, "content", None)
|
|
40
|
+
if isinstance(content, bytes):
|
|
41
|
+
return content.decode("utf-8", errors="replace").strip()
|
|
42
|
+
if content:
|
|
43
|
+
return str(content).strip()
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _response_json_field(response, field: str) -> Optional[str]:
|
|
48
|
+
text = _response_text(response)
|
|
49
|
+
if not text:
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(text)
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
value = payload.get(field)
|
|
56
|
+
if value in (None, ""):
|
|
57
|
+
return None
|
|
58
|
+
return str(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _trim_message(message: str) -> str:
|
|
62
|
+
message = message.strip()
|
|
63
|
+
if not message:
|
|
64
|
+
return message
|
|
65
|
+
return message if message.endswith(".") else f"{message}."
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _friendly_http_error(response) -> Optional[UserFacingError]:
|
|
69
|
+
status = getattr(response, "status_code", None)
|
|
70
|
+
if status is None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
api_message = _response_json_field(response, "message")
|
|
74
|
+
incident_id = _response_json_field(response, "incidentIdentifier")
|
|
75
|
+
|
|
76
|
+
if status == 400:
|
|
77
|
+
return UserFacingError(
|
|
78
|
+
_trim_message(api_message or "CWMS rejected the request"),
|
|
79
|
+
"Check the command arguments and input values, then try again.",
|
|
80
|
+
exit_code=2,
|
|
81
|
+
)
|
|
82
|
+
if status in (401, 403):
|
|
83
|
+
return UserFacingError(
|
|
84
|
+
_trim_message(api_message or "Authentication failed while calling CWMS"),
|
|
85
|
+
"Check CDA_API_KEY, --api-key, and whether the account can access the requested office.",
|
|
86
|
+
exit_code=2,
|
|
87
|
+
)
|
|
88
|
+
if status == 404:
|
|
89
|
+
return UserFacingError(
|
|
90
|
+
_trim_message(api_message or "Requested CWMS resource was not found"),
|
|
91
|
+
"Verify the identifier, office, and any category or group arguments.",
|
|
92
|
+
)
|
|
93
|
+
if status == 409:
|
|
94
|
+
return UserFacingError(
|
|
95
|
+
_trim_message(api_message or "CWMS reported a conflict for this request"),
|
|
96
|
+
"Review overwrite or replace options and confirm the resource state before retrying.",
|
|
97
|
+
)
|
|
98
|
+
if status == 429:
|
|
99
|
+
return UserFacingError(
|
|
100
|
+
_trim_message(api_message or "CWMS rate limited the request"),
|
|
101
|
+
"Wait briefly and retry. If this persists, reduce request frequency.",
|
|
102
|
+
)
|
|
103
|
+
if isinstance(status, int) and status >= 500:
|
|
104
|
+
hint = "Retry later or contact the service owner."
|
|
105
|
+
if incident_id:
|
|
106
|
+
hint += f" Include incidentIdentifier {incident_id}."
|
|
107
|
+
return UserFacingError(
|
|
108
|
+
_trim_message(api_message or "CWMS returned a server error"),
|
|
109
|
+
hint,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return UserFacingError(
|
|
113
|
+
_trim_message(api_message or f"CWMS request failed with HTTP {status}"),
|
|
114
|
+
"Re-run with `CWMS_CLI_DEBUG=1` or `cwms-cli --log-level DEBUG ...etc` for a traceback if you need the raw exception details.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_requests_exception(exc: BaseException, name: str) -> bool:
|
|
119
|
+
try:
|
|
120
|
+
import requests
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
return isinstance(exc, getattr(requests.exceptions, name, ()))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def to_user_facing_error(exc: BaseException) -> Optional[UserFacingError]:
|
|
128
|
+
for candidate in _walk_exception_chain(exc):
|
|
129
|
+
response = getattr(candidate, "response", None)
|
|
130
|
+
if response is not None:
|
|
131
|
+
friendly = _friendly_http_error(response)
|
|
132
|
+
if friendly is not None:
|
|
133
|
+
return friendly
|
|
134
|
+
|
|
135
|
+
if _is_requests_exception(candidate, "Timeout"):
|
|
136
|
+
return UserFacingError(
|
|
137
|
+
"Timed out while waiting for CWMS to respond.",
|
|
138
|
+
"Confirm the service is reachable and try again.",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if _is_requests_exception(candidate, "ConnectionError"):
|
|
142
|
+
return UserFacingError(
|
|
143
|
+
"Could not reach the CWMS API endpoint.",
|
|
144
|
+
"Check --api-root, network connectivity, and whether the server is running.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return None
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import tempfile
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_update_package_spec(target_version: Optional[str]) -> str:
|
|
7
|
+
if target_version:
|
|
8
|
+
return f"cwms-cli=={target_version}"
|
|
9
|
+
return "cwms-cli"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def looks_like_missing_version(pip_output: str, package_spec: str) -> bool:
|
|
13
|
+
return (
|
|
14
|
+
"No matching distribution found for" in pip_output
|
|
15
|
+
or "Could not find a version that satisfies the requirement" in pip_output
|
|
16
|
+
) and package_spec in pip_output
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write_windows_update_script(cmd: List[str]) -> str:
|
|
20
|
+
quoted_cmd = subprocess.list2cmdline(cmd)
|
|
21
|
+
script = "\r\n".join(
|
|
22
|
+
[
|
|
23
|
+
"@echo off",
|
|
24
|
+
"setlocal",
|
|
25
|
+
"echo Waiting for cwms-cli to exit before updating...",
|
|
26
|
+
"timeout /t 1 /nobreak >nul",
|
|
27
|
+
quoted_cmd,
|
|
28
|
+
'set "EXIT_CODE=%ERRORLEVEL%"',
|
|
29
|
+
'if "%EXIT_CODE%"=="0" (',
|
|
30
|
+
" echo Update complete. Run cwms-cli --version to verify.",
|
|
31
|
+
") else (",
|
|
32
|
+
" echo.",
|
|
33
|
+
" echo cwms-cli update failed. Review pip output above.",
|
|
34
|
+
")",
|
|
35
|
+
"echo.",
|
|
36
|
+
"echo Press any key to close this window.",
|
|
37
|
+
"pause >nul",
|
|
38
|
+
'(goto) 2>nul & del "%~f0"',
|
|
39
|
+
"exit /b %EXIT_CODE%",
|
|
40
|
+
"",
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
with tempfile.NamedTemporaryFile(
|
|
44
|
+
mode="w",
|
|
45
|
+
suffix=".cmd",
|
|
46
|
+
delete=False,
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
newline="",
|
|
49
|
+
) as fh:
|
|
50
|
+
fh.write(script)
|
|
51
|
+
return fh.name
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def launch_windows_update(cmd: List[str]) -> str:
|
|
55
|
+
script_path = write_windows_update_script(cmd)
|
|
56
|
+
creationflags = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
|
|
57
|
+
subprocess.Popen(
|
|
58
|
+
["cmd.exe", "/c", script_path],
|
|
59
|
+
creationflags=creationflags,
|
|
60
|
+
)
|
|
61
|
+
return script_path
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from urllib import error, request
|
|
10
|
+
|
|
11
|
+
PYPI_JSON_URL = "https://pypi.org/pypi/cwms-cli/json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=1)
|
|
15
|
+
def get_cwms_cli_version() -> str:
|
|
16
|
+
"""Return installed cwms-cli version, with pyproject fallback for source runs."""
|
|
17
|
+
try:
|
|
18
|
+
return metadata.version("cwms-cli")
|
|
19
|
+
except metadata.PackageNotFoundError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
23
|
+
if not pyproject.exists():
|
|
24
|
+
return "unknown"
|
|
25
|
+
|
|
26
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
# Prefer the [tool.poetry] version declaration.
|
|
29
|
+
in_poetry_section = False
|
|
30
|
+
for line in text.splitlines():
|
|
31
|
+
stripped = line.strip()
|
|
32
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
33
|
+
in_poetry_section = stripped == "[tool.poetry]"
|
|
34
|
+
continue
|
|
35
|
+
if in_poetry_section:
|
|
36
|
+
m = re.match(r'^version\s*=\s*"([^"]+)"\s*$', stripped)
|
|
37
|
+
if m:
|
|
38
|
+
return m.group(1)
|
|
39
|
+
|
|
40
|
+
return "unknown"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _version_key(version: str) -> Optional[tuple[int, ...]]:
|
|
44
|
+
match = re.fullmatch(r"\d+(?:\.\d+)*", version.strip())
|
|
45
|
+
if not match:
|
|
46
|
+
return None
|
|
47
|
+
return tuple(int(part) for part in version.split("."))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_newer_version_available(current_version: str, latest_version: str) -> bool:
|
|
51
|
+
current_key = _version_key(current_version)
|
|
52
|
+
latest_key = _version_key(latest_version)
|
|
53
|
+
if current_key is None or latest_key is None:
|
|
54
|
+
return False
|
|
55
|
+
return latest_key > current_key
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_latest_cwms_cli_version(timeout: float = 1.0) -> Optional[str]:
|
|
59
|
+
try:
|
|
60
|
+
with request.urlopen(PYPI_JSON_URL, timeout=timeout) as response:
|
|
61
|
+
payload = json.load(response)
|
|
62
|
+
except (OSError, ValueError, error.URLError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
version = payload.get("info", {}).get("version")
|
|
66
|
+
if not isinstance(version, str) or not version.strip():
|
|
67
|
+
return None
|
|
68
|
+
return version.strip()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from cwmscli.utils import colors
|
|
9
|
+
from cwmscli.utils.version import (
|
|
10
|
+
get_cwms_cli_version,
|
|
11
|
+
get_latest_cwms_cli_version,
|
|
12
|
+
is_newer_version_available,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def version_output_allows_color(
|
|
17
|
+
no_color: bool,
|
|
18
|
+
log_file: Optional[str],
|
|
19
|
+
) -> bool:
|
|
20
|
+
return sys.stdout.isatty() and (not no_color) and (not log_file)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def show_version_and_exit(
|
|
24
|
+
ctx: click.Context, _param: click.Parameter, value: bool
|
|
25
|
+
) -> None:
|
|
26
|
+
if not value or ctx.resilient_parsing:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
current_version = get_cwms_cli_version()
|
|
30
|
+
colors.set_enabled(
|
|
31
|
+
version_output_allows_color(
|
|
32
|
+
no_color=bool(ctx.params.get("no_color")),
|
|
33
|
+
log_file=ctx.params.get("log_file"),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
click.echo(f"cwms-cli version {colors.c(current_version, 'cyan', bright=True)}")
|
|
38
|
+
|
|
39
|
+
latest_version = get_latest_cwms_cli_version()
|
|
40
|
+
if latest_version and is_newer_version_available(current_version, latest_version):
|
|
41
|
+
click.echo(
|
|
42
|
+
colors.warn("Newer version available: ")
|
|
43
|
+
+ colors.c(latest_version, "yellow", bright=True)
|
|
44
|
+
)
|
|
45
|
+
click.echo(f"Run: {colors.c('cwms-cli update', 'cyan', bright=True)}")
|
|
46
|
+
|
|
47
|
+
ctx.exit()
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
from importlib import metadata
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@lru_cache(maxsize=1)
|
|
10
|
-
def get_cwms_cli_version() -> str:
|
|
11
|
-
"""Return installed cwms-cli version, with pyproject fallback for source runs."""
|
|
12
|
-
try:
|
|
13
|
-
return metadata.version("cwms-cli")
|
|
14
|
-
except metadata.PackageNotFoundError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
18
|
-
if not pyproject.exists():
|
|
19
|
-
return "unknown"
|
|
20
|
-
|
|
21
|
-
text = pyproject.read_text(encoding="utf-8")
|
|
22
|
-
|
|
23
|
-
# Prefer the [tool.poetry] version declaration.
|
|
24
|
-
in_poetry_section = False
|
|
25
|
-
for line in text.splitlines():
|
|
26
|
-
stripped = line.strip()
|
|
27
|
-
if stripped.startswith("[") and stripped.endswith("]"):
|
|
28
|
-
in_poetry_section = stripped == "[tool.poetry]"
|
|
29
|
-
continue
|
|
30
|
-
if in_poetry_section:
|
|
31
|
-
m = re.match(r'^version\s*=\s*"([^"]+)"\s*$', stripped)
|
|
32
|
-
if m:
|
|
33
|
-
return m.group(1)
|
|
34
|
-
|
|
35
|
-
return "unknown"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.3.5 → cwms_cli-0.3.7}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|