cwms-cli 0.6.0__tar.gz → 0.7.1__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.6.0 → cwms_cli-0.7.1}/PKG-INFO +1 -1
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/blob.py +20 -9
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/clob.py +13 -10
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/location/location.py +19 -6
- cwms_cli-0.7.1/cwmscli/load/location/location_ids.py +147 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/location/location_ids_bygroup.py +10 -2
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/root.py +72 -6
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/__init__.py +61 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/pyproject.toml +1 -1
- cwms_cli-0.6.0/cwmscli/load/location/location_ids.py +0 -105
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/LICENSE +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/README.md +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/__main__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/_generated/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/_generated/ownership_data.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/commands_cwms.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/shef/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/shef/import_critfile.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/shef/import_infile.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/shef/shef_parameters.csv +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/users.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/timeseries/timeseries_data.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/ownership.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/getusgs_cda.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/auth.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/callback_success.html +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/click_help.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/deps.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/utils/version.py +0 -0
- {cwms_cli-0.6.0 → cwms_cli-0.7.1}/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.7.1
|
|
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
|
|
@@ -11,10 +11,12 @@ from typing import Optional, Sequence, Tuple, Union
|
|
|
11
11
|
|
|
12
12
|
from cwmscli.utils import (
|
|
13
13
|
colors,
|
|
14
|
+
format_local_download_error,
|
|
14
15
|
get_api_key,
|
|
15
16
|
has_invalid_chars,
|
|
16
17
|
init_cwms_session,
|
|
17
18
|
log_scoped_read_hint,
|
|
19
|
+
validate_default_download_dest,
|
|
18
20
|
)
|
|
19
21
|
from cwmscli.utils.click_help import DOCS_BASE_URL
|
|
20
22
|
from cwmscli.utils.deps import requires
|
|
@@ -109,6 +111,14 @@ def _save_blob_content(
|
|
|
109
111
|
return dest
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
def _default_download_dest(blob_id: str) -> str:
|
|
115
|
+
return validate_default_download_dest(
|
|
116
|
+
blob_id,
|
|
117
|
+
resource_name="Blob",
|
|
118
|
+
docs_url=BLOB_DOCS_URL,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
112
122
|
def _blob_media_type(cwms_module, office: str, blob_id: str) -> Optional[str]:
|
|
113
123
|
try:
|
|
114
124
|
result = cwms_module.get_blobs(office_id=office, blob_id_like=blob_id)
|
|
@@ -602,7 +612,7 @@ def download_cmd(
|
|
|
602
612
|
|
|
603
613
|
try:
|
|
604
614
|
blob_content = cwms.get_blob(office_id=office, blob_id=bid)
|
|
605
|
-
target = dest or bid
|
|
615
|
+
target = dest or _default_download_dest(bid)
|
|
606
616
|
_save_blob_content(
|
|
607
617
|
blob_content,
|
|
608
618
|
dest=target,
|
|
@@ -621,14 +631,15 @@ def download_cmd(
|
|
|
621
631
|
)
|
|
622
632
|
sys.exit(1)
|
|
623
633
|
except Exception as e:
|
|
624
|
-
logging.error(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
634
|
+
logging.error(format_local_download_error(e, BLOB_DOCS_URL))
|
|
635
|
+
if not isinstance(e, (OSError, ValueError)):
|
|
636
|
+
log_scoped_read_hint(
|
|
637
|
+
credential_kind=credential_kind,
|
|
638
|
+
anonymous=anonymous,
|
|
639
|
+
office=office,
|
|
640
|
+
action="download",
|
|
641
|
+
resource="blob content",
|
|
642
|
+
)
|
|
632
643
|
sys.exit(1)
|
|
633
644
|
|
|
634
645
|
|
|
@@ -9,7 +9,13 @@ import pandas as pd
|
|
|
9
9
|
import requests
|
|
10
10
|
from cwms import api as cwms_api
|
|
11
11
|
|
|
12
|
-
from cwmscli.utils import
|
|
12
|
+
from cwmscli.utils import (
|
|
13
|
+
format_local_download_error,
|
|
14
|
+
get_api_key,
|
|
15
|
+
has_invalid_chars,
|
|
16
|
+
log_scoped_read_hint,
|
|
17
|
+
validate_default_download_dest,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
def _join_api_url(api_root: str, path: str) -> str:
|
|
@@ -29,6 +35,10 @@ def _write_clob_content(content: str, dest: str) -> str:
|
|
|
29
35
|
return dest
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
def _default_download_dest(clob_id: str) -> str:
|
|
39
|
+
return validate_default_download_dest(clob_id, resource_name="Clob")
|
|
40
|
+
|
|
41
|
+
|
|
32
42
|
def _clob_endpoint_id(clob_id: str) -> tuple[str, Optional[str]]:
|
|
33
43
|
normalized = clob_id.upper()
|
|
34
44
|
if has_invalid_chars(normalized):
|
|
@@ -198,7 +208,7 @@ def download_cmd(
|
|
|
198
208
|
content = str(payload)
|
|
199
209
|
else:
|
|
200
210
|
content = _get_special_clob_text(office=office, clob_id=query_id)
|
|
201
|
-
target = dest or bid
|
|
211
|
+
target = dest or _default_download_dest(bid)
|
|
202
212
|
_write_clob_content(content, target)
|
|
203
213
|
logging.info(f"Downloaded clob to: {target}")
|
|
204
214
|
except requests.HTTPError as e:
|
|
@@ -213,14 +223,7 @@ def download_cmd(
|
|
|
213
223
|
)
|
|
214
224
|
sys.exit(1)
|
|
215
225
|
except Exception as e:
|
|
216
|
-
logging.error(
|
|
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
|
-
)
|
|
226
|
+
logging.error(format_local_download_error(e, ""))
|
|
224
227
|
sys.exit(1)
|
|
225
228
|
|
|
226
229
|
|
|
@@ -5,6 +5,7 @@ import click
|
|
|
5
5
|
|
|
6
6
|
from cwmscli import requirements as reqs
|
|
7
7
|
from cwmscli.load.root import (
|
|
8
|
+
csv_source_target_options,
|
|
8
9
|
load_group,
|
|
9
10
|
shared_source_target_options,
|
|
10
11
|
validate_cda_targets,
|
|
@@ -24,12 +25,14 @@ def location(ctx):
|
|
|
24
25
|
@location.command(
|
|
25
26
|
"ids-all",
|
|
26
27
|
help=(
|
|
27
|
-
"Copy locations from a source CDA catalog to a target CDA
|
|
28
|
+
"Copy locations from a source CDA catalog to a target CDA, "
|
|
29
|
+
"or to/from a CSV file via --source-csv / --target-csv. "
|
|
28
30
|
"The --like and --location-kind-like filters use CDA regex semantics. "
|
|
29
31
|
f"Regex guide: {CDA_REGEXP_GUIDE_URL}"
|
|
30
32
|
),
|
|
31
33
|
)
|
|
32
34
|
@shared_source_target_options
|
|
35
|
+
@csv_source_target_options(allow_source_csv=True, allow_target_csv=True)
|
|
33
36
|
@click.option(
|
|
34
37
|
"--like",
|
|
35
38
|
default=None,
|
|
@@ -57,14 +60,16 @@ def location(ctx):
|
|
|
57
60
|
@requires(reqs.cwms)
|
|
58
61
|
@validate_cda_targets
|
|
59
62
|
def load_locations(
|
|
60
|
-
source_cda: str,
|
|
61
|
-
source_office: str,
|
|
62
|
-
target_cda: str,
|
|
63
|
+
source_cda: Optional[str],
|
|
64
|
+
source_office: Optional[str],
|
|
65
|
+
target_cda: Optional[str],
|
|
63
66
|
target_api_key: Optional[str],
|
|
64
67
|
verbose: int,
|
|
65
68
|
dry_run: bool,
|
|
66
69
|
like: Optional[str],
|
|
67
70
|
location_kind_like: Optional[Iterable[str]] = None,
|
|
71
|
+
source_csv: Optional[str] = None,
|
|
72
|
+
target_csv: Optional[str] = None,
|
|
68
73
|
):
|
|
69
74
|
from cwmscli.load.location.location_ids import load_locations as _load_locations
|
|
70
75
|
|
|
@@ -77,14 +82,20 @@ def load_locations(
|
|
|
77
82
|
dry_run=dry_run,
|
|
78
83
|
like=like,
|
|
79
84
|
location_kind_like=location_kind_like,
|
|
85
|
+
source_csv=source_csv,
|
|
86
|
+
target_csv=target_csv,
|
|
80
87
|
)
|
|
81
88
|
|
|
82
89
|
|
|
83
90
|
@location.command(
|
|
84
91
|
"ids-bygroup",
|
|
85
|
-
help=
|
|
92
|
+
help=(
|
|
93
|
+
"Copy locations from a CWMS Location Group (source CDA) to a target CDA, "
|
|
94
|
+
"or export the resolved members to a CSV file via --target-csv."
|
|
95
|
+
),
|
|
86
96
|
)
|
|
87
97
|
@shared_source_target_options
|
|
98
|
+
@csv_source_target_options(allow_source_csv=False, allow_target_csv=True)
|
|
88
99
|
@click.option(
|
|
89
100
|
"--group-id", required=True, help="Location Group ID (e.g., 'Ark Basin')."
|
|
90
101
|
)
|
|
@@ -112,7 +123,7 @@ def load_locations(
|
|
|
112
123
|
def load_locations_from_group(
|
|
113
124
|
source_cda: str,
|
|
114
125
|
source_office: str,
|
|
115
|
-
target_cda: str,
|
|
126
|
+
target_cda: Optional[str],
|
|
116
127
|
target_api_key: Optional[str],
|
|
117
128
|
verbose: int,
|
|
118
129
|
group_id: str,
|
|
@@ -121,6 +132,7 @@ def load_locations_from_group(
|
|
|
121
132
|
category_office_id: Optional[str],
|
|
122
133
|
filter_office: bool,
|
|
123
134
|
dry_run: bool,
|
|
135
|
+
target_csv: Optional[str] = None,
|
|
124
136
|
):
|
|
125
137
|
from cwmscli.load.location.location_ids_bygroup import copy_from_group
|
|
126
138
|
|
|
@@ -136,4 +148,5 @@ def load_locations_from_group(
|
|
|
136
148
|
category_office_id=category_office_id,
|
|
137
149
|
filter_office=filter_office,
|
|
138
150
|
dry_run=dry_run,
|
|
151
|
+
target_csv=target_csv,
|
|
139
152
|
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
from typing import Iterable, Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import cwms
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from cwmscli.utils import init_cwms_session
|
|
11
|
+
from cwmscli.utils.links import CDA_REGEXP_GUIDE_URL
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_locations(
|
|
17
|
+
source_cda: Optional[str],
|
|
18
|
+
source_office: Optional[str],
|
|
19
|
+
target_cda: Optional[str],
|
|
20
|
+
target_api_key: Optional[str],
|
|
21
|
+
verbose: int,
|
|
22
|
+
dry_run: bool,
|
|
23
|
+
like: Optional[str],
|
|
24
|
+
location_kind_like: Optional[Iterable[str]] = "ALL",
|
|
25
|
+
source_csv: Optional[str] = None,
|
|
26
|
+
target_csv: Optional[str] = None,
|
|
27
|
+
):
|
|
28
|
+
src_label = source_csv or source_cda or "-"
|
|
29
|
+
tgt_label = target_csv or target_cda or "-"
|
|
30
|
+
|
|
31
|
+
if verbose:
|
|
32
|
+
logger.info(
|
|
33
|
+
f"[load locations] source={src_label} ({source_office or '-'}) -> target={tgt_label}"
|
|
34
|
+
)
|
|
35
|
+
logger.info(
|
|
36
|
+
f" like={like or '-'} kinds={list(location_kind_like) or '-'} dry_run={dry_run}"
|
|
37
|
+
)
|
|
38
|
+
if like or (
|
|
39
|
+
location_kind_like
|
|
40
|
+
and list(location_kind_like) != ["ALL"]
|
|
41
|
+
and list(location_kind_like) != []
|
|
42
|
+
):
|
|
43
|
+
logger.info(" CDA regex guide: %s", CDA_REGEXP_GUIDE_URL)
|
|
44
|
+
|
|
45
|
+
if source_csv:
|
|
46
|
+
df = pd.read_csv(source_csv)
|
|
47
|
+
locations = df.to_dict(orient="records")
|
|
48
|
+
else:
|
|
49
|
+
init_cwms_session(cwms, api_root=source_cda)
|
|
50
|
+
locations = _fetch_locations_from_cda(
|
|
51
|
+
source_office=source_office,
|
|
52
|
+
like=like,
|
|
53
|
+
location_kind_like=location_kind_like,
|
|
54
|
+
verbose=verbose,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if verbose:
|
|
58
|
+
logger.info("Got %s locations from source", len(locations))
|
|
59
|
+
|
|
60
|
+
if dry_run:
|
|
61
|
+
for loc in locations:
|
|
62
|
+
logger.info(
|
|
63
|
+
f"[dry-run] would store Location(name={loc['name']}) to {tgt_label} "
|
|
64
|
+
f"({source_office or loc.get('office-id') or '-'})"
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if target_csv:
|
|
69
|
+
pd.DataFrame(locations).to_csv(target_csv, index=False)
|
|
70
|
+
click.echo(f"Wrote {len(locations)} locations to {target_csv}")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
init_cwms_session(cwms, api_root=target_cda, api_key=target_api_key)
|
|
74
|
+
|
|
75
|
+
errors = 0
|
|
76
|
+
for loc in locations:
|
|
77
|
+
loc = _clean_row(loc)
|
|
78
|
+
try:
|
|
79
|
+
if loc.get("active") is True:
|
|
80
|
+
result = cwms.store_location(data=loc, fail_if_exists=False)
|
|
81
|
+
if verbose:
|
|
82
|
+
logger.info("%s", result)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
errors += 1
|
|
85
|
+
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
86
|
+
|
|
87
|
+
if errors:
|
|
88
|
+
raise click.ClickException(f"Completed with {errors} error(s).")
|
|
89
|
+
|
|
90
|
+
click.echo("Done.")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fetch_locations_from_cda(
|
|
94
|
+
source_office: str,
|
|
95
|
+
like: Optional[str],
|
|
96
|
+
location_kind_like: Optional[Iterable[str]],
|
|
97
|
+
verbose: int,
|
|
98
|
+
) -> list:
|
|
99
|
+
cat_kwargs = {"office_id": source_office}
|
|
100
|
+
if like:
|
|
101
|
+
cat_kwargs["like"] = like
|
|
102
|
+
kinds = list(location_kind_like) if location_kind_like else ["ALL"]
|
|
103
|
+
if "ALL" in kinds:
|
|
104
|
+
kinds = ["ALL"]
|
|
105
|
+
|
|
106
|
+
if kinds == ["ALL"] and not like:
|
|
107
|
+
return cwms.get_locations(office_id=source_office).json
|
|
108
|
+
|
|
109
|
+
locations = []
|
|
110
|
+
seen_location_ids = set()
|
|
111
|
+
for kind in kinds:
|
|
112
|
+
cat_kwargs_k = dict(cat_kwargs)
|
|
113
|
+
if kind != "ALL":
|
|
114
|
+
cat_kwargs_k["location_kind_like"] = kind
|
|
115
|
+
|
|
116
|
+
if verbose >= 2:
|
|
117
|
+
logger.debug(" > catalog query: %s", cat_kwargs_k)
|
|
118
|
+
|
|
119
|
+
resp = cwms.get_locations_catalog(**cat_kwargs_k)
|
|
120
|
+
if resp.df.empty:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
for location_id in resp.df["name"].tolist():
|
|
124
|
+
if location_id in seen_location_ids:
|
|
125
|
+
continue
|
|
126
|
+
seen_location_ids.add(location_id)
|
|
127
|
+
if verbose >= 2:
|
|
128
|
+
logger.debug(" > location fetch: %s", location_id)
|
|
129
|
+
detail_resp = cwms.get_locations(
|
|
130
|
+
office_id=source_office,
|
|
131
|
+
location_ids=rf"^{re.escape(location_id)}$",
|
|
132
|
+
)
|
|
133
|
+
if detail_resp and detail_resp.json:
|
|
134
|
+
locations.extend(detail_resp.json)
|
|
135
|
+
return locations
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _clean_row(loc: dict) -> dict:
|
|
139
|
+
cleaned = {}
|
|
140
|
+
for k, v in loc.items():
|
|
141
|
+
if isinstance(v, float) and math.isnan(v):
|
|
142
|
+
cleaned[k] = None
|
|
143
|
+
elif isinstance(v, str) and v in ("True", "False"):
|
|
144
|
+
cleaned[k] = v == "True"
|
|
145
|
+
else:
|
|
146
|
+
cleaned[k] = v
|
|
147
|
+
return cleaned
|
|
@@ -5,6 +5,7 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
import cwms
|
|
8
|
+
import pandas as pd
|
|
8
9
|
|
|
9
10
|
from cwmscli.utils import init_cwms_session
|
|
10
11
|
|
|
@@ -22,7 +23,7 @@ def exact_or_regex(ids: list[str]) -> str:
|
|
|
22
23
|
def copy_from_group(
|
|
23
24
|
source_cda: str,
|
|
24
25
|
source_office: str,
|
|
25
|
-
target_cda: str,
|
|
26
|
+
target_cda: Optional[str],
|
|
26
27
|
target_api_key: Optional[str],
|
|
27
28
|
verbose: int,
|
|
28
29
|
group_id: str,
|
|
@@ -31,6 +32,7 @@ def copy_from_group(
|
|
|
31
32
|
category_office_id: Optional[str],
|
|
32
33
|
filter_office: bool,
|
|
33
34
|
dry_run: bool,
|
|
35
|
+
target_csv: Optional[str] = None,
|
|
34
36
|
):
|
|
35
37
|
group_office_id = group_office_id or source_office
|
|
36
38
|
category_office_id = category_office_id or source_office
|
|
@@ -103,10 +105,16 @@ def copy_from_group(
|
|
|
103
105
|
if dry_run:
|
|
104
106
|
for loc in locations:
|
|
105
107
|
logger.info(
|
|
106
|
-
f"[dry-run] would store Location(name={loc['name']}) to
|
|
108
|
+
f"[dry-run] would store Location(name={loc['name']}) to "
|
|
109
|
+
f"{target_csv or target_cda} ({source_office})"
|
|
107
110
|
)
|
|
108
111
|
return
|
|
109
112
|
|
|
113
|
+
if target_csv:
|
|
114
|
+
pd.DataFrame(locations).to_csv(target_csv, index=False)
|
|
115
|
+
click.echo(f"Wrote {len(locations)} locations to {target_csv}")
|
|
116
|
+
return
|
|
117
|
+
|
|
110
118
|
try:
|
|
111
119
|
init_cwms_session(cwms, api_root=target_cda, api_key=target_api_key)
|
|
112
120
|
except Exception as e:
|
|
@@ -43,11 +43,39 @@ def _norm_office(o: Optional[str]) -> str:
|
|
|
43
43
|
def validate_cda_targets(func):
|
|
44
44
|
@functools.wraps(func)
|
|
45
45
|
def wrapper(*args, **kwargs):
|
|
46
|
+
source_csv = kwargs.get("source_csv")
|
|
47
|
+
target_csv = kwargs.get("target_csv")
|
|
48
|
+
|
|
49
|
+
if source_csv and target_csv:
|
|
50
|
+
raise click.ClickException(
|
|
51
|
+
"--source-csv and --target-csv are both set, but no CDA is involved. "
|
|
52
|
+
"Use a plain file copy instead."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if source_csv:
|
|
56
|
+
if kwargs.get("source_cda") and _param_was_explicit("source_cda"):
|
|
57
|
+
raise click.ClickException(
|
|
58
|
+
"--source-csv and --source-cda are mutually exclusive."
|
|
59
|
+
)
|
|
60
|
+
kwargs["source_cda"] = None
|
|
61
|
+
|
|
62
|
+
if target_csv:
|
|
63
|
+
if kwargs.get("target_cda") and _param_was_explicit("target_cda"):
|
|
64
|
+
raise click.ClickException(
|
|
65
|
+
"--target-csv and --target-cda are mutually exclusive."
|
|
66
|
+
)
|
|
67
|
+
kwargs["target_cda"] = None
|
|
68
|
+
|
|
46
69
|
source_cda = _normalize_url(kwargs.get("source_cda"))
|
|
47
70
|
target_cda = _normalize_url(kwargs.get("target_cda"))
|
|
48
71
|
source_office = _norm_office(kwargs.get("source_office"))
|
|
49
72
|
target_office = _norm_office(kwargs.get("target_office"))
|
|
50
73
|
|
|
74
|
+
if source_cda and not source_office:
|
|
75
|
+
raise click.ClickException(
|
|
76
|
+
"--source-office is required when reading from a source CDA."
|
|
77
|
+
)
|
|
78
|
+
|
|
51
79
|
same_root = source_cda == target_cda and bool(source_cda)
|
|
52
80
|
same_office = source_office == target_office and bool(source_office)
|
|
53
81
|
|
|
@@ -64,33 +92,40 @@ def validate_cda_targets(func):
|
|
|
64
92
|
"This is allowed, but double-check intent.",
|
|
65
93
|
)
|
|
66
94
|
|
|
95
|
+
src_label = source_csv or source_cda or "-"
|
|
96
|
+
tgt_label = target_csv or target_cda or "-"
|
|
67
97
|
logger.info(
|
|
68
|
-
f"Source: {
|
|
69
|
-
f"Target: {
|
|
98
|
+
f"Source: {src_label} (office={source_office or '-'})\n"
|
|
99
|
+
f"Target: {tgt_label} (office={target_office or source_office or '-'})",
|
|
70
100
|
)
|
|
71
101
|
return func(*args, **kwargs)
|
|
72
102
|
|
|
73
103
|
return wrapper
|
|
74
104
|
|
|
75
105
|
|
|
106
|
+
def _param_was_explicit(name: str) -> bool:
|
|
107
|
+
ctx = click.get_current_context(silent=True)
|
|
108
|
+
if ctx is None:
|
|
109
|
+
return False
|
|
110
|
+
src = ctx.get_parameter_source(name)
|
|
111
|
+
return src is not None and src.name != "DEFAULT"
|
|
112
|
+
|
|
113
|
+
|
|
76
114
|
def shared_source_target_options(f):
|
|
77
115
|
f = click.option(
|
|
78
116
|
"--source-cda",
|
|
79
117
|
envvar="CDA_SOURCE_URL",
|
|
80
|
-
required=True,
|
|
81
118
|
default="https://cwms-data.usace.army.mil/cwms-data/",
|
|
82
119
|
help="Source CWMS Data API root. Default: https://cwms-data.usace.army.mil/cwms-data/",
|
|
83
120
|
)(f)
|
|
84
121
|
f = click.option(
|
|
85
122
|
"--source-office",
|
|
86
123
|
envvar="CDA_SOURCE_OFFICE",
|
|
87
|
-
|
|
88
|
-
help="Source office ID (e.g. SWT, SWL).",
|
|
124
|
+
help="Source office ID (e.g. SWT, SWL). Required when reading from a CDA.",
|
|
89
125
|
)(f)
|
|
90
126
|
f = click.option(
|
|
91
127
|
"--target-cda",
|
|
92
128
|
envvar="CDA_TARGET_URL",
|
|
93
|
-
required=True,
|
|
94
129
|
default="http://localhost:8081/cwms-data/",
|
|
95
130
|
help="Target CWMS Data API root. Default: http://localhost:8081/cwms-data/",
|
|
96
131
|
)(f)
|
|
@@ -115,6 +150,37 @@ def shared_source_target_options(f):
|
|
|
115
150
|
return f
|
|
116
151
|
|
|
117
152
|
|
|
153
|
+
def csv_source_target_options(*, allow_source_csv: bool, allow_target_csv: bool):
|
|
154
|
+
"""Add --source-csv and/or --target-csv to a command, depending on flags."""
|
|
155
|
+
|
|
156
|
+
def decorator(f):
|
|
157
|
+
if allow_target_csv:
|
|
158
|
+
f = click.option(
|
|
159
|
+
"--target-csv",
|
|
160
|
+
"target_csv",
|
|
161
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
162
|
+
default=None,
|
|
163
|
+
help=(
|
|
164
|
+
"Write fetched locations to this CSV file instead of POSTing "
|
|
165
|
+
"to a target CDA. Mutually exclusive with --target-cda."
|
|
166
|
+
),
|
|
167
|
+
)(f)
|
|
168
|
+
if allow_source_csv:
|
|
169
|
+
f = click.option(
|
|
170
|
+
"--source-csv",
|
|
171
|
+
"source_csv",
|
|
172
|
+
type=click.Path(exists=True, dir_okay=False, readable=True),
|
|
173
|
+
default=None,
|
|
174
|
+
help=(
|
|
175
|
+
"Read locations from this CSV file instead of fetching from "
|
|
176
|
+
"a source CDA. Mutually exclusive with --source-cda."
|
|
177
|
+
),
|
|
178
|
+
)(f)
|
|
179
|
+
return f
|
|
180
|
+
|
|
181
|
+
return decorator
|
|
182
|
+
|
|
183
|
+
|
|
118
184
|
@click.group(
|
|
119
185
|
name="load",
|
|
120
186
|
help="Load data from one CWMS Data API instance to another.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging as py_logging
|
|
2
|
+
import re
|
|
2
3
|
import time
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Optional, Union
|
|
@@ -235,6 +236,66 @@ def log_scoped_read_hint(
|
|
|
235
236
|
)
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def format_local_download_error(error: Exception, docs_url: str) -> str:
|
|
240
|
+
if isinstance(error, (OSError, ValueError)):
|
|
241
|
+
message = (
|
|
242
|
+
f"{colors.c('Failed to download:', 'red', bright=True)} {error}. "
|
|
243
|
+
f"If this is a local destination/path issue, pass "
|
|
244
|
+
f"{colors.c('--dest', 'cyan', bright=True)} explicitly."
|
|
245
|
+
)
|
|
246
|
+
if docs_url:
|
|
247
|
+
message = (
|
|
248
|
+
f"{message} {colors.c('Docs:', 'blue', bright=True)} "
|
|
249
|
+
f"{colors.c(docs_url, 'blue', bright=True)}"
|
|
250
|
+
)
|
|
251
|
+
return message
|
|
252
|
+
return f"{colors.c('Failed to download:', 'red', bright=True)} {error}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def validate_default_download_dest(
|
|
256
|
+
raw_id: str,
|
|
257
|
+
*,
|
|
258
|
+
resource_name: str,
|
|
259
|
+
docs_url: str = "",
|
|
260
|
+
) -> str:
|
|
261
|
+
if raw_id is None:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"{resource_name} ID must include a non-root destination name. "
|
|
264
|
+
f"Pass --dest explicitly if needed."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if raw_id.startswith("//") or raw_id.startswith("\\\\"):
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"{resource_name} ID must resolve to a relative local path. "
|
|
270
|
+
f"Pass --dest explicitly if needed."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
target = raw_id.lstrip("/\\")
|
|
274
|
+
if not target:
|
|
275
|
+
message = (
|
|
276
|
+
f"{resource_name} ID must include a non-root destination name. "
|
|
277
|
+
f"Pass --dest explicitly if needed."
|
|
278
|
+
)
|
|
279
|
+
if docs_url:
|
|
280
|
+
message = f"{message} Docs: {docs_url}"
|
|
281
|
+
raise ValueError(message)
|
|
282
|
+
|
|
283
|
+
if re.match(r"^[A-Za-z]:", target):
|
|
284
|
+
raise ValueError(
|
|
285
|
+
f"{resource_name} ID must resolve to a relative local path. "
|
|
286
|
+
f"Pass --dest explicitly if needed."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
parts = re.split(r"[\\/]", target)
|
|
290
|
+
if any(part in {"", ".", ".."} for part in parts):
|
|
291
|
+
raise ValueError(
|
|
292
|
+
f"{resource_name} ID must resolve to a relative local path. "
|
|
293
|
+
f"Pass --dest explicitly if needed."
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return target
|
|
297
|
+
|
|
298
|
+
|
|
238
299
|
def common_api_options(f):
|
|
239
300
|
f = log_level_option(f)
|
|
240
301
|
f = office_option(f)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import re
|
|
3
|
-
from typing import Iterable, Optional
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
import cwms
|
|
7
|
-
|
|
8
|
-
from cwmscli.utils import init_cwms_session
|
|
9
|
-
from cwmscli.utils.links import CDA_REGEXP_GUIDE_URL
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def load_locations(
|
|
15
|
-
source_cda: str,
|
|
16
|
-
source_office: str,
|
|
17
|
-
target_cda: str,
|
|
18
|
-
target_api_key: Optional[str],
|
|
19
|
-
verbose: int,
|
|
20
|
-
dry_run: bool,
|
|
21
|
-
like: Optional[str],
|
|
22
|
-
location_kind_like: Optional[Iterable[str]] = "ALL",
|
|
23
|
-
):
|
|
24
|
-
if verbose:
|
|
25
|
-
logger.info(
|
|
26
|
-
f"[load locations] source={source_cda} ({source_office}) -> target={target_cda}"
|
|
27
|
-
)
|
|
28
|
-
logger.info(
|
|
29
|
-
f" like={like or '-'} kinds={list(location_kind_like) or '-'} dry_run={dry_run}"
|
|
30
|
-
)
|
|
31
|
-
if like or (
|
|
32
|
-
location_kind_like
|
|
33
|
-
and list(location_kind_like) != ["ALL"]
|
|
34
|
-
and list(location_kind_like) != []
|
|
35
|
-
):
|
|
36
|
-
logger.info(" CDA regex guide: %s", CDA_REGEXP_GUIDE_URL)
|
|
37
|
-
|
|
38
|
-
init_cwms_session(cwms, api_root=source_cda)
|
|
39
|
-
|
|
40
|
-
cat_kwargs = {"office_id": source_office}
|
|
41
|
-
if like:
|
|
42
|
-
cat_kwargs["like"] = like
|
|
43
|
-
kinds = list(location_kind_like) if location_kind_like else ["ALL"]
|
|
44
|
-
if "ALL" in kinds:
|
|
45
|
-
kinds = ["ALL"]
|
|
46
|
-
|
|
47
|
-
locations = []
|
|
48
|
-
|
|
49
|
-
if kinds == ["ALL"] and not like:
|
|
50
|
-
locations = cwms.get_locations(office_id=source_office).json
|
|
51
|
-
else:
|
|
52
|
-
seen_location_ids = set()
|
|
53
|
-
for kind in kinds:
|
|
54
|
-
cat_kwargs_k = dict(cat_kwargs)
|
|
55
|
-
if kind != "ALL":
|
|
56
|
-
cat_kwargs_k["location_kind_like"] = kind
|
|
57
|
-
|
|
58
|
-
if verbose >= 2:
|
|
59
|
-
logger.debug(" > catalog query: %s", cat_kwargs_k)
|
|
60
|
-
|
|
61
|
-
resp = cwms.get_locations_catalog(**cat_kwargs_k)
|
|
62
|
-
if resp.df.empty:
|
|
63
|
-
continue
|
|
64
|
-
|
|
65
|
-
for location_id in resp.df["name"].tolist():
|
|
66
|
-
if location_id in seen_location_ids:
|
|
67
|
-
continue
|
|
68
|
-
seen_location_ids.add(location_id)
|
|
69
|
-
if verbose >= 2:
|
|
70
|
-
logger.debug(" > location fetch: %s", location_id)
|
|
71
|
-
detail_resp = cwms.get_locations(
|
|
72
|
-
office_id=source_office,
|
|
73
|
-
location_ids=rf"^{re.escape(location_id)}$",
|
|
74
|
-
)
|
|
75
|
-
if detail_resp and detail_resp.json:
|
|
76
|
-
locations.extend(detail_resp.json)
|
|
77
|
-
|
|
78
|
-
if verbose:
|
|
79
|
-
logger.info("Fetched %s locations from source", len(locations))
|
|
80
|
-
|
|
81
|
-
if dry_run:
|
|
82
|
-
for loc in locations:
|
|
83
|
-
logger.info(
|
|
84
|
-
f"[dry-run] would store Location(name={loc['name']}) to {target_cda} ({source_office})"
|
|
85
|
-
)
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
# init target once
|
|
89
|
-
init_cwms_session(cwms, api_root=target_cda, api_key=target_api_key)
|
|
90
|
-
|
|
91
|
-
errors = 0
|
|
92
|
-
for loc in locations:
|
|
93
|
-
try:
|
|
94
|
-
if loc["active"] is True:
|
|
95
|
-
result = cwms.store_location(data=loc, fail_if_exists=False)
|
|
96
|
-
if verbose:
|
|
97
|
-
logger.info("%s", result)
|
|
98
|
-
except Exception as e:
|
|
99
|
-
errors += 1
|
|
100
|
-
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
101
|
-
|
|
102
|
-
if errors:
|
|
103
|
-
raise click.ClickException(f"Completed with {errors} error(s).")
|
|
104
|
-
|
|
105
|
-
click.echo("Done.")
|
|
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
|
{cwms_cli-0.6.0 → cwms_cli-0.7.1}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.6.0 → cwms_cli-0.7.1}/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
|
|
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
|