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.
Files changed (78) hide show
  1. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/PKG-INFO +2 -1
  2. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/__main__.py +2 -1
  3. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/blob.py +47 -14
  4. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/commands_cwms.py +282 -3
  5. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/__main__.py +4 -2
  6. cwms_cli-0.5.0/cwmscli/commands/shef_critfile_import.py → cwms_cli-0.7.0/cwmscli/commands/shef/import_critfile.py +27 -3
  7. cwms_cli-0.7.0/cwmscli/commands/shef/import_infile.py +714 -0
  8. cwms_cli-0.7.0/cwmscli/commands/shef/shef_parameters.csv +272 -0
  9. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/users.py +2 -3
  10. cwms_cli-0.7.0/cwmscli/load/__init__.py +0 -0
  11. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/location/location.py +19 -6
  12. cwms_cli-0.7.0/cwmscli/load/location/location_ids.py +147 -0
  13. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/location/location_ids_bygroup.py +14 -4
  14. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/root.py +73 -7
  15. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries_ids.py +4 -2
  16. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/requirements.py +2 -3
  17. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/__init__.py +10 -2
  18. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getUSGS_ratings_cda.py +3 -2
  19. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getusgs_cda.py +2 -3
  20. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/getusgs_measurements_cda.py +3 -2
  21. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/rating_ini_file_import.py +40 -17
  22. cwms_cli-0.7.0/cwmscli/utils/__init__.py +243 -0
  23. cwms_cli-0.7.0/cwmscli/utils/auth.py +693 -0
  24. cwms_cli-0.7.0/cwmscli/utils/callback_success.html +160 -0
  25. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/click_help.py +21 -0
  26. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/pyproject.toml +17 -1
  27. cwms_cli-0.5.0/cwmscli/load/location/location_ids.py +0 -104
  28. cwms_cli-0.5.0/cwmscli/utils/__init__.py +0 -151
  29. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/LICENSE +0 -0
  30. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/README.md +0 -0
  31. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/__init__.py +0 -0
  32. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/_generated/__init__.py +0 -0
  33. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/_generated/ownership_data.py +0 -0
  34. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/callbacks/__init__.py +0 -0
  35. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/clob.py +0 -0
  36. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
  37. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/README.md +0 -0
  38. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
  39. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/config.py +0 -0
  40. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
  41. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
  42. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
  43. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
  44. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
  45. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
  46. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
  47. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
  48. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
  49. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
  50. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
  51. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
  52. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
  53. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
  54. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
  55. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
  56. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
  57. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
  58. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
  59. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
  60. {cwms_cli-0.5.0/cwmscli/load → cwms_cli-0.7.0/cwmscli/commands/shef}/__init__.py +0 -0
  61. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/README.md +0 -0
  62. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/__main__.py +0 -0
  63. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries.py +0 -0
  64. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/load/timeseries/timeseries_data.py +0 -0
  65. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/ownership.py +0 -0
  66. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/usgs/__main__.py +0 -0
  67. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/colors.py +0 -0
  68. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/deps.py +0 -0
  69. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/friendly_errors.py +0 -0
  70. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/intervals.py +0 -0
  71. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/io.py +0 -0
  72. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/links.py +0 -0
  73. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/logging/__init__.py +0 -0
  74. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/logging/formatters.py +0 -0
  75. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/ssl_errors.py +0 -0
  76. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/update.py +0 -0
  77. {cwms_cli-0.5.0 → cwms_cli-0.7.0}/cwmscli/utils/version.py +0 -0
  78. {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.5.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.shefcritimport)
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 colors, get_api_key, log_scoped_read_hint
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.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
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
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
574
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
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
- api_key=resolved_api_key,
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
- api_key=resolved_api_key,
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.init_session(api_root=api_root, api_key=api_key)
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
- cwms.delete_blob(office_id=office, blob_id=blob_id)
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
- blob_id,
659
+ bid,
627
660
  office,
628
661
  )
629
662
  return
630
663
  raise
631
- logging.info(f"Deleted blob: {blob_id} for office: {office}")
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.init_session(api_root=api_root, api_key=api_key)
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
- resolved_api_key = _resolve_optional_api_key(api_key, anonymous)
699
- cwms.init_session(api_root=api_root, api_key=resolved_api_key)
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
- api_key=resolved_api_key,
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
- "shefcritimport",
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 shefcritimport(filename, office, api_root, api_key, api_key_loc):
47
- from cwmscli.commands.shef_critfile_import import import_shef_critfile
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
- cwms.api.init_session(
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
- api_key = "apikey " + api_key
44
- cwms.api.init_session(api_root=api_root, api_key=api_key)
45
- logging.info(f"CDA connection: {api_root}")
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: