cwms-cli 0.1.5__tar.gz → 0.2.2__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.1.5 → cwms_cli-0.2.2}/PKG-INFO +1 -1
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/__init__.py +2 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/__main__.py +2 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/blob.py +29 -9
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/commands_cwms.py +6 -2
- cwms_cli-0.2.2/cwmscli/load/README.md +7 -0
- cwms_cli-0.2.2/cwmscli/load/__init__.py +0 -0
- cwms_cli-0.2.2/cwmscli/load/__main__.py +5 -0
- cwms_cli-0.2.2/cwmscli/load/location/location.py +131 -0
- cwms_cli-0.2.2/cwmscli/load/location/location_ids.py +83 -0
- cwms_cli-0.2.2/cwmscli/load/location/location_ids_bygroup.py +127 -0
- cwms_cli-0.2.2/cwmscli/load/root.py +123 -0
- cwms_cli-0.2.2/cwmscli/load/timeseries/timeseries.py +55 -0
- cwms_cli-0.2.2/cwmscli/load/timeseries/timeseries_ids.py +71 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/usgs/__init__.py +11 -14
- cwms_cli-0.2.2/cwmscli/usgs/__main__.py +7 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -2
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/usgs/getusgs_cda.py +0 -2
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/usgs/getusgs_measurements_cda.py +5 -5
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/usgs/rating_ini_file_import.py +0 -1
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/utils/deps.py +13 -1
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/pyproject.toml +3 -3
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/LICENSE +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/README.md +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/examples/hourly.json +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/examples/minutes.json +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/utils/terminal.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.1.5 → cwms_cli-0.2.2}/cwmscli/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from cwmscli.commands import commands_cwms
|
|
4
|
+
from cwmscli.load import __main__ as load
|
|
4
5
|
from cwmscli.usgs import usgs_group
|
|
5
6
|
|
|
6
7
|
|
|
@@ -13,3 +14,4 @@ cli.add_command(usgs_group, name="usgs")
|
|
|
13
14
|
cli.add_command(commands_cwms.shefcritimport)
|
|
14
15
|
cli.add_command(commands_cwms.csv2cwms_cmd)
|
|
15
16
|
cli.add_command(commands_cwms.blob_group)
|
|
17
|
+
cli.add_command(load.load_group)
|
|
@@ -7,10 +7,6 @@ import re
|
|
|
7
7
|
import sys
|
|
8
8
|
from typing import Optional, Sequence
|
|
9
9
|
|
|
10
|
-
import cwms
|
|
11
|
-
import pandas as pd
|
|
12
|
-
import requests
|
|
13
|
-
|
|
14
10
|
from cwmscli.utils import get_api_key
|
|
15
11
|
from cwmscli.utils.deps import requires
|
|
16
12
|
|
|
@@ -90,6 +86,9 @@ def _save_base64(
|
|
|
90
86
|
|
|
91
87
|
|
|
92
88
|
def store_blob(**kwargs):
|
|
89
|
+
import cwms
|
|
90
|
+
import requests
|
|
91
|
+
|
|
93
92
|
file_data = kwargs.get("file_data")
|
|
94
93
|
blob_id = kwargs.get("blob_id", "").upper()
|
|
95
94
|
# Attempt to determine what media type should be used for the mime-type if one is not presented based on the file extension
|
|
@@ -145,6 +144,9 @@ def store_blob(**kwargs):
|
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
def retrieve_blob(**kwargs):
|
|
147
|
+
import cwms
|
|
148
|
+
import requests
|
|
149
|
+
|
|
148
150
|
blob_id = kwargs.get("blob_id", "").upper()
|
|
149
151
|
if not blob_id:
|
|
150
152
|
logging.warning(
|
|
@@ -172,14 +174,17 @@ def retrieve_blob(**kwargs):
|
|
|
172
174
|
|
|
173
175
|
|
|
174
176
|
def delete_blob(**kwargs):
|
|
177
|
+
import cwms
|
|
178
|
+
import requests
|
|
179
|
+
|
|
175
180
|
blob_id = kwargs.get("blob_id").upper()
|
|
176
181
|
logging.debug(f"Office: {kwargs.get('office')} Blob ID: {blob_id}")
|
|
177
182
|
|
|
178
183
|
try:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
cwms.delete_blob(
|
|
185
|
+
office_id=kwargs.get("office"),
|
|
186
|
+
blob_id=kwargs.get("blob_id").upper(),
|
|
187
|
+
)
|
|
183
188
|
logging.info(f"Successfully deleted blob with ID: {blob_id}")
|
|
184
189
|
except requests.HTTPError as e:
|
|
185
190
|
details = getattr(e.response, "text", "") or str(e)
|
|
@@ -197,8 +202,11 @@ def list_blobs(
|
|
|
197
202
|
sort_by: Optional[Sequence[str]] = None,
|
|
198
203
|
ascending: bool = True,
|
|
199
204
|
limit: Optional[int] = None,
|
|
200
|
-
)
|
|
205
|
+
):
|
|
201
206
|
logging.info(f"Listing blobs for office: {office!r}...")
|
|
207
|
+
import cwms
|
|
208
|
+
import pandas as pd
|
|
209
|
+
|
|
202
210
|
result = cwms.get_blobs(office_id=office, blob_id_like=blob_id_like)
|
|
203
211
|
|
|
204
212
|
# Accept either a DataFrame or a JSON/dict-like response
|
|
@@ -250,6 +258,9 @@ def upload_cmd(
|
|
|
250
258
|
api_root: str,
|
|
251
259
|
api_key: str,
|
|
252
260
|
):
|
|
261
|
+
import cwms
|
|
262
|
+
import requests
|
|
263
|
+
|
|
253
264
|
cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, ""))
|
|
254
265
|
try:
|
|
255
266
|
file_size = os.path.getsize(input_file)
|
|
@@ -307,6 +318,9 @@ def upload_cmd(
|
|
|
307
318
|
def download_cmd(
|
|
308
319
|
blob_id: str, dest: str, office: str, api_root: str, api_key: str, dry_run: bool
|
|
309
320
|
):
|
|
321
|
+
import cwms
|
|
322
|
+
import requests
|
|
323
|
+
|
|
310
324
|
if dry_run:
|
|
311
325
|
logging.info(
|
|
312
326
|
f"DRY RUN: would GET {api_root} blob with blob-id={blob_id} office={office}."
|
|
@@ -331,6 +345,7 @@ def download_cmd(
|
|
|
331
345
|
|
|
332
346
|
|
|
333
347
|
def delete_cmd(blob_id: str, office: str, api_root: str, api_key: str, dry_run: bool):
|
|
348
|
+
import cwms
|
|
334
349
|
|
|
335
350
|
if dry_run:
|
|
336
351
|
logging.info(
|
|
@@ -353,6 +368,8 @@ def update_cmd(
|
|
|
353
368
|
api_root: str,
|
|
354
369
|
api_key: str,
|
|
355
370
|
):
|
|
371
|
+
import cwms
|
|
372
|
+
|
|
356
373
|
if dry_run:
|
|
357
374
|
logging.info(
|
|
358
375
|
f"DRY RUN: would PATCH {api_root} blob with blob-id={blob_id} office={office}"
|
|
@@ -399,6 +416,9 @@ def list_cmd(
|
|
|
399
416
|
api_root: str,
|
|
400
417
|
api_key: str,
|
|
401
418
|
):
|
|
419
|
+
import cwms
|
|
420
|
+
import pandas as pd
|
|
421
|
+
|
|
402
422
|
cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
|
|
403
423
|
df = list_blobs(
|
|
404
424
|
office=office,
|
|
@@ -107,7 +107,6 @@ def csv2cwms_cmd(**kwargs):
|
|
|
107
107
|
"""
|
|
108
108
|
),
|
|
109
109
|
)
|
|
110
|
-
@requires(reqs.cwms)
|
|
111
110
|
def blob_group():
|
|
112
111
|
pass
|
|
113
112
|
|
|
@@ -130,13 +129,14 @@ def blob_group():
|
|
|
130
129
|
help="Override media type (guessed from file if omitted).",
|
|
131
130
|
)
|
|
132
131
|
@click.option(
|
|
133
|
-
"--overwrite
|
|
132
|
+
"--overwrite",
|
|
134
133
|
default=False,
|
|
135
134
|
show_default=True,
|
|
136
135
|
help="If true, replace existing blob.",
|
|
137
136
|
)
|
|
138
137
|
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
139
138
|
@common_api_options
|
|
139
|
+
@requires(reqs.cwms)
|
|
140
140
|
def blob_upload(**kwargs):
|
|
141
141
|
from cwmscli.commands.blob import upload_cmd
|
|
142
142
|
|
|
@@ -156,6 +156,7 @@ def blob_upload(**kwargs):
|
|
|
156
156
|
)
|
|
157
157
|
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
158
158
|
@common_api_options
|
|
159
|
+
@requires(reqs.cwms)
|
|
159
160
|
def blob_download(**kwargs):
|
|
160
161
|
from cwmscli.commands.blob import download_cmd
|
|
161
162
|
|
|
@@ -169,6 +170,7 @@ def blob_download(**kwargs):
|
|
|
169
170
|
@click.option("--blob-id", required=True, type=str, help="Blob ID to delete.")
|
|
170
171
|
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
171
172
|
@common_api_options
|
|
173
|
+
@requires(reqs.cwms)
|
|
172
174
|
def delete_cmd(**kwargs):
|
|
173
175
|
from cwmscli.commands.blob import delete_cmd
|
|
174
176
|
|
|
@@ -204,6 +206,7 @@ def delete_cmd(**kwargs):
|
|
|
204
206
|
help="If true, replace existing blob.",
|
|
205
207
|
)
|
|
206
208
|
@common_api_options
|
|
209
|
+
@requires(reqs.cwms)
|
|
207
210
|
def update_cmd(**kwargs):
|
|
208
211
|
from cwmscli.commands.blob import update_cmd
|
|
209
212
|
|
|
@@ -243,6 +246,7 @@ def update_cmd(**kwargs):
|
|
|
243
246
|
help="If set, write results to this CSV file.",
|
|
244
247
|
)
|
|
245
248
|
@common_api_options
|
|
249
|
+
@requires(reqs.cwms)
|
|
246
250
|
def list_cmd(**kwargs):
|
|
247
251
|
from cwmscli.commands.blob import list_cmd
|
|
248
252
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Loader Scripts
|
|
2
|
+
|
|
3
|
+
This command supports loading data in some form or fashion.
|
|
4
|
+
|
|
5
|
+
Initially it was intended to load data across from one CDA instance (GET) to another (POST). But could be expanded to other forms of loading including backloading jobs that potentially wrap existing other commands.
|
|
6
|
+
|
|
7
|
+
It differs from the other scripts in that it is effectively a `CDA2CDA` script which is usually only needed for `loading` data in initial setups. (Dev/Docker Compose)
|
|
File without changes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# cwmscli/load/__main__.py
|
|
2
|
+
# Side-effect imports to register subcommands under `load_group`
|
|
3
|
+
from cwmscli.load.location import location as _locations
|
|
4
|
+
from cwmscli.load.root import load_group # export for callers
|
|
5
|
+
from cwmscli.load.timeseries import timeseries as _timeseries
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# cwmscli/load/locations.py
|
|
2
|
+
from typing import Iterable, Optional
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from cwmscli import requirements as reqs
|
|
7
|
+
from cwmscli.load.root import (
|
|
8
|
+
load_group,
|
|
9
|
+
shared_source_target_options,
|
|
10
|
+
validate_cda_targets,
|
|
11
|
+
)
|
|
12
|
+
from cwmscli.utils.deps import requires
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@load_group.group(
|
|
16
|
+
"location", help="Copy location data from a source CDA to a target CDA."
|
|
17
|
+
)
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def location(ctx):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@location.command(
|
|
24
|
+
"ids-all",
|
|
25
|
+
help="Copy ALL locations from a source CDA to a target CDA.",
|
|
26
|
+
)
|
|
27
|
+
@shared_source_target_options
|
|
28
|
+
@click.option(
|
|
29
|
+
"--like",
|
|
30
|
+
default=None,
|
|
31
|
+
type=str,
|
|
32
|
+
help="LIKE filter for location name (e.g. 'Turbine*').",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--location-kind-like",
|
|
36
|
+
"location_kind_like",
|
|
37
|
+
default=["ALL"],
|
|
38
|
+
multiple=True,
|
|
39
|
+
help=(
|
|
40
|
+
"Filter by LOCATION_KIND using LIKE; may be passed multiple times.\n\n"
|
|
41
|
+
"Default is to pull all Location kinds.\n\n"
|
|
42
|
+
"Common kinds: SITE, EMBANKMENT, OVERFLOW, TURBINE, STREAM, PROJECT, "
|
|
43
|
+
"STREAMGAGE, BASIN, OUTLET, LOCK, GATE.\n\n"
|
|
44
|
+
"Examples:\n"
|
|
45
|
+
" --location-kind-like PROJECT --location-kind-like STREAM\n"
|
|
46
|
+
" --location-kind-like '(SITE|STREAM)' # Posix regex"
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
@requires(reqs.cwms)
|
|
50
|
+
@validate_cda_targets
|
|
51
|
+
def load_locations(
|
|
52
|
+
source_cda: str,
|
|
53
|
+
source_office: str,
|
|
54
|
+
target_cda: str,
|
|
55
|
+
target_api_key: Optional[str],
|
|
56
|
+
verbose: int,
|
|
57
|
+
dry_run: bool,
|
|
58
|
+
like: Optional[str],
|
|
59
|
+
location_kind_like: Optional[Iterable[str]] = None,
|
|
60
|
+
):
|
|
61
|
+
from cwmscli.load.location.location_ids import load_locations as _load_locations
|
|
62
|
+
|
|
63
|
+
_load_locations(
|
|
64
|
+
source_cda=source_cda,
|
|
65
|
+
source_office=source_office,
|
|
66
|
+
target_cda=target_cda,
|
|
67
|
+
target_api_key=target_api_key,
|
|
68
|
+
verbose=verbose,
|
|
69
|
+
dry_run=dry_run,
|
|
70
|
+
like=like,
|
|
71
|
+
location_kind_like=location_kind_like,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@location.command(
|
|
76
|
+
"ids-bygroup",
|
|
77
|
+
help="Copy locations from a CWMS Location Group (source CDA) to a target CDA.",
|
|
78
|
+
)
|
|
79
|
+
@shared_source_target_options
|
|
80
|
+
@click.option(
|
|
81
|
+
"--group-id", required=True, help="Location Group ID (e.g., 'Ark Basin')."
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--category-id", required=True, help="Location Category ID (e.g., 'Basin')."
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--group-office-id",
|
|
88
|
+
default=None,
|
|
89
|
+
help="Owning office of the Location Group (defaults to --source-office).",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--category-office-id",
|
|
93
|
+
default=None,
|
|
94
|
+
help="Owning office of the Category (defaults to --source-office).",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--filter-office/--no-filter-office",
|
|
98
|
+
default=True,
|
|
99
|
+
show_default=True,
|
|
100
|
+
help="If set, only copy members whose 'office-id' equals --source-office.",
|
|
101
|
+
)
|
|
102
|
+
@requires(reqs.cwms)
|
|
103
|
+
@validate_cda_targets
|
|
104
|
+
def load_locations_from_group(
|
|
105
|
+
source_cda: str,
|
|
106
|
+
source_office: str,
|
|
107
|
+
target_cda: str,
|
|
108
|
+
target_api_key: Optional[str],
|
|
109
|
+
verbose: int,
|
|
110
|
+
group_id: str,
|
|
111
|
+
category_id: str,
|
|
112
|
+
group_office_id: Optional[str],
|
|
113
|
+
category_office_id: Optional[str],
|
|
114
|
+
filter_office: bool,
|
|
115
|
+
dry_run: bool,
|
|
116
|
+
):
|
|
117
|
+
from cwmscli.load.location.location_ids_bygroup import copy_from_group
|
|
118
|
+
|
|
119
|
+
copy_from_group(
|
|
120
|
+
source_cda=source_cda,
|
|
121
|
+
source_office=source_office,
|
|
122
|
+
target_cda=target_cda,
|
|
123
|
+
target_api_key=target_api_key,
|
|
124
|
+
verbose=verbose,
|
|
125
|
+
group_id=group_id,
|
|
126
|
+
category_id=category_id,
|
|
127
|
+
group_office_id=group_office_id,
|
|
128
|
+
category_office_id=category_office_id,
|
|
129
|
+
filter_office=filter_office,
|
|
130
|
+
dry_run=dry_run,
|
|
131
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Iterable, Optional
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import cwms
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_locations(
|
|
8
|
+
source_cda: str,
|
|
9
|
+
source_office: str,
|
|
10
|
+
target_cda: str,
|
|
11
|
+
target_api_key: Optional[str],
|
|
12
|
+
verbose: int,
|
|
13
|
+
dry_run: bool,
|
|
14
|
+
like: Optional[str],
|
|
15
|
+
location_kind_like: Optional[Iterable[str]] = "ALL",
|
|
16
|
+
):
|
|
17
|
+
if verbose:
|
|
18
|
+
click.echo(
|
|
19
|
+
f"[load locations] source={source_cda} ({source_office}) -> target={target_cda}"
|
|
20
|
+
)
|
|
21
|
+
click.echo(
|
|
22
|
+
f" like={like or '-'} kinds={list(location_kind_like) or '-'} dry_run={dry_run}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
cwms.init_session(api_root=source_cda, api_key=None)
|
|
26
|
+
|
|
27
|
+
cat_kwargs = {"office_id": source_office}
|
|
28
|
+
if like:
|
|
29
|
+
cat_kwargs["like"] = like
|
|
30
|
+
kinds = list(location_kind_like) if location_kind_like else [None]
|
|
31
|
+
|
|
32
|
+
locations = []
|
|
33
|
+
|
|
34
|
+
if "ALL" in kinds:
|
|
35
|
+
locations = cwms.get_locations(office_id=source_office).json
|
|
36
|
+
else:
|
|
37
|
+
locations = []
|
|
38
|
+
for kind in kinds:
|
|
39
|
+
cat_kwargs_k = dict(cat_kwargs)
|
|
40
|
+
if kind != "ALL":
|
|
41
|
+
cat_kwargs_k["location_kind_like"] = kind
|
|
42
|
+
|
|
43
|
+
if verbose >= 2:
|
|
44
|
+
click.echo(f" > catalog query: {cat_kwargs_k}")
|
|
45
|
+
|
|
46
|
+
resp = cwms.get_locations_catalog(**cat_kwargs_k)
|
|
47
|
+
if resp.df.empty:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
loc_ids = resp.df["name"].tolist()
|
|
51
|
+
locations_resp = cwms.get_locations(
|
|
52
|
+
office_id=source_office, location_ids=loc_ids
|
|
53
|
+
)
|
|
54
|
+
locations.extend(locations_resp.json or [])
|
|
55
|
+
|
|
56
|
+
if verbose:
|
|
57
|
+
click.echo(f"Fetched {len(locations)} locations from source")
|
|
58
|
+
|
|
59
|
+
if dry_run:
|
|
60
|
+
for loc in locations:
|
|
61
|
+
click.echo(
|
|
62
|
+
f"[dry-run] would store Location(name={loc['name']}) to {target_cda} ({source_office})"
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# init target once
|
|
67
|
+
cwms.init_session(api_root=target_cda, api_key=target_api_key)
|
|
68
|
+
|
|
69
|
+
errors = 0
|
|
70
|
+
for loc in locations:
|
|
71
|
+
try:
|
|
72
|
+
if loc["active"] is True:
|
|
73
|
+
result = cwms.store_location(data=loc, fail_if_exists=False)
|
|
74
|
+
if verbose:
|
|
75
|
+
click.echo(result)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
errors += 1
|
|
78
|
+
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
79
|
+
|
|
80
|
+
if errors:
|
|
81
|
+
raise click.ClickException(f"Completed with {errors} error(s).")
|
|
82
|
+
if verbose:
|
|
83
|
+
click.echo("Done.")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# cwmscli/load/location_group.py
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import cwms
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def exact_or_regex(ids: list[str]) -> str:
|
|
10
|
+
if not ids:
|
|
11
|
+
return r"^$"
|
|
12
|
+
if len(ids) == 1:
|
|
13
|
+
return rf"^{re.escape(ids[0])}$"
|
|
14
|
+
return r"^(?:" + "|".join(re.escape(x) for x in ids) + r")$"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def copy_from_group(
|
|
18
|
+
source_cda: str,
|
|
19
|
+
source_office: str,
|
|
20
|
+
target_cda: str,
|
|
21
|
+
target_api_key: Optional[str],
|
|
22
|
+
verbose: int,
|
|
23
|
+
group_id: str,
|
|
24
|
+
category_id: str,
|
|
25
|
+
group_office_id: Optional[str],
|
|
26
|
+
category_office_id: Optional[str],
|
|
27
|
+
filter_office: bool,
|
|
28
|
+
dry_run: bool,
|
|
29
|
+
):
|
|
30
|
+
group_office_id = group_office_id or source_office
|
|
31
|
+
category_office_id = category_office_id or source_office
|
|
32
|
+
|
|
33
|
+
if verbose:
|
|
34
|
+
click.echo(
|
|
35
|
+
f"[load location group] source={source_cda} ({source_office}) -> target={target_cda})"
|
|
36
|
+
)
|
|
37
|
+
click.echo(
|
|
38
|
+
f" group={group_id} category={category_id} "
|
|
39
|
+
f"group_office={group_office_id} category_office={category_office_id} "
|
|
40
|
+
f"filter_office={filter_office} dry_run={dry_run}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
cwms.init_session(api_root=source_cda, api_key=None)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
grp = cwms.get_location_group(
|
|
47
|
+
loc_group_id=group_id,
|
|
48
|
+
category_id=category_id,
|
|
49
|
+
office_id=source_office,
|
|
50
|
+
group_office_id=group_office_id,
|
|
51
|
+
category_office_id=category_office_id,
|
|
52
|
+
)
|
|
53
|
+
if verbose:
|
|
54
|
+
click.echo(f"Fetched Location Group '{group_id}' from source:")
|
|
55
|
+
if hasattr(grp, "df"):
|
|
56
|
+
click.echo(grp.df)
|
|
57
|
+
else:
|
|
58
|
+
click.echo(grp.json)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise click.ClickException(
|
|
61
|
+
f"Failed to read location group '{group_id}' in category '{category_id}': {e}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
df = getattr(grp, "df", None)
|
|
65
|
+
if df is None or df.empty:
|
|
66
|
+
click.echo("No members found in the specified location group.")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if filter_office and "office-id" in df.columns:
|
|
70
|
+
df = df[df["office-id"] == source_office].copy()
|
|
71
|
+
|
|
72
|
+
member_ids = sorted(df["location-id"].dropna().unique().tolist())
|
|
73
|
+
if verbose:
|
|
74
|
+
click.echo(f"Group members found: {len(member_ids)}")
|
|
75
|
+
if not member_ids:
|
|
76
|
+
click.echo("No valid location IDs to copy.")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
locations = []
|
|
81
|
+
BATCH = 200 # optional batching
|
|
82
|
+
for batch in (
|
|
83
|
+
member_ids[i : i + BATCH] for i in range(0, len(member_ids), BATCH)
|
|
84
|
+
):
|
|
85
|
+
pattern = exact_or_regex(batch)
|
|
86
|
+
resp = cwms.get_locations(office_id=source_office, location_ids=pattern)
|
|
87
|
+
if verbose and getattr(resp, "df", None) is not None:
|
|
88
|
+
click.echo(f"Fetched {len(resp.df)} matching Locations in batch")
|
|
89
|
+
if resp and resp.json:
|
|
90
|
+
locations.extend(resp.json)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise click.ClickException(f"Failed to fetch locations from source: {e}")
|
|
94
|
+
|
|
95
|
+
if verbose:
|
|
96
|
+
click.echo(f"Fetched {len(locations)} Location objects from source")
|
|
97
|
+
|
|
98
|
+
if dry_run:
|
|
99
|
+
for loc in locations:
|
|
100
|
+
click.echo(
|
|
101
|
+
f"[dry-run] would store Location(name={loc.name}) to {target_cda} ({source_office})"
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
cwms.init_session(api_root=target_cda, api_key=target_api_key)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise click.ClickException(f"Failed to init target session: {e}")
|
|
109
|
+
|
|
110
|
+
errors = 0
|
|
111
|
+
for loc in locations:
|
|
112
|
+
try:
|
|
113
|
+
if verbose:
|
|
114
|
+
click.echo(f"Store: {loc['name']}")
|
|
115
|
+
cwms.store_location(data=loc, fail_if_exists=False)
|
|
116
|
+
if verbose:
|
|
117
|
+
click.echo("\tStored successfully.")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
errors += 1
|
|
120
|
+
click.echo(f"Error storing location {loc}: \n\t{e}", err=True)
|
|
121
|
+
|
|
122
|
+
click.echo(
|
|
123
|
+
f"Successfully stored {len(locations) - errors} / {len(locations)} locations."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if errors:
|
|
127
|
+
raise click.ClickException(f"Completed with {errors} error(s).")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from cwmscli import requirements as reqs
|
|
11
|
+
from cwmscli.utils.deps import requires
|
|
12
|
+
|
|
13
|
+
CONTEXT = dict(
|
|
14
|
+
help_option_names=["-h", "--help"],
|
|
15
|
+
max_content_width=160,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CdaEndpoints:
|
|
21
|
+
source_cda: str
|
|
22
|
+
source_office: str
|
|
23
|
+
target_cda: str
|
|
24
|
+
target_office: str
|
|
25
|
+
target_api_key: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_url(u: str) -> str:
|
|
29
|
+
if not u:
|
|
30
|
+
return ""
|
|
31
|
+
p = urlparse(u)
|
|
32
|
+
path = (p.path or "").rstrip("/")
|
|
33
|
+
return f"{p.scheme.lower()}://{p.netloc.lower()}{path}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _norm_office(o: Optional[str]) -> str:
|
|
37
|
+
return (o or "").strip().upper()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_cda_targets(func):
|
|
41
|
+
@functools.wraps(func)
|
|
42
|
+
def wrapper(*args, **kwargs):
|
|
43
|
+
source_cda = _normalize_url(kwargs.get("source_cda"))
|
|
44
|
+
target_cda = _normalize_url(kwargs.get("target_cda"))
|
|
45
|
+
source_office = _norm_office(kwargs.get("source_office"))
|
|
46
|
+
target_office = _norm_office(kwargs.get("target_office"))
|
|
47
|
+
|
|
48
|
+
same_root = source_cda == target_cda and bool(source_cda)
|
|
49
|
+
same_office = source_office == target_office and bool(source_office)
|
|
50
|
+
|
|
51
|
+
if same_root and same_office:
|
|
52
|
+
raise click.ClickException(
|
|
53
|
+
"Circular reference detected: source and target CDA endpoints "
|
|
54
|
+
"are identical (URL + office). This would read-from and write-to "
|
|
55
|
+
"the same system.\n\nChange the source or target CDA URL or office. "
|
|
56
|
+
"Type cwms-cli load --help for arg options."
|
|
57
|
+
)
|
|
58
|
+
elif same_root and not same_office:
|
|
59
|
+
click.secho(
|
|
60
|
+
"Warning: source and target use the same CDA root URL but different offices. "
|
|
61
|
+
"This is allowed, but double-check intent.",
|
|
62
|
+
fg="yellow",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
click.secho(
|
|
66
|
+
f"Source: {source_cda} (office={source_office or '-'})\n"
|
|
67
|
+
f"Target: {target_cda} (office={source_office or '-'})",
|
|
68
|
+
fg="green" if not same_root else "yellow",
|
|
69
|
+
)
|
|
70
|
+
return func(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def shared_source_target_options(f):
|
|
76
|
+
f = click.option(
|
|
77
|
+
"--source-cda",
|
|
78
|
+
envvar="CDA_SOURCE_URL",
|
|
79
|
+
required=True,
|
|
80
|
+
default="https://cwms-data.usace.army.mil/cwms-data/",
|
|
81
|
+
help="Source CWMS Data API root. Default: https://cwms-data.usace.army.mil/cwms-data/",
|
|
82
|
+
)(f)
|
|
83
|
+
f = click.option(
|
|
84
|
+
"--source-office",
|
|
85
|
+
envvar="CDA_SOURCE_OFFICE",
|
|
86
|
+
required=True,
|
|
87
|
+
help="Source office ID (e.g. SWT, SWL).",
|
|
88
|
+
)(f)
|
|
89
|
+
f = click.option(
|
|
90
|
+
"--target-cda",
|
|
91
|
+
envvar="CDA_TARGET_URL",
|
|
92
|
+
required=True,
|
|
93
|
+
default="http://localhost:8081/cwms-data/",
|
|
94
|
+
help="Target CWMS Data API root. Default: http://localhost:8081/cwms-data/",
|
|
95
|
+
)(f)
|
|
96
|
+
f = click.option(
|
|
97
|
+
"--target-api-key",
|
|
98
|
+
envvar="CDA_API_KEY",
|
|
99
|
+
help="Target API key (if required by the target CDA).",
|
|
100
|
+
)(f)
|
|
101
|
+
f = click.option(
|
|
102
|
+
"--dry-run/--no-dry-run",
|
|
103
|
+
is_flag=True,
|
|
104
|
+
default=False,
|
|
105
|
+
show_default=True,
|
|
106
|
+
help="Show what would be written without storing to target.",
|
|
107
|
+
)(f)
|
|
108
|
+
f = click.option(
|
|
109
|
+
"-v",
|
|
110
|
+
"--verbose",
|
|
111
|
+
count=True,
|
|
112
|
+
help="Increase verbosity (repeat for more detail).",
|
|
113
|
+
)(f)
|
|
114
|
+
return f
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@click.group(
|
|
118
|
+
name="load",
|
|
119
|
+
help="Load data from one CWMS Data API instance to another.",
|
|
120
|
+
context_settings=CONTEXT,
|
|
121
|
+
)
|
|
122
|
+
def load_group():
|
|
123
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from cwmscli import requirements as reqs
|
|
6
|
+
from cwmscli.load.root import (
|
|
7
|
+
load_group,
|
|
8
|
+
shared_source_target_options,
|
|
9
|
+
validate_cda_targets,
|
|
10
|
+
)
|
|
11
|
+
from cwmscli.utils.deps import requires
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@load_group.group(
|
|
15
|
+
"timeseries", help="Copy timeseries IDs from a source CDA to a target CDA."
|
|
16
|
+
)
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def timeseries(ctx):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@timeseries.command(
|
|
23
|
+
"ids-all",
|
|
24
|
+
help="Copy ALL timeseries IDs for locations in a target CDA from a source CDA.",
|
|
25
|
+
)
|
|
26
|
+
@shared_source_target_options
|
|
27
|
+
@click.option(
|
|
28
|
+
"--timeseries-id-regex",
|
|
29
|
+
"timeseries_id_regex",
|
|
30
|
+
default=None,
|
|
31
|
+
type=str,
|
|
32
|
+
help="regex filter for timeseries ID (e.g. 'LocID.*').",
|
|
33
|
+
)
|
|
34
|
+
@requires(reqs.cwms)
|
|
35
|
+
@validate_cda_targets
|
|
36
|
+
def load_timeseries_ids_all(
|
|
37
|
+
source_cda: str,
|
|
38
|
+
source_office: str,
|
|
39
|
+
target_cda: str,
|
|
40
|
+
target_api_key: Optional[str],
|
|
41
|
+
verbose: int,
|
|
42
|
+
timeseries_id_regex: Optional[str],
|
|
43
|
+
dry_run: bool,
|
|
44
|
+
):
|
|
45
|
+
from cwmscli.load.timeseries.timeseries_ids import load_timeseries_ids
|
|
46
|
+
|
|
47
|
+
load_timeseries_ids(
|
|
48
|
+
source_cda=source_cda,
|
|
49
|
+
source_office=source_office,
|
|
50
|
+
target_cda=target_cda,
|
|
51
|
+
target_api_key=target_api_key,
|
|
52
|
+
verbose=verbose,
|
|
53
|
+
timeseries_id_regex=timeseries_id_regex,
|
|
54
|
+
dry_run=dry_run,
|
|
55
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# cwmscli/load/timeseries_ids.py
|
|
2
|
+
from turtle import pd
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_timeseries_ids(
|
|
10
|
+
source_cda: str,
|
|
11
|
+
source_office: str,
|
|
12
|
+
target_cda: str,
|
|
13
|
+
target_api_key: Optional[str],
|
|
14
|
+
verbose: int,
|
|
15
|
+
dry_run: bool,
|
|
16
|
+
timeseries_id_regex: Optional[str] = None,
|
|
17
|
+
):
|
|
18
|
+
import cwms
|
|
19
|
+
|
|
20
|
+
if verbose:
|
|
21
|
+
click.echo(
|
|
22
|
+
f"Loading timeseries IDs from source CDA '{source_cda}' (office '{source_office}') "
|
|
23
|
+
f"to target CDA '{target_cda}'."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
cwms.init_session(api_root=source_cda, api_key=None)
|
|
27
|
+
ts_ids = cwms.get_timeseries_identifiers(
|
|
28
|
+
office_id=source_office, timeseries_id_regex=timeseries_id_regex
|
|
29
|
+
).df
|
|
30
|
+
|
|
31
|
+
cwms.init_session(api_root=target_cda, api_key=target_api_key)
|
|
32
|
+
# only grab time_ids for locations that are in the target database
|
|
33
|
+
locations = cwms.get_locations_catalog(office_id=source_office)
|
|
34
|
+
ts_ids[["location-id", "param", "type", "int", "dur", "ver"]] = ts_ids[
|
|
35
|
+
"time-series-id"
|
|
36
|
+
].str.split(".", expand=True)
|
|
37
|
+
locs = locations.df.rename(columns={"name": "location-id", "office": "office-id"})
|
|
38
|
+
ts_lo_ids = pd.merge(ts_ids, locs, how="inner", on=["location-id", "office-id"])
|
|
39
|
+
|
|
40
|
+
if verbose:
|
|
41
|
+
click.echo(f"Found {len(ts_lo_ids)} timeseries IDs to copy.")
|
|
42
|
+
|
|
43
|
+
errors = 0
|
|
44
|
+
for i, row in ts_lo_ids.iterrows():
|
|
45
|
+
ts_id = row["time-series-id"]
|
|
46
|
+
if dry_run:
|
|
47
|
+
click.echo(
|
|
48
|
+
f"[dry-run] would store Timeseries ID(name={ts_id}) to {target_cda} ({source_office})"
|
|
49
|
+
)
|
|
50
|
+
continue
|
|
51
|
+
t_id_json = {
|
|
52
|
+
"office-id": row["office-id"],
|
|
53
|
+
"time-series-id": ts_id,
|
|
54
|
+
"timezone-name": row["timezone-name"],
|
|
55
|
+
"interval-offset-minutes": float(row["interval-offset-minutes"]),
|
|
56
|
+
"active": row["active_x"],
|
|
57
|
+
}
|
|
58
|
+
try:
|
|
59
|
+
result = cwms.store_timeseries_identifier(
|
|
60
|
+
data=t_id_json, fail_if_exists=False
|
|
61
|
+
)
|
|
62
|
+
if verbose:
|
|
63
|
+
click.echo(result)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
errors += 1
|
|
66
|
+
click.echo(f"Error storing location {ts_id}: \n\t{e}", err=True)
|
|
67
|
+
|
|
68
|
+
if errors:
|
|
69
|
+
raise click.ClickException(f"Completed with {errors} error(s).")
|
|
70
|
+
if verbose:
|
|
71
|
+
click.echo("Timeseries ID copy operation completed.")
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
|
-
from cwmscli import requirements as reqs
|
|
4
|
-
from cwmscli.utils.deps import requires
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@click.group()
|
|
8
|
-
def usgs_group():
|
|
9
|
-
"""USGS utilities"""
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import click
|
|
14
|
-
|
|
15
3
|
from cwmscli import requirements as reqs
|
|
16
4
|
from cwmscli.utils import (
|
|
17
5
|
api_key_loc_option,
|
|
@@ -22,6 +10,13 @@ from cwmscli.utils import (
|
|
|
22
10
|
)
|
|
23
11
|
from cwmscli.utils.deps import requires
|
|
24
12
|
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def usgs_group():
|
|
16
|
+
"""USGS utilities"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
25
20
|
days_back_option = click.option(
|
|
26
21
|
"-d",
|
|
27
22
|
"--days-back",
|
|
@@ -123,13 +118,15 @@ def ratingsinifileimport(filename, api_root, api_key, api_key_loc):
|
|
|
123
118
|
@click.option(
|
|
124
119
|
"-d",
|
|
125
120
|
"--days-back-modified",
|
|
126
|
-
default=
|
|
121
|
+
default=2,
|
|
122
|
+
type=int,
|
|
127
123
|
help="Days back from current time measurements have been modified in USGS database. Can be integer value",
|
|
128
124
|
)
|
|
129
125
|
@click.option(
|
|
130
126
|
"-c",
|
|
131
127
|
"--days-back-collected",
|
|
132
|
-
default=
|
|
128
|
+
default=365,
|
|
129
|
+
type=int,
|
|
133
130
|
help="Days back from current time measurements have been collected. Can be integer value",
|
|
134
131
|
)
|
|
135
132
|
@office_option
|
|
@@ -17,7 +17,6 @@ def getusgs_rating_cda(
|
|
|
17
17
|
days_back: float = 1,
|
|
18
18
|
rating_subset: list = None,
|
|
19
19
|
):
|
|
20
|
-
|
|
21
20
|
api_key = "apikey " + api_key
|
|
22
21
|
cwms.api.init_session(api_root=api_root, api_key=api_key)
|
|
23
22
|
logging.info(f"CDA connection: {api_root}")
|
|
@@ -180,7 +179,6 @@ def get_begin_with_date(data, str_starts):
|
|
|
180
179
|
|
|
181
180
|
|
|
182
181
|
def get_usgs_effective_date(data, rating_type):
|
|
183
|
-
|
|
184
182
|
date_string = None
|
|
185
183
|
if rating_type == "EXSA":
|
|
186
184
|
line = data[data[0].str.startswith("# //RATING SHIFTED=")].iloc[0, 0]
|
|
@@ -14,7 +14,6 @@ def getusgs_cda(
|
|
|
14
14
|
api_key: str,
|
|
15
15
|
backfill_tsids: list = None,
|
|
16
16
|
):
|
|
17
|
-
|
|
18
17
|
api_key = "apikey " + api_key
|
|
19
18
|
cwms.api.init_session(api_root=api_root, api_key=api_key)
|
|
20
19
|
logging.info(f"CDA connection: {api_root}")
|
|
@@ -249,7 +248,6 @@ def CWMS_writeData(USGS_ts, USGS_data, USGS_data_method, days_back):
|
|
|
249
248
|
USGS_data_row = USGS_data_method.loc[USGS_Id_param]
|
|
250
249
|
if USGS_data_row is not None:
|
|
251
250
|
try:
|
|
252
|
-
|
|
253
251
|
# grab the time series values obtained from USGS API.
|
|
254
252
|
values_df = pd.DataFrame(USGS_data_row["values"])
|
|
255
253
|
if values_df.shape[0] > 1:
|
|
@@ -478,7 +478,6 @@ def check_and_drop_duplicates(df_store, df_existing):
|
|
|
478
478
|
"""
|
|
479
479
|
|
|
480
480
|
if not df_existing.empty:
|
|
481
|
-
|
|
482
481
|
# cast number columns as int, sometimes USGS won't resolve to int...drop those rows
|
|
483
482
|
df_invalid = df_store[pd.to_numeric(df_store["number"], errors="coerce").isna()]
|
|
484
483
|
if not df_invalid.empty:
|
|
@@ -603,7 +602,6 @@ def realtime_mode(DAYS_BACK_COLLECTED, DAYS_BACK_MODIFIED, measurement_site_df):
|
|
|
603
602
|
f"Fetching USGS discharge measurements from {startDT.isoformat()} (modified in last {DAYS_BACK_MODIFIED} days)..."
|
|
604
603
|
)
|
|
605
604
|
try:
|
|
606
|
-
|
|
607
605
|
df_meas_usgs, meta = nwis.get_discharge_measurements(
|
|
608
606
|
# sites=["05058000", "05059500"],
|
|
609
607
|
period=f"P{DAYS_BACK_COLLECTED}D",
|
|
@@ -829,9 +827,11 @@ def backfill_mode(BACKFILL_LIST, measurement_site_df):
|
|
|
829
827
|
logging.info(
|
|
830
828
|
"Overwrite flag is off. Filtering out any conflicting measurements"
|
|
831
829
|
)
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
830
|
+
(
|
|
831
|
+
df_store,
|
|
832
|
+
df_rejected_number,
|
|
833
|
+
df_rejected_instant,
|
|
834
|
+
) = check_and_drop_duplicates(df_meas_usgs, df_existing)
|
|
835
835
|
|
|
836
836
|
if not df_rejected_number.empty:
|
|
837
837
|
logging.info(
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
import importlib.metadata
|
|
3
|
+
import os
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
def _pip_command():
|
|
9
|
+
# Check OS to determine pip vs pip3
|
|
10
|
+
if os.name == "nt":
|
|
11
|
+
return "pip"
|
|
12
|
+
# Avoid potential issues with multiple python (2/3) versions on Unix/Linux systems
|
|
13
|
+
else:
|
|
14
|
+
return "pip3"
|
|
15
|
+
|
|
16
|
+
|
|
7
17
|
def requires(*requirements):
|
|
8
18
|
"""
|
|
9
19
|
Decorator that ensures required Python modules are installed and meet optional minimum version constraints.
|
|
@@ -84,7 +94,9 @@ def requires(*requirements):
|
|
|
84
94
|
error_lines.append("Missing module(s):")
|
|
85
95
|
for msg, _ in missing:
|
|
86
96
|
error_lines.append(msg)
|
|
87
|
-
install_cmd = "
|
|
97
|
+
install_cmd = f"{_pip_command()} install " + " ".join(
|
|
98
|
+
pkg for _, pkg in missing
|
|
99
|
+
)
|
|
88
100
|
error_lines.append(
|
|
89
101
|
f"\nInstall missing packages:\n {install_cmd}"
|
|
90
102
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "cwms-cli"
|
|
3
3
|
repository = "https://github.com/HydrologicEngineeringCenter/cwms-cli"
|
|
4
4
|
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.2.2"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
packages = [
|
|
@@ -25,7 +25,7 @@ black = "^24.2.0"
|
|
|
25
25
|
isort = "^5.13.2"
|
|
26
26
|
mypy = "^1.9.0"
|
|
27
27
|
pre-commit = "^3.6.2"
|
|
28
|
-
|
|
28
|
+
pytest = { version = "^9.0.2", python = ">=3.10" }
|
|
29
29
|
#pytest-cov = "^4.1.0"
|
|
30
30
|
#pandas-stubs = "^2.2.1.240316"
|
|
31
31
|
yamlfix = "^1.16.0"
|
|
@@ -44,4 +44,4 @@ explicit_start = false
|
|
|
44
44
|
preserve_quotes = true
|
|
45
45
|
|
|
46
46
|
[tool.poetry.scripts]
|
|
47
|
-
cwms-cli = "cwmscli.__main__:cli"
|
|
47
|
+
cwms-cli = "cwmscli.__main__:cli"
|
|
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.1.5 → cwms_cli-0.2.2}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.1.5 → cwms_cli-0.2.2}/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
|