cwms-cli 0.5.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/PKG-INFO +2 -1
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/__main__.py +2 -1
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/blob.py +47 -14
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/commands_cwms.py +282 -3
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/__main__.py +4 -2
- cwms_cli-0.5.0/cwmscli/commands/shef_critfile_import.py → cwms_cli-0.7.0/cwmscli/commands/shef/import_critfile.py +27 -3
- cwms_cli-0.7.0/cwmscli/commands/shef/import_infile.py +714 -0
- cwms_cli-0.7.0/cwmscli/commands/shef/shef_parameters.csv +272 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/users.py +2 -3
- cwms_cli-0.7.0/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/location/location.py +19 -6
- cwms_cli-0.7.0/cwmscli/load/location/location_ids.py +147 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/location/location_ids_bygroup.py +14 -4
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/root.py +73 -7
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries_ids.py +4 -2
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/requirements.py +2 -3
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/__init__.py +10 -2
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getUSGS_ratings_cda.py +3 -2
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getusgs_cda.py +2 -3
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getusgs_measurements_cda.py +3 -2
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/rating_ini_file_import.py +40 -17
- cwms_cli-0.7.0/cwmscli/utils/__init__.py +243 -0
- cwms_cli-0.7.0/cwmscli/utils/auth.py +693 -0
- cwms_cli-0.7.0/cwmscli/utils/callback_success.html +160 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/click_help.py +21 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/pyproject.toml +17 -1
- cwms_cli-0.5.0/cwmscli/load/location/location_ids.py +0 -104
- cwms_cli-0.5.0/cwmscli/utils/__init__.py +0 -151
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/LICENSE +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/README.md +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/_generated/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/_generated/ownership_data.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/clob.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.5.0/cwmscli/load → cwms_cli-0.7.0/cwmscli/commands/shef}/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries_data.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/ownership.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/colors.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/deps.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/version.py +0 -0
- {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/version_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Command line utilities for Corps Water Management Systems (CWMS) python scripts. This is a collection of shared scripts across the enterprise Water Management Enterprise System (WMES) teams.
|
|
5
5
|
License: LICENSE
|
|
6
6
|
License-File: LICENSE
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
20
20
|
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
21
|
+
Requires-Dist: requests (>=2.30.0,<3.0.0)
|
|
21
22
|
Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-cli
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
@@ -81,8 +81,9 @@ def cli(
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
cli.add_command(usgs_group, name="usgs")
|
|
84
|
-
cli.add_command(commands_cwms.
|
|
84
|
+
cli.add_command(commands_cwms.shef_group)
|
|
85
85
|
cli.add_command(commands_cwms.csv2cwms_cmd)
|
|
86
|
+
cli.add_command(commands_cwms.login_cmd)
|
|
86
87
|
cli.add_command(commands_cwms.update_cli_cmd)
|
|
87
88
|
cli.add_command(commands_cwms.blob_group)
|
|
88
89
|
cli.add_command(commands_cwms.clob_group)
|
|
@@ -9,7 +9,13 @@ import sys
|
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
from typing import Optional, Sequence, Tuple, Union
|
|
11
11
|
|
|
12
|
-
from cwmscli.utils import
|
|
12
|
+
from cwmscli.utils import (
|
|
13
|
+
colors,
|
|
14
|
+
get_api_key,
|
|
15
|
+
has_invalid_chars,
|
|
16
|
+
init_cwms_session,
|
|
17
|
+
log_scoped_read_hint,
|
|
18
|
+
)
|
|
13
19
|
from cwmscli.utils.click_help import DOCS_BASE_URL
|
|
14
20
|
from cwmscli.utils.deps import requires
|
|
15
21
|
|
|
@@ -133,11 +139,30 @@ def _resolve_optional_api_key(api_key: Optional[str], anonymous: bool) -> Option
|
|
|
133
139
|
return get_api_key(api_key, None)
|
|
134
140
|
|
|
135
141
|
|
|
142
|
+
def _resolve_credential_kind(api_key: Optional[str], anonymous: bool) -> Optional[str]:
|
|
143
|
+
if anonymous:
|
|
144
|
+
return None
|
|
145
|
+
from cwmscli.utils import get_saved_login_token
|
|
146
|
+
|
|
147
|
+
if get_saved_login_token():
|
|
148
|
+
return "token"
|
|
149
|
+
if _resolve_optional_api_key(api_key, anonymous):
|
|
150
|
+
return "api_key"
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
136
154
|
def _response_status_code(exc: BaseException) -> Optional[int]:
|
|
137
155
|
response = getattr(exc, "response", None)
|
|
138
156
|
return getattr(response, "status_code", None)
|
|
139
157
|
|
|
140
158
|
|
|
159
|
+
def _blob_endpoint_id(blob_id: str) -> tuple[str, Optional[str]]:
|
|
160
|
+
normalized = blob_id.upper()
|
|
161
|
+
if has_invalid_chars(normalized):
|
|
162
|
+
return "ignored", normalized
|
|
163
|
+
return normalized, None
|
|
164
|
+
|
|
165
|
+
|
|
141
166
|
def store_blob(**kwargs):
|
|
142
167
|
import cwms
|
|
143
168
|
import requests
|
|
@@ -411,7 +436,7 @@ def upload_cmd(
|
|
|
411
436
|
import cwms
|
|
412
437
|
import requests
|
|
413
438
|
|
|
414
|
-
cwms
|
|
439
|
+
init_cwms_session(cwms, api_root=api_root, api_key=api_key)
|
|
415
440
|
|
|
416
441
|
using_single = bool(input_file)
|
|
417
442
|
using_multi = bool(input_dir)
|
|
@@ -570,8 +595,8 @@ def download_cmd(
|
|
|
570
595
|
f"DRY RUN: would GET {api_root} blob with blob-id={blob_id} office={office}."
|
|
571
596
|
)
|
|
572
597
|
return
|
|
573
|
-
|
|
574
|
-
cwms
|
|
598
|
+
credential_kind = _resolve_credential_kind(api_key, anonymous)
|
|
599
|
+
init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
|
|
575
600
|
bid = blob_id.upper()
|
|
576
601
|
logging.debug(f"Office={office} BlobID={bid}")
|
|
577
602
|
|
|
@@ -588,7 +613,7 @@ def download_cmd(
|
|
|
588
613
|
detail = getattr(e.response, "text", "") or str(e)
|
|
589
614
|
logging.error(f"Failed to download (HTTP): {detail}")
|
|
590
615
|
log_scoped_read_hint(
|
|
591
|
-
|
|
616
|
+
credential_kind=credential_kind,
|
|
592
617
|
anonymous=anonymous,
|
|
593
618
|
office=office,
|
|
594
619
|
action="download",
|
|
@@ -598,7 +623,7 @@ def download_cmd(
|
|
|
598
623
|
except Exception as e:
|
|
599
624
|
logging.error(f"Failed to download: {e}")
|
|
600
625
|
log_scoped_read_hint(
|
|
601
|
-
|
|
626
|
+
credential_kind=credential_kind,
|
|
602
627
|
anonymous=anonymous,
|
|
603
628
|
office=office,
|
|
604
629
|
action="download",
|
|
@@ -616,19 +641,27 @@ def delete_cmd(blob_id: str, office: str, api_root: str, api_key: str, dry_run:
|
|
|
616
641
|
f"DRY RUN: would DELETE {api_root} blob with blob-id={blob_id} office={office}"
|
|
617
642
|
)
|
|
618
643
|
return
|
|
619
|
-
cwms
|
|
644
|
+
init_cwms_session(cwms, api_root=api_root, api_key=api_key)
|
|
645
|
+
bid = blob_id.upper()
|
|
646
|
+
path_id, query_id = _blob_endpoint_id(bid)
|
|
620
647
|
try:
|
|
621
|
-
|
|
648
|
+
if query_id is None:
|
|
649
|
+
cwms.delete_blob(office_id=office, blob_id=bid)
|
|
650
|
+
else:
|
|
651
|
+
cwms.api.delete(
|
|
652
|
+
f"blobs/{path_id}",
|
|
653
|
+
params={"office": office, "blob-id": query_id},
|
|
654
|
+
)
|
|
622
655
|
except requests.HTTPError as e:
|
|
623
656
|
if _response_status_code(e) == 404:
|
|
624
657
|
logging.info(
|
|
625
658
|
"Blob %s was already absent in office %s. Nothing to delete.",
|
|
626
|
-
|
|
659
|
+
bid,
|
|
627
660
|
office,
|
|
628
661
|
)
|
|
629
662
|
return
|
|
630
663
|
raise
|
|
631
|
-
logging.info(f"Deleted blob: {
|
|
664
|
+
logging.info(f"Deleted blob: {bid} for office: {office}")
|
|
632
665
|
|
|
633
666
|
|
|
634
667
|
def update_cmd(
|
|
@@ -649,7 +682,7 @@ def update_cmd(
|
|
|
649
682
|
f"DRY RUN: would PATCH {api_root} blob with blob-id={blob_id} office={office}"
|
|
650
683
|
)
|
|
651
684
|
return
|
|
652
|
-
cwms
|
|
685
|
+
init_cwms_session(cwms, api_root=api_root, api_key=api_key)
|
|
653
686
|
file_data = None
|
|
654
687
|
if input_file:
|
|
655
688
|
try:
|
|
@@ -695,8 +728,8 @@ def list_cmd(
|
|
|
695
728
|
import cwms
|
|
696
729
|
import pandas as pd
|
|
697
730
|
|
|
698
|
-
|
|
699
|
-
cwms
|
|
731
|
+
credential_kind = _resolve_credential_kind(api_key, anonymous)
|
|
732
|
+
init_cwms_session(cwms, api_root=api_root, api_key=api_key, anonymous=anonymous)
|
|
700
733
|
try:
|
|
701
734
|
df = list_blobs(
|
|
702
735
|
office=office,
|
|
@@ -709,7 +742,7 @@ def list_cmd(
|
|
|
709
742
|
)
|
|
710
743
|
except Exception:
|
|
711
744
|
log_scoped_read_hint(
|
|
712
|
-
|
|
745
|
+
credential_kind=credential_kind,
|
|
713
746
|
anonymous=anonymous,
|
|
714
747
|
office=office,
|
|
715
748
|
action="list",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
7
9
|
import click
|
|
@@ -20,6 +22,7 @@ from cwmscli.utils import (
|
|
|
20
22
|
office_option_notrequired,
|
|
21
23
|
to_uppercase,
|
|
22
24
|
)
|
|
25
|
+
from cwmscli.utils.auth import DEFAULT_REDIRECT_HOST, DEFAULT_REDIRECT_PORT
|
|
23
26
|
from cwmscli.utils.deps import requires
|
|
24
27
|
from cwmscli.utils.update import (
|
|
25
28
|
build_update_package_spec,
|
|
@@ -30,7 +33,224 @@ from cwmscli.utils.version import get_cwms_cli_version
|
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
@click.command(
|
|
33
|
-
"
|
|
36
|
+
"login",
|
|
37
|
+
help="Authenticate with CWBI OIDC using PKCE and save tokens for reuse.",
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--provider",
|
|
41
|
+
type=click.Choice(["federation-eams", "login.gov"], case_sensitive=False),
|
|
42
|
+
default="federation-eams",
|
|
43
|
+
show_default=True,
|
|
44
|
+
help="Identity provider hint to send to Keycloak.",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--client-id",
|
|
48
|
+
default="cwms",
|
|
49
|
+
show_default=True,
|
|
50
|
+
help="OIDC client ID.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"-a",
|
|
54
|
+
"--api-root",
|
|
55
|
+
default="https://cwms-data.usace.army.mil/cwms-data",
|
|
56
|
+
envvar="CDA_API_ROOT",
|
|
57
|
+
show_default=True,
|
|
58
|
+
help="CDA API root used to discover the OpenID Connect configuration.",
|
|
59
|
+
)
|
|
60
|
+
@click.option(
|
|
61
|
+
"--oidc-base-url",
|
|
62
|
+
default=None,
|
|
63
|
+
show_default=True,
|
|
64
|
+
hidden=True,
|
|
65
|
+
help="Override the discovered OIDC realm base URL ending in /protocol/openid-connect.",
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--scope",
|
|
69
|
+
default="openid profile",
|
|
70
|
+
show_default=True,
|
|
71
|
+
help="OIDC scopes to request.",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--redirect-host",
|
|
75
|
+
default=DEFAULT_REDIRECT_HOST,
|
|
76
|
+
show_default=True,
|
|
77
|
+
help="Local host for the login callback listener.",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--redirect-port",
|
|
81
|
+
default=DEFAULT_REDIRECT_PORT,
|
|
82
|
+
type=int,
|
|
83
|
+
show_default=True,
|
|
84
|
+
help="Local port for the login callback listener.",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--token-file",
|
|
88
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
89
|
+
default=None,
|
|
90
|
+
help="Path to save the login session JSON. Defaults to a provider-specific file under ~/.config/cwms-cli/auth/.",
|
|
91
|
+
)
|
|
92
|
+
@click.option(
|
|
93
|
+
"--refresh",
|
|
94
|
+
"refresh_only",
|
|
95
|
+
is_flag=True,
|
|
96
|
+
default=False,
|
|
97
|
+
help="Refresh an existing saved session instead of opening a new browser login.",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--no-browser",
|
|
101
|
+
is_flag=True,
|
|
102
|
+
default=False,
|
|
103
|
+
help="Print the authorization URL instead of trying to open a browser automatically.",
|
|
104
|
+
)
|
|
105
|
+
@click.option(
|
|
106
|
+
"--timeout",
|
|
107
|
+
default=30,
|
|
108
|
+
type=int,
|
|
109
|
+
show_default=True,
|
|
110
|
+
help="Seconds to wait for the local login callback.",
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--ca-bundle",
|
|
114
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path),
|
|
115
|
+
default=None,
|
|
116
|
+
help="CA bundle to use for TLS verification.",
|
|
117
|
+
)
|
|
118
|
+
@requires(reqs.requests)
|
|
119
|
+
def login_cmd(
|
|
120
|
+
provider: str,
|
|
121
|
+
client_id: str,
|
|
122
|
+
api_root: str,
|
|
123
|
+
oidc_base_url: Optional[str],
|
|
124
|
+
scope: str,
|
|
125
|
+
redirect_host: str,
|
|
126
|
+
redirect_port: int,
|
|
127
|
+
token_file: Path,
|
|
128
|
+
refresh_only: bool,
|
|
129
|
+
no_browser: bool,
|
|
130
|
+
timeout: int,
|
|
131
|
+
ca_bundle: Path,
|
|
132
|
+
):
|
|
133
|
+
from cwmscli.utils.auth import (
|
|
134
|
+
DEFAULT_CDA_API_ROOT,
|
|
135
|
+
AuthError,
|
|
136
|
+
CallbackBindError,
|
|
137
|
+
LoginTimeoutError,
|
|
138
|
+
OIDCLoginConfig,
|
|
139
|
+
default_token_file,
|
|
140
|
+
discover_oidc_base_url,
|
|
141
|
+
discover_oidc_configuration,
|
|
142
|
+
login_with_browser,
|
|
143
|
+
refresh_saved_login,
|
|
144
|
+
refresh_token_expiry_text,
|
|
145
|
+
save_login,
|
|
146
|
+
token_expiry_text,
|
|
147
|
+
)
|
|
148
|
+
from cwmscli.utils.colors import c, err
|
|
149
|
+
|
|
150
|
+
provider = provider.lower()
|
|
151
|
+
token_file = token_file or default_token_file(provider)
|
|
152
|
+
verify = str(ca_bundle) if ca_bundle else None
|
|
153
|
+
api_root = (api_root or DEFAULT_CDA_API_ROOT).rstrip("/")
|
|
154
|
+
action = (
|
|
155
|
+
"refreshed your saved sign-in for" if refresh_only else "authenticated against"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
if refresh_only:
|
|
160
|
+
result = refresh_saved_login(token_file=token_file, verify=verify)
|
|
161
|
+
config = result["config"]
|
|
162
|
+
token = result["token"]
|
|
163
|
+
else:
|
|
164
|
+
discovered_oidc = (
|
|
165
|
+
{
|
|
166
|
+
"oidc_base_url": oidc_base_url.rstrip("/"),
|
|
167
|
+
"authorization_endpoint": f"{oidc_base_url.rstrip('/')}/auth",
|
|
168
|
+
"token_endpoint": f"{oidc_base_url.rstrip('/')}/token",
|
|
169
|
+
}
|
|
170
|
+
if oidc_base_url
|
|
171
|
+
else discover_oidc_configuration(
|
|
172
|
+
api_root=api_root,
|
|
173
|
+
verify=verify,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
config = OIDCLoginConfig(
|
|
177
|
+
client_id=client_id,
|
|
178
|
+
oidc_base_url=discovered_oidc["oidc_base_url"].rstrip("/"),
|
|
179
|
+
authorization_endpoint_url=discovered_oidc["authorization_endpoint"],
|
|
180
|
+
token_endpoint_url=discovered_oidc["token_endpoint"],
|
|
181
|
+
redirect_host=redirect_host,
|
|
182
|
+
redirect_port=redirect_port,
|
|
183
|
+
scope=scope,
|
|
184
|
+
provider=provider,
|
|
185
|
+
timeout_seconds=timeout,
|
|
186
|
+
verify=verify,
|
|
187
|
+
)
|
|
188
|
+
auth_url_shown = False
|
|
189
|
+
|
|
190
|
+
def show_auth_url(url: str) -> None:
|
|
191
|
+
nonlocal auth_url_shown
|
|
192
|
+
click.echo("Visit this URL to authenticate:")
|
|
193
|
+
click.echo(url)
|
|
194
|
+
auth_url_shown = True
|
|
195
|
+
|
|
196
|
+
result = login_with_browser(
|
|
197
|
+
config=config,
|
|
198
|
+
launch_browser=not no_browser,
|
|
199
|
+
authorization_url_callback=show_auth_url if no_browser else None,
|
|
200
|
+
)
|
|
201
|
+
config = result.get("config", config)
|
|
202
|
+
if (not auth_url_shown) and (not result["browser_opened"]):
|
|
203
|
+
click.echo("Visit this URL to authenticate:")
|
|
204
|
+
click.echo(result["authorization_url"])
|
|
205
|
+
token = result["token"]
|
|
206
|
+
|
|
207
|
+
save_login(token_file=token_file, config=config, token=token)
|
|
208
|
+
except LoginTimeoutError as e:
|
|
209
|
+
click.echo(err(f"ALERT: {e}"), err=True)
|
|
210
|
+
raise click.exceptions.Exit(1) from e
|
|
211
|
+
except CallbackBindError as e:
|
|
212
|
+
click.echo(err(f"ALERT: {e}"), err=True)
|
|
213
|
+
raise click.exceptions.Exit(1) from e
|
|
214
|
+
except AuthError as e:
|
|
215
|
+
raise click.ClickException(str(e)) from e
|
|
216
|
+
except OSError as e:
|
|
217
|
+
raise click.ClickException(f"Login setup failed: {e}") from e
|
|
218
|
+
|
|
219
|
+
click.echo(f"You have successfully {action} CWBI.")
|
|
220
|
+
refresh_expiry = refresh_token_expiry_text(token)
|
|
221
|
+
if refresh_expiry:
|
|
222
|
+
click.echo(
|
|
223
|
+
c(
|
|
224
|
+
f"Your refresh session is good until {refresh_expiry}.",
|
|
225
|
+
"blue",
|
|
226
|
+
bright=True,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
logging.debug("Saved login session to %s", token_file)
|
|
230
|
+
expiry = token_expiry_text(token)
|
|
231
|
+
if expiry:
|
|
232
|
+
logging.debug("Access token expires at %s", expiry)
|
|
233
|
+
if token.get("refresh_token"):
|
|
234
|
+
logging.debug("A refresh token is available for future reuse.")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# region SHEF
|
|
238
|
+
# ================================================================================
|
|
239
|
+
# SHEF
|
|
240
|
+
# ================================================================================
|
|
241
|
+
@click.group(
|
|
242
|
+
"shef",
|
|
243
|
+
help="Manage SHEF file imports (crit files, .in configuration files)",
|
|
244
|
+
)
|
|
245
|
+
def shef_group():
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ================================================================================
|
|
250
|
+
# Import Crit File
|
|
251
|
+
# ================================================================================
|
|
252
|
+
@shef_group.command(
|
|
253
|
+
"import_crit",
|
|
34
254
|
help="Import SHEF crit file into timeseries group for SHEF file processing",
|
|
35
255
|
)
|
|
36
256
|
@click.option(
|
|
@@ -42,9 +262,10 @@ from cwmscli.utils.version import get_cwms_cli_version
|
|
|
42
262
|
)
|
|
43
263
|
@common_api_options
|
|
44
264
|
@api_key_loc_option
|
|
265
|
+
@click.option("--dry-run", is_flag=True, help="Show request; do not send.")
|
|
45
266
|
@requires(reqs.cwms)
|
|
46
|
-
def
|
|
47
|
-
from cwmscli.commands.
|
|
267
|
+
def shef_import_crit(filename, office, api_root, api_key, api_key_loc, dry_run):
|
|
268
|
+
from cwmscli.commands.shef.import_critfile import import_shef_critfile
|
|
48
269
|
from cwmscli.utils import get_api_key
|
|
49
270
|
|
|
50
271
|
api_key = get_api_key(api_key, api_key_loc)
|
|
@@ -53,9 +274,67 @@ def shefcritimport(filename, office, api_root, api_key, api_key_loc):
|
|
|
53
274
|
office_id=office,
|
|
54
275
|
api_root=api_root,
|
|
55
276
|
api_key=api_key,
|
|
277
|
+
dry_run=dry_run,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ================================================================================
|
|
282
|
+
# Import .in File
|
|
283
|
+
# ================================================================================
|
|
284
|
+
@shef_group.command(
|
|
285
|
+
"import_infile",
|
|
286
|
+
help="Import SHEF .in file into timeseries group for SHEF file processing",
|
|
287
|
+
)
|
|
288
|
+
@click.option(
|
|
289
|
+
"-f",
|
|
290
|
+
"--filename",
|
|
291
|
+
required=True,
|
|
292
|
+
type=str,
|
|
293
|
+
help="filename of SHEF .in file to be processed",
|
|
294
|
+
)
|
|
295
|
+
@click.option(
|
|
296
|
+
"-g",
|
|
297
|
+
"--group-name",
|
|
298
|
+
required=True,
|
|
299
|
+
type=str,
|
|
300
|
+
help="CWMS timeseries group name",
|
|
301
|
+
)
|
|
302
|
+
@click.option(
|
|
303
|
+
"--category",
|
|
304
|
+
type=str,
|
|
305
|
+
default="SHEF Export",
|
|
306
|
+
show_default=True,
|
|
307
|
+
help="Timeseries category ID",
|
|
308
|
+
)
|
|
309
|
+
@common_api_options
|
|
310
|
+
@api_key_loc_option
|
|
311
|
+
@click.option(
|
|
312
|
+
"--dry-run",
|
|
313
|
+
is_flag=True,
|
|
314
|
+
help="Parse the .in file and print the JSON payload without posting to the API.",
|
|
315
|
+
)
|
|
316
|
+
@requires(reqs.cwms)
|
|
317
|
+
def shef_import_infile(
|
|
318
|
+
filename, group_name, category, office, api_root, api_key, api_key_loc, dry_run
|
|
319
|
+
):
|
|
320
|
+
from cwmscli.commands.shef.import_infile import import_shef_infile
|
|
321
|
+
from cwmscli.utils import get_api_key
|
|
322
|
+
|
|
323
|
+
api_key = get_api_key(api_key, api_key_loc)
|
|
324
|
+
import_shef_infile(
|
|
325
|
+
in_file=filename,
|
|
326
|
+
group_name=group_name,
|
|
327
|
+
office_id=office,
|
|
328
|
+
api_root=api_root,
|
|
329
|
+
api_key=api_key,
|
|
330
|
+
category_id=category,
|
|
331
|
+
dry_run=dry_run,
|
|
56
332
|
)
|
|
57
333
|
|
|
58
334
|
|
|
335
|
+
# endregion
|
|
336
|
+
|
|
337
|
+
|
|
59
338
|
@click.command("csv2cwms", help="Store CSV TimeSeries data to CWMS using a config file")
|
|
60
339
|
@common_api_options
|
|
61
340
|
@click.option(
|
|
@@ -6,6 +6,8 @@ from datetime import datetime
|
|
|
6
6
|
|
|
7
7
|
import cwms
|
|
8
8
|
|
|
9
|
+
from cwmscli.utils import init_cwms_session
|
|
10
|
+
|
|
9
11
|
# Add the current directory to the path
|
|
10
12
|
# This is necessary for the script to be run as a standalone script
|
|
11
13
|
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
|
@@ -70,8 +72,8 @@ def main(*args, **kwargs):
|
|
|
70
72
|
tz = safe_zoneinfo(kwargs.get("tz"))
|
|
71
73
|
begin_time = _resolve_begin_time(tz, kwargs.get("begin"))
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
api_root=kwargs.get("api_root"), api_key=kwargs.get("api_key")
|
|
75
|
+
init_cwms_session(
|
|
76
|
+
cwms, api_root=kwargs.get("api_root"), api_key=kwargs.get("api_key")
|
|
75
77
|
)
|
|
76
78
|
setup_logger(kwargs.get("log"), verbose=kwargs.get("verbose"))
|
|
77
79
|
logger.info(f"Begin time: {begin_time}")
|
|
@@ -5,6 +5,8 @@ from typing import Dict, List
|
|
|
5
5
|
import cwms
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
|
+
from cwmscli.utils import init_cwms_session
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def import_shef_critfile(
|
|
10
12
|
file_path: str,
|
|
@@ -16,6 +18,7 @@ def import_shef_critfile(
|
|
|
16
18
|
group_office_id: str = "CWMS",
|
|
17
19
|
category_office_id: str = "CWMS",
|
|
18
20
|
replace_assigned_ts: bool = False,
|
|
21
|
+
dry_run: bool = False,
|
|
19
22
|
) -> None:
|
|
20
23
|
"""
|
|
21
24
|
Processes a .crit file and saves the information to the SHEF Data Acquisition time series group.
|
|
@@ -34,19 +37,40 @@ def import_shef_critfile(
|
|
|
34
37
|
The specified office group associated with the timeseries data. Defaults to "CWMS".
|
|
35
38
|
replace_assigned_ts : bool, optional
|
|
36
39
|
Specifies whether to unassign all existing time series before assigning new time series specified in the content body. Default is False.
|
|
40
|
+
dry_run : bool, optional
|
|
41
|
+
Parse the .crit file and print the JSON payload without posting to the API. Default is False.
|
|
37
42
|
|
|
38
43
|
Returns
|
|
39
44
|
-------
|
|
40
45
|
None
|
|
41
46
|
"""
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if not dry_run:
|
|
49
|
+
init_cwms_session(cwms, api_root=api_root, api_key="apikey " + api_key)
|
|
50
|
+
logging.info(f"CDA connection: {api_root}")
|
|
46
51
|
|
|
47
52
|
# Parse the file and get the parsed data
|
|
48
53
|
parsed_data = parse_crit_file(file_path)
|
|
49
54
|
logging.info("CRIT file has been parsed")
|
|
55
|
+
logging.info(f"Found {len(parsed_data)} timeseries entries:")
|
|
56
|
+
for data in parsed_data:
|
|
57
|
+
logging.info(f" {data['Timeseries ID']} --> alias={data['Alias']}")
|
|
58
|
+
|
|
59
|
+
if not parsed_data:
|
|
60
|
+
logging.error("No timeseries entries found in the CRIT file")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if dry_run:
|
|
64
|
+
logging.info(
|
|
65
|
+
f"\n--- DRY RUN: The following timeseries entries will be added to {group_id} ---"
|
|
66
|
+
)
|
|
67
|
+
for data in parsed_data:
|
|
68
|
+
logging.info(
|
|
69
|
+
f" timeseries-id: {data['Timeseries ID']}, alias-id: {data['Alias']}"
|
|
70
|
+
)
|
|
71
|
+
logging.info(f"--- Dry run complete. Nothing was posted to the API. ---\n")
|
|
72
|
+
return
|
|
73
|
+
|
|
50
74
|
# df = pd.DataFrame()
|
|
51
75
|
logging.info(f"Saving Timeseries IDs to group: {group_id}")
|
|
52
76
|
for data in parsed_data:
|