cwms-python 1.0.7__tar.gz → 1.0.9__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_python-1.0.7 → cwms_python-1.0.9}/PKG-INFO +12 -3
- {cwms_python-1.0.7 → cwms_python-1.0.9}/README.md +5 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/__init__.py +2 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/api.py +77 -7
- cwms_python-1.0.9/cwms/locations/lookups.py +161 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/projects/projects.py +3 -3
- cwms_python-1.0.9/cwms/properties/properties.py +168 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries.py +100 -50
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_group.py +30 -8
- {cwms_python-1.0.7 → cwms_python-1.0.9}/pyproject.toml +21 -5
- {cwms_python-1.0.7 → cwms_python-1.0.9}/LICENSE +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/catalog/blobs.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/catalog/catalog.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/catalog/clobs.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/cwms_types.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/forecast/forecast_instance.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/forecast/forecast_spec.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/levels/location_levels.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/levels/specified_levels.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/locations/gate_changes.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/locations/location_groups.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/locations/physical_locations.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/measurements/measurements.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/outlets/outlets.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/outlets/virtual_outlets.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/projects/project_lock_rights.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/projects/project_locks.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/projects/water_supply/accounting.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/ratings/ratings.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/ratings/ratings_spec.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/ratings/ratings_template.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/standard_text/standard_text.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_bin.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_identifier.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_profile.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_profile_instance.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_profile_parser.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/timeseries/timeseries_txt.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/turbines/turbines.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/users/users.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/utils/__init__.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.9}/cwms/utils/checks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Corps water management systems (CWMS) REST API for Data Retrieval of USACE water data
|
|
5
5
|
License: LICENSE
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,8 +16,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: numpy (>=1.23.5,<3) ; python_version >= "3.9" and python_version < "3.13"
|
|
20
|
+
Requires-Dist: numpy (>=2.1.0,<3) ; python_version == "3.13"
|
|
21
|
+
Requires-Dist: numpy (>=2.3.3,<3) ; python_version >= "3.14"
|
|
22
|
+
Requires-Dist: pandas (>=2.3.3,<3.0.0)
|
|
23
|
+
Requires-Dist: requests (>=2.32.4,<3.0.0) ; python_version == "3.9"
|
|
24
|
+
Requires-Dist: requests (>=2.33.0,<3.0.0) ; python_version >= "3.10"
|
|
21
25
|
Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
|
|
22
26
|
Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-python
|
|
23
27
|
Description-Content-Type: text/markdown
|
|
@@ -124,3 +128,8 @@ endpoint implementation.
|
|
|
124
128
|
|
|
125
129
|
Please view the contribution documentation here: [CONTRIBUTING.md]
|
|
126
130
|
|
|
131
|
+
## Contributing and releases
|
|
132
|
+
|
|
133
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development checks, PR title conventions,
|
|
134
|
+
and the Release Please publishing workflow.
|
|
135
|
+
|
|
@@ -99,3 +99,8 @@ endpoint implementation.
|
|
|
99
99
|
## Contributing
|
|
100
100
|
|
|
101
101
|
Please view the contribution documentation here: [CONTRIBUTING.md]
|
|
102
|
+
|
|
103
|
+
## Contributing and releases
|
|
104
|
+
|
|
105
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development checks, PR title conventions,
|
|
106
|
+
and the Release Please publishing workflow.
|
|
@@ -10,6 +10,7 @@ from cwms.levels.location_levels import *
|
|
|
10
10
|
from cwms.levels.specified_levels import *
|
|
11
11
|
from cwms.locations.gate_changes import *
|
|
12
12
|
from cwms.locations.location_groups import *
|
|
13
|
+
from cwms.locations.lookups import *
|
|
13
14
|
from cwms.locations.physical_locations import *
|
|
14
15
|
from cwms.measurements.measurements import *
|
|
15
16
|
from cwms.outlets.outlets import *
|
|
@@ -18,6 +19,7 @@ from cwms.projects.project_lock_rights import *
|
|
|
18
19
|
from cwms.projects.project_locks import *
|
|
19
20
|
from cwms.projects.projects import *
|
|
20
21
|
from cwms.projects.water_supply.accounting import *
|
|
22
|
+
from cwms.properties.properties import *
|
|
21
23
|
from cwms.ratings.ratings import *
|
|
22
24
|
from cwms.ratings.ratings_spec import *
|
|
23
25
|
from cwms.ratings.ratings_template import *
|
|
@@ -48,6 +48,9 @@ from cwms.cwms_types import JSON, RequestParams
|
|
|
48
48
|
API_ROOT = "https://cwms-data.usace.army.mil/cwms-data/"
|
|
49
49
|
API_VERSION = 2
|
|
50
50
|
|
|
51
|
+
# Specify whether LRTS will use new ID format
|
|
52
|
+
USE_NEW_LRTS_IDS = False
|
|
53
|
+
|
|
51
54
|
# Initialize a non-authenticated session with the default root URL and set default pool connections.
|
|
52
55
|
|
|
53
56
|
retry_strategy = Retry(
|
|
@@ -168,6 +171,7 @@ def init_session(
|
|
|
168
171
|
api_key: Optional[str] = None,
|
|
169
172
|
token: Optional[str] = None,
|
|
170
173
|
pool_connections: int = 100,
|
|
174
|
+
use_new_lrts_format: bool = False,
|
|
171
175
|
) -> BaseUrlSession:
|
|
172
176
|
"""Specify a root URL and authentication credentials for the CWMS Data API.
|
|
173
177
|
|
|
@@ -186,7 +190,7 @@ def init_session(
|
|
|
186
190
|
Returns the updated session object.
|
|
187
191
|
"""
|
|
188
192
|
|
|
189
|
-
global SESSION
|
|
193
|
+
global SESSION, USE_NEW_LRTS_IDS
|
|
190
194
|
if api_root:
|
|
191
195
|
# Ensure the API_ROOT ends with a single slash
|
|
192
196
|
api_root = api_root.rstrip("/") + "/"
|
|
@@ -206,11 +210,23 @@ def init_session(
|
|
|
206
210
|
# Ensure we don't provide the bearer text twice
|
|
207
211
|
if token.lower().startswith("bearer "):
|
|
208
212
|
token = token[7:]
|
|
209
|
-
SESSION.headers.update(
|
|
213
|
+
SESSION.headers.update(
|
|
214
|
+
{
|
|
215
|
+
"Authorization": "Bearer " + token,
|
|
216
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
217
|
+
}
|
|
218
|
+
)
|
|
210
219
|
elif api_key:
|
|
211
220
|
if api_key.startswith("apikey "):
|
|
212
221
|
api_key = api_key.replace("apikey ", "")
|
|
213
|
-
SESSION.headers.update(
|
|
222
|
+
SESSION.headers.update(
|
|
223
|
+
{
|
|
224
|
+
"Authorization": "apikey " + api_key,
|
|
225
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
USE_NEW_LRTS_IDS = use_new_lrts_format
|
|
214
230
|
|
|
215
231
|
return SESSION
|
|
216
232
|
|
|
@@ -225,6 +241,46 @@ def return_base_url() -> str:
|
|
|
225
241
|
return str(SESSION.base_url)
|
|
226
242
|
|
|
227
243
|
|
|
244
|
+
def set_use_new_lrts_ids(state: bool) -> None:
|
|
245
|
+
"""Sets whether the new LRTS identifer format is used for subsequent operations.
|
|
246
|
+
|
|
247
|
+
The old Local Regular Time Series (LRTS) identifier format is the same as the
|
|
248
|
+
Pseudo-Regular Time Series (PRTS) identifier format, where both prepend a tilde
|
|
249
|
+
character ('~') to valid Regular Time Series (RTS) interval identifiers (e.g.,
|
|
250
|
+
~6Hours, ~1Day).
|
|
251
|
+
|
|
252
|
+
The new LRTS identifiers instead append "Local" to valid RTS interval identifiers
|
|
253
|
+
(e.g., 6HoursLocal, 1DayLocal), leaving the old format to specify only PRTS.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
state: Whether the new LRTS identifier format is used
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
global USE_NEW_LRTS_IDS
|
|
260
|
+
|
|
261
|
+
USE_NEW_LRTS_IDS = state
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_use_new_lrts_ids() -> bool:
|
|
265
|
+
"""Gets whether the new LRTS identifer format is used for subsequent operations.
|
|
266
|
+
|
|
267
|
+
The old Local Regular Time Series (LRTS) identifier format is the same as the
|
|
268
|
+
Pseudo-Regular Time Series (PRTS) identifier format, where both prepend a tilde
|
|
269
|
+
character ('~') to valid Regular Time Series (RTS) interval identifiers (e.g.,
|
|
270
|
+
~6Hours, ~1Day).
|
|
271
|
+
|
|
272
|
+
The new LRTS identifiers instead append "Local" to valid RTS interval identifiers
|
|
273
|
+
(e.g., 6HoursLocal, 1DayLocal), leaving the old format to specify only PRTS.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Whether the new LRTS identifier format is used
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
global USE_NEW_LRTS_IDS
|
|
280
|
+
|
|
281
|
+
return USE_NEW_LRTS_IDS
|
|
282
|
+
|
|
283
|
+
|
|
228
284
|
def api_version_text(api_version: int) -> str:
|
|
229
285
|
"""Initialize CDA request headers.
|
|
230
286
|
|
|
@@ -329,7 +385,10 @@ def get(
|
|
|
329
385
|
ApiError: If an error response is return by the API.
|
|
330
386
|
"""
|
|
331
387
|
|
|
332
|
-
headers = {
|
|
388
|
+
headers = {
|
|
389
|
+
"Accept": api_version_text(api_version),
|
|
390
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
391
|
+
}
|
|
333
392
|
try:
|
|
334
393
|
with SESSION.get(endpoint, params=params, headers=headers) as response:
|
|
335
394
|
if not response.ok:
|
|
@@ -389,7 +448,11 @@ def _post_function(
|
|
|
389
448
|
) -> Any:
|
|
390
449
|
|
|
391
450
|
# post requires different headers than get for
|
|
392
|
-
headers = {
|
|
451
|
+
headers = {
|
|
452
|
+
"accept": "*/*",
|
|
453
|
+
"Content-Type": api_version_text(api_version),
|
|
454
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
455
|
+
}
|
|
393
456
|
if isinstance(data, dict) or isinstance(data, list):
|
|
394
457
|
data = json.dumps(data)
|
|
395
458
|
try:
|
|
@@ -487,7 +550,11 @@ def patch(
|
|
|
487
550
|
ApiError: If an error response is return by the API.
|
|
488
551
|
"""
|
|
489
552
|
|
|
490
|
-
headers = {
|
|
553
|
+
headers = {
|
|
554
|
+
"accept": "*/*",
|
|
555
|
+
"Content-Type": api_version_text(api_version),
|
|
556
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
557
|
+
}
|
|
491
558
|
|
|
492
559
|
if data and isinstance(data, dict) or isinstance(data, list):
|
|
493
560
|
data = json.dumps(data)
|
|
@@ -522,7 +589,10 @@ def delete(
|
|
|
522
589
|
ApiError: If an error response is return by the API.
|
|
523
590
|
"""
|
|
524
591
|
|
|
525
|
-
headers = {
|
|
592
|
+
headers = {
|
|
593
|
+
"Accept": api_version_text(api_version),
|
|
594
|
+
"X-CWMS-LRTS-Formatting": str(USE_NEW_LRTS_IDS).lower(),
|
|
595
|
+
}
|
|
526
596
|
try:
|
|
527
597
|
with SESSION.delete(endpoint, params=params, headers=headers) as response:
|
|
528
598
|
if not response.ok:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Copyright (c) 2026
|
|
2
|
+
# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
|
|
3
|
+
# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
|
|
4
|
+
# Source may not be released without written approval from HEC
|
|
5
|
+
|
|
6
|
+
import cwms.api as api
|
|
7
|
+
from cwms.cwms_types import JSON, Data
|
|
8
|
+
|
|
9
|
+
ENDPOINT = "lookup-types"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_all_lookups(category: str, prefix: str, office_id: str) -> Data:
|
|
13
|
+
"""
|
|
14
|
+
Retrieves all lookups for a given category, prefix, and office.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
category : str
|
|
19
|
+
Filters lookup types to the specified category
|
|
20
|
+
prefix : str
|
|
21
|
+
Filters lookup types to the specified prefix
|
|
22
|
+
office_id : str
|
|
23
|
+
Filters lookup types to the specified office ID
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Data
|
|
28
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
29
|
+
|
|
30
|
+
Raises
|
|
31
|
+
------
|
|
32
|
+
ValueError
|
|
33
|
+
If any required argument is missing.
|
|
34
|
+
ClientError
|
|
35
|
+
If a 400 range error code response is returned from the server.
|
|
36
|
+
NoDataFoundError
|
|
37
|
+
If a 404 range error code response is returned from the server.
|
|
38
|
+
ServerError
|
|
39
|
+
If a 500 range error code response is returned from the server.
|
|
40
|
+
"""
|
|
41
|
+
if not all([category, prefix, office_id]):
|
|
42
|
+
raise ValueError("Category, Prefix, and Office ID must be specified")
|
|
43
|
+
|
|
44
|
+
params = {"category": category, "prefix": prefix, "office": office_id}
|
|
45
|
+
response = api.get(ENDPOINT, params, api_version=1)
|
|
46
|
+
return Data(response)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_lookup(data: JSON, category: str, prefix: str) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Creates a new lookup entry.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
data: JSON
|
|
56
|
+
A dictionary representing the JSON data to be stored. This should match the
|
|
57
|
+
LookupType structure as defined by the API.
|
|
58
|
+
category : str
|
|
59
|
+
Specifies the category of the lookup.
|
|
60
|
+
prefix : str
|
|
61
|
+
Specifies the prefix of the lookup.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
None
|
|
66
|
+
|
|
67
|
+
Raises
|
|
68
|
+
------
|
|
69
|
+
ValueError
|
|
70
|
+
If any required argument is missing.
|
|
71
|
+
ClientError
|
|
72
|
+
If a 400 range error code response is returned from the server.
|
|
73
|
+
NoDataFoundError
|
|
74
|
+
If a 404 range error code response is returned from the server.
|
|
75
|
+
ServerError
|
|
76
|
+
If a 500 range error code response is returned from the server.
|
|
77
|
+
"""
|
|
78
|
+
if not all([category, prefix]):
|
|
79
|
+
raise ValueError("Category and Prefix must be specified")
|
|
80
|
+
if not data:
|
|
81
|
+
raise ValueError("Data must be specified")
|
|
82
|
+
params = {"category": category, "prefix": prefix}
|
|
83
|
+
api.post(ENDPOINT, data, params, api_version=1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def update_lookup(data: JSON, category: str, prefix: str) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Updates a specified lookup entry.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
data : JSON
|
|
93
|
+
A dictionary representing the JSON data to be stored.
|
|
94
|
+
If the `data` value is None, a `ValueError` will be raised.
|
|
95
|
+
category : str
|
|
96
|
+
Specifies the category of the lookup.
|
|
97
|
+
prefix : str
|
|
98
|
+
Specifies the prefix of the lookup.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
None
|
|
103
|
+
|
|
104
|
+
Raises
|
|
105
|
+
------
|
|
106
|
+
ValueError
|
|
107
|
+
If any required argument is missing.
|
|
108
|
+
ClientError
|
|
109
|
+
If a 400 range error code response is returned from the server.
|
|
110
|
+
NoDataFoundError
|
|
111
|
+
If a 404 range error code response is returned from the server.
|
|
112
|
+
ServerError
|
|
113
|
+
If a 500 range error code response is returned from the server.
|
|
114
|
+
"""
|
|
115
|
+
if not all([category, prefix]):
|
|
116
|
+
raise ValueError("Category and Prefix must be specified")
|
|
117
|
+
if not data:
|
|
118
|
+
raise ValueError("Data must be specified")
|
|
119
|
+
|
|
120
|
+
# Note that the path parameter is unused in CDA
|
|
121
|
+
endpoint = f"{ENDPOINT}/{category}"
|
|
122
|
+
params = {"category": category, "prefix": prefix}
|
|
123
|
+
api.patch(endpoint, data, params, api_version=1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def delete_lookup(name: str, category: str, prefix: str, office_id: str) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Deletes a specified lookup entry.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
name : str
|
|
133
|
+
Specifies the location type to delete.
|
|
134
|
+
category : str
|
|
135
|
+
Specifies the category id of the lookup type to be deleted.
|
|
136
|
+
prefix : str
|
|
137
|
+
Specifies the prefix of the lookup type to be deleted.
|
|
138
|
+
office_id : str
|
|
139
|
+
Specifies the owning office of the lookup type to be deleted.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
None
|
|
144
|
+
|
|
145
|
+
Raises
|
|
146
|
+
------
|
|
147
|
+
ValueError
|
|
148
|
+
If any required argument is missing.
|
|
149
|
+
ClientError
|
|
150
|
+
If a 400 range error code response is returned from the server.
|
|
151
|
+
NoDataFoundError
|
|
152
|
+
If a 404 range error code response is returned from the server.
|
|
153
|
+
ServerError
|
|
154
|
+
If a 500 range error code response is returned from the server.
|
|
155
|
+
"""
|
|
156
|
+
if not all([name, category, prefix, office_id]):
|
|
157
|
+
raise ValueError("Name, Category, Prefix, and Office ID must be specified")
|
|
158
|
+
|
|
159
|
+
endpoint = f"{ENDPOINT}/{name}"
|
|
160
|
+
params = {"category": category, "prefix": prefix, "office": office_id}
|
|
161
|
+
api.delete(endpoint, params, api_version=1)
|
|
@@ -42,7 +42,7 @@ def get_project(office_id: str, name: str) -> Data:
|
|
|
42
42
|
|
|
43
43
|
endpoint = f"projects/{name}"
|
|
44
44
|
params = {"office": office_id}
|
|
45
|
-
response = api.get(endpoint, params)
|
|
45
|
+
response = api.get(endpoint, params, api_version=1)
|
|
46
46
|
return Data(response)
|
|
47
47
|
|
|
48
48
|
|
|
@@ -129,7 +129,7 @@ def get_project_locations(
|
|
|
129
129
|
"project-like": project_like,
|
|
130
130
|
"location-kind-like": location_id_like,
|
|
131
131
|
}
|
|
132
|
-
response = api.get(endpoint, params)
|
|
132
|
+
response = api.get(endpoint, params, api_version=1)
|
|
133
133
|
return Data(response)
|
|
134
134
|
|
|
135
135
|
|
|
@@ -169,7 +169,7 @@ def delete_project(office_id: str, name: str, delete_method: DeleteMethod) -> No
|
|
|
169
169
|
|
|
170
170
|
endpoint = f"projects/{name}"
|
|
171
171
|
params = {"office": office_id, "method": delete_method.name}
|
|
172
|
-
api.delete(endpoint, params)
|
|
172
|
+
api.delete(endpoint, params, api_version=1)
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
def rename_project(office_id: str, old_name: str, new_name: str) -> None:
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Copyright (c) 2026
|
|
2
|
+
# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
|
|
3
|
+
# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
|
|
4
|
+
# Source may not be released without written approval from HEC
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import cwms.api as api
|
|
9
|
+
from cwms.cwms_types import JSON, Data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_properties(
|
|
13
|
+
office_mask: Optional[str] = None,
|
|
14
|
+
category_id_mask: Optional[str] = None,
|
|
15
|
+
name_mask: Optional[str] = None,
|
|
16
|
+
) -> Data:
|
|
17
|
+
"""
|
|
18
|
+
Returns matching CWMS Property Data.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
office_mask: string, optional
|
|
23
|
+
Filters properties to the specified office mask
|
|
24
|
+
category_id_mask: string, optional
|
|
25
|
+
Filters properties to the specified category mask
|
|
26
|
+
name_mask: string, optional
|
|
27
|
+
Filters properties to the specified name mask
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
cwms data type
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
endpoint = "properties"
|
|
35
|
+
params = {
|
|
36
|
+
"office-mask": office_mask,
|
|
37
|
+
"category-id-mask": category_id_mask,
|
|
38
|
+
"name-mask": name_mask,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
response = api.get(endpoint, params, api_version=1)
|
|
42
|
+
return Data(response)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_property(
|
|
46
|
+
name: str, office: str, category_id: str, default_value: Optional[str] = None
|
|
47
|
+
) -> Data:
|
|
48
|
+
"""
|
|
49
|
+
Returns CWMS Property Data.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
name: string
|
|
54
|
+
Specifies the name of the property to be retrieved.
|
|
55
|
+
office: string
|
|
56
|
+
Specifies the owning office of the property to be retrieved.
|
|
57
|
+
category_id: string
|
|
58
|
+
Specifies the category id of the property to be retrieved.
|
|
59
|
+
default_value: string, optional
|
|
60
|
+
Specifies the default value if the property does not exist.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
cwms data type
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
endpoint = f"properties/{name}"
|
|
68
|
+
params = {
|
|
69
|
+
"office": office,
|
|
70
|
+
"category-id": category_id,
|
|
71
|
+
"default-value": default_value,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
response = api.get(endpoint, params, api_version=1)
|
|
75
|
+
return Data(response)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_property(data: JSON) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Create CWMS Property.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
data: JSON dictionary
|
|
85
|
+
Property data to be stored.
|
|
86
|
+
Example:
|
|
87
|
+
{
|
|
88
|
+
"office-id": "string",
|
|
89
|
+
"name": "string",
|
|
90
|
+
"category": "string",
|
|
91
|
+
"value": "string",
|
|
92
|
+
"comment": "string"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
None
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
endpoint = "properties"
|
|
101
|
+
|
|
102
|
+
if data is None:
|
|
103
|
+
raise ValueError("Cannot store a property without JSON data")
|
|
104
|
+
|
|
105
|
+
return api.post(endpoint, data, api_version=1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def update_property(name: str, data: JSON) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Update CWMS Property.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
name: string
|
|
115
|
+
Specifies the name of the property to be updated.
|
|
116
|
+
data: JSON dictionary
|
|
117
|
+
Property data to be updated.
|
|
118
|
+
Example:
|
|
119
|
+
{
|
|
120
|
+
"office-id": "string",
|
|
121
|
+
"name": "string",
|
|
122
|
+
"category": "string",
|
|
123
|
+
"value": "string",
|
|
124
|
+
"comment": "string"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
None
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
endpoint = f"properties/{name}"
|
|
133
|
+
|
|
134
|
+
if name is None:
|
|
135
|
+
raise ValueError("Must specify a property name to update")
|
|
136
|
+
|
|
137
|
+
if data is None:
|
|
138
|
+
raise ValueError("Cannot update a property without JSON data")
|
|
139
|
+
|
|
140
|
+
return api.patch(endpoint, data, api_version=1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def delete_property(name: str, office: str, category_id: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Delete CWMS Property.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
name: string
|
|
150
|
+
Specifies the name of the property to be deleted.
|
|
151
|
+
office: string
|
|
152
|
+
Specifies the owning office of the property to be deleted.
|
|
153
|
+
category_id: string
|
|
154
|
+
Specifies the category id of the property to be deleted.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
None
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
endpoint = f"properties/{name}"
|
|
162
|
+
|
|
163
|
+
params = {
|
|
164
|
+
"office": office,
|
|
165
|
+
"category-id": category_id,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return api.delete(endpoint, params, api_version=1)
|
|
@@ -154,6 +154,26 @@ def get_timeseries_chunk(
|
|
|
154
154
|
return Data(response, selector=selector)
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
# Number of attempts for a single chunked timeseries request. CDA occasionally
|
|
158
|
+
# returns 500s caused by connection-pool exhaustion that succeed on retry; 500
|
|
159
|
+
# is intentionally not in the session-level status_forcelist (see PR #282), so
|
|
160
|
+
# we retry here, scoped to the chunked store/fetch paths only.
|
|
161
|
+
_CHUNK_ATTEMPTS = 6
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _call_with_retry(fn: Any, *args: Any, attempts: int = _CHUNK_ATTEMPTS) -> Any:
|
|
165
|
+
for i in range(attempts):
|
|
166
|
+
try:
|
|
167
|
+
return fn(*args)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
status_code = getattr(getattr(e, "response", None), "status_code", None)
|
|
170
|
+
if status_code == 404:
|
|
171
|
+
raise
|
|
172
|
+
if i == attempts - 1:
|
|
173
|
+
raise
|
|
174
|
+
logging.warning(f"chunk attempt {i + 1}/{attempts} failed: {e}")
|
|
175
|
+
|
|
176
|
+
|
|
157
177
|
def fetch_timeseries_chunks(
|
|
158
178
|
chunks: List[Tuple[datetime, datetime]],
|
|
159
179
|
params: Dict[str, Any],
|
|
@@ -161,14 +181,13 @@ def fetch_timeseries_chunks(
|
|
|
161
181
|
endpoint: str,
|
|
162
182
|
max_workers: int,
|
|
163
183
|
) -> List[Data]:
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
results: List[Data] = []
|
|
185
|
+
errors: List[str] = []
|
|
166
186
|
|
|
167
|
-
# Create a ThreadPoolExecutor to manage multithreading
|
|
168
187
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
169
|
-
# Submit tasks for each chunk to the api
|
|
170
188
|
future_to_chunk = {
|
|
171
189
|
executor.submit(
|
|
190
|
+
_call_with_retry,
|
|
172
191
|
get_timeseries_chunk,
|
|
173
192
|
selector,
|
|
174
193
|
endpoint,
|
|
@@ -179,18 +198,23 @@ def fetch_timeseries_chunks(
|
|
|
179
198
|
for chunk_start, chunk_end in chunks
|
|
180
199
|
}
|
|
181
200
|
|
|
182
|
-
# Process completed threads as they finish
|
|
183
201
|
for future in concurrent.futures.as_completed(future_to_chunk):
|
|
202
|
+
chunk_start, chunk_end = future_to_chunk[future]
|
|
184
203
|
try:
|
|
185
|
-
|
|
186
|
-
result = future.result()
|
|
187
|
-
results.append(result)
|
|
204
|
+
results.append(future.result())
|
|
188
205
|
except Exception as e:
|
|
189
|
-
|
|
190
|
-
# Log or handle any errors that occur during execution
|
|
191
|
-
logging.error(
|
|
206
|
+
error_msg = (
|
|
192
207
|
f"Failed to fetch data from {chunk_start} to {chunk_end}: {e}"
|
|
193
208
|
)
|
|
209
|
+
logging.error(error_msg)
|
|
210
|
+
errors.append(error_msg)
|
|
211
|
+
|
|
212
|
+
if errors:
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
f"{len(errors)} of {len(chunks)} chunk(s) failed to fetch:\n"
|
|
215
|
+
+ "\n".join(errors)
|
|
216
|
+
)
|
|
217
|
+
|
|
194
218
|
return results
|
|
195
219
|
|
|
196
220
|
|
|
@@ -504,7 +528,7 @@ def store_multi_timeseries_df(
|
|
|
504
528
|
DELETE_INSERT.
|
|
505
529
|
override_protection: bool, optional, default is False
|
|
506
530
|
A flag to ignore the protected data quality flag when storing data.
|
|
507
|
-
multithread: bool, default is
|
|
531
|
+
multithread: bool, default is true
|
|
508
532
|
Specifies whether to store chunked time series values using multiple threads.
|
|
509
533
|
max_workers: Int, Optional, default is None
|
|
510
534
|
It is a number of Threads aka size of pool in concurrent.futures.ThreadPoolExecutor.
|
|
@@ -512,6 +536,12 @@ def store_multi_timeseries_df(
|
|
|
512
536
|
Returns
|
|
513
537
|
-------
|
|
514
538
|
None
|
|
539
|
+
|
|
540
|
+
Raises
|
|
541
|
+
------
|
|
542
|
+
RuntimeError
|
|
543
|
+
If any series fails to store. The message identifies failed series;
|
|
544
|
+
other series may already have been stored successfully.
|
|
515
545
|
"""
|
|
516
546
|
|
|
517
547
|
def store_ts_ids(
|
|
@@ -520,24 +550,21 @@ def store_multi_timeseries_df(
|
|
|
520
550
|
office_id: str,
|
|
521
551
|
version_date: Optional[datetime] = None,
|
|
522
552
|
) -> None:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
)
|
|
539
|
-
except Exception as e:
|
|
540
|
-
print(f"Error processing {ts_id}: {e}")
|
|
553
|
+
units = data["units"].iloc[0]
|
|
554
|
+
data_json = timeseries_df_to_json(
|
|
555
|
+
data=data,
|
|
556
|
+
ts_id=ts_id,
|
|
557
|
+
units=units,
|
|
558
|
+
office_id=office_id,
|
|
559
|
+
version_date=version_date,
|
|
560
|
+
)
|
|
561
|
+
store_timeseries(
|
|
562
|
+
data=data_json,
|
|
563
|
+
create_as_ltrs=create_as_ltrs,
|
|
564
|
+
store_rule=store_rule,
|
|
565
|
+
override_protection=override_protection,
|
|
566
|
+
multithread=multithread,
|
|
567
|
+
)
|
|
541
568
|
return None
|
|
542
569
|
|
|
543
570
|
required_columns = ["date-time", "value", "ts_id", "units"]
|
|
@@ -553,7 +580,9 @@ def store_multi_timeseries_df(
|
|
|
553
580
|
ts_data_all["ts_id"].astype(str) + ":" + ts_data_all["version_date"].astype(str)
|
|
554
581
|
).unique()
|
|
555
582
|
|
|
583
|
+
errors: List[str] = []
|
|
556
584
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
585
|
+
futures = {}
|
|
557
586
|
for unique_tsid in unique_tsids:
|
|
558
587
|
ts_id, version_date = unique_tsid.split(":", 1)
|
|
559
588
|
if version_date != "NaT":
|
|
@@ -568,9 +597,21 @@ def store_multi_timeseries_df(
|
|
|
568
597
|
(ts_data_all["ts_id"] == ts_id) & ts_data_all["version_date"].isna()
|
|
569
598
|
]
|
|
570
599
|
if not data.empty:
|
|
571
|
-
executor.submit(
|
|
600
|
+
future = executor.submit(
|
|
572
601
|
store_ts_ids, ts_data, ts_id, office_id, version_date_dt
|
|
573
602
|
)
|
|
603
|
+
futures[future] = unique_tsid
|
|
604
|
+
|
|
605
|
+
for future in concurrent.futures.as_completed(futures):
|
|
606
|
+
try:
|
|
607
|
+
future.result()
|
|
608
|
+
except Exception as e:
|
|
609
|
+
errors.append(f"{futures[future]}: {e}")
|
|
610
|
+
|
|
611
|
+
if errors:
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
f"{len(errors)} time series failed to store:\n" + "\n".join(errors)
|
|
614
|
+
)
|
|
574
615
|
|
|
575
616
|
|
|
576
617
|
def chunk_timeseries_data(
|
|
@@ -662,36 +703,45 @@ def store_timeseries(
|
|
|
662
703
|
if len(chunks) == 1 or not multithread:
|
|
663
704
|
return api.post(endpoint, data, params)
|
|
664
705
|
|
|
665
|
-
|
|
706
|
+
if max_workers <= 0:
|
|
707
|
+
raise ValueError("max_workers must be greater than 0")
|
|
708
|
+
|
|
709
|
+
# A new series must exist before multiple transactions can write its data.
|
|
710
|
+
# Complete one normal write first, then retain parallelism for the rest.
|
|
711
|
+
_call_with_retry(api.post, endpoint, chunks[0], params)
|
|
712
|
+
remaining_chunks = chunks[1:]
|
|
713
|
+
actual_workers = min(max_workers, len(remaining_chunks))
|
|
666
714
|
logging.debug(
|
|
667
715
|
f"Storing {len(chunks)} chunks of timeseries data with {actual_workers} threads"
|
|
668
716
|
)
|
|
669
717
|
|
|
670
718
|
# Store chunks concurrently
|
|
671
719
|
responses: List[Dict[str, Any]] = []
|
|
672
|
-
|
|
673
|
-
# Initialize an empty list to store futures
|
|
674
|
-
futures = []
|
|
675
|
-
# Submit each chunk as a separate task to the executor
|
|
676
|
-
for chunk in chunks:
|
|
677
|
-
future = executor.submit(
|
|
678
|
-
api.post, # The function to execute
|
|
679
|
-
endpoint,
|
|
680
|
-
chunk, # The chunk of data to store
|
|
681
|
-
params,
|
|
682
|
-
)
|
|
683
|
-
futures.append(future) # Add the future to the list
|
|
720
|
+
errors: List[str] = []
|
|
684
721
|
|
|
685
|
-
|
|
722
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
|
723
|
+
future_to_chunk = {
|
|
724
|
+
executor.submit(_call_with_retry, api.post, endpoint, chunk, params): chunk
|
|
725
|
+
for chunk in remaining_chunks
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for future in concurrent.futures.as_completed(future_to_chunk):
|
|
729
|
+
chunk = future_to_chunk[future]
|
|
686
730
|
try:
|
|
687
|
-
responses.append({"success
|
|
731
|
+
responses.append({"success": future.result()})
|
|
688
732
|
except Exception as e:
|
|
689
733
|
start_time = chunk["values"][0][0]
|
|
690
734
|
end_time = chunk["values"][-1][0]
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
responses.append({"error":
|
|
735
|
+
error_msg = f"Error storing chunk from {start_time} to {end_time}: {e}"
|
|
736
|
+
logging.error(error_msg)
|
|
737
|
+
errors.append(error_msg)
|
|
738
|
+
responses.append({"error": error_msg})
|
|
739
|
+
|
|
740
|
+
if errors:
|
|
741
|
+
raise RuntimeError(
|
|
742
|
+
f"{len(errors)} of {len(chunks)} chunk(s) failed to store:\n"
|
|
743
|
+
+ "\n".join(errors)
|
|
744
|
+
)
|
|
695
745
|
|
|
696
746
|
return
|
|
697
747
|
|
|
@@ -11,8 +11,8 @@ from cwms.cwms_types import JSON, Data
|
|
|
11
11
|
|
|
12
12
|
def get_timeseries_group(
|
|
13
13
|
group_id: str,
|
|
14
|
-
category_id: str,
|
|
15
|
-
category_office_id: str,
|
|
14
|
+
category_id: Optional[str] = None,
|
|
15
|
+
category_office_id: Optional[str] = None,
|
|
16
16
|
office_id: Optional[str] = None,
|
|
17
17
|
group_office_id: Optional[str] = None,
|
|
18
18
|
) -> Data:
|
|
@@ -54,14 +54,16 @@ def get_timeseries_groups(
|
|
|
54
54
|
timeseries_category_like: Optional[str] = None,
|
|
55
55
|
timeseries_group_like: Optional[str] = None,
|
|
56
56
|
category_office_id: Optional[str] = None,
|
|
57
|
+
group_office_id: Optional[str] = None,
|
|
57
58
|
) -> Data:
|
|
58
59
|
"""
|
|
59
60
|
Retreives a list of time series groups.
|
|
60
61
|
|
|
61
62
|
Parameters
|
|
62
63
|
----------
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
office_id: string
|
|
65
|
+
Specifies the owning office of the timeseries assigned to the group(s).
|
|
66
|
+
If not specified, group information for all assigned TS offices is returned.
|
|
65
67
|
include_assigned: Boolean
|
|
66
68
|
Include the assigned timeseries in the returned timeseries groups. (default: true)
|
|
67
69
|
timeseries_category_like: string
|
|
@@ -70,6 +72,8 @@ def get_timeseries_groups(
|
|
|
70
72
|
Posix regular expression matching against the timeseries group id
|
|
71
73
|
category_office_id: string
|
|
72
74
|
Specifies the owning office of the timeseries group category
|
|
75
|
+
group_office_id: string
|
|
76
|
+
Specifies the owning office of the timeseries group
|
|
73
77
|
Returns
|
|
74
78
|
-------
|
|
75
79
|
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
@@ -78,9 +82,10 @@ def get_timeseries_groups(
|
|
|
78
82
|
endpoint = "timeseries/group"
|
|
79
83
|
params = {
|
|
80
84
|
"office": office_id,
|
|
85
|
+
"group-office-id": group_office_id,
|
|
81
86
|
"include-assigned": include_assigned,
|
|
82
87
|
"timeseries-category-like": timeseries_category_like,
|
|
83
|
-
"
|
|
88
|
+
"timeseries-group-like": timeseries_group_like,
|
|
84
89
|
"category-office-id": category_office_id,
|
|
85
90
|
}
|
|
86
91
|
response = api.get(endpoint=endpoint, params=params, api_version=1)
|
|
@@ -165,7 +170,11 @@ def timeseries_group_df_to_json(
|
|
|
165
170
|
return json_dict
|
|
166
171
|
|
|
167
172
|
|
|
168
|
-
def store_timeseries_groups(
|
|
173
|
+
def store_timeseries_groups(
|
|
174
|
+
data: JSON,
|
|
175
|
+
fail_if_exists: Optional[bool] = True,
|
|
176
|
+
ignore_nulls: Optional[bool] = True,
|
|
177
|
+
) -> None:
|
|
169
178
|
"""
|
|
170
179
|
Create new TimeSeriesGroup
|
|
171
180
|
Parameters
|
|
@@ -174,6 +183,11 @@ def store_timeseries_groups(data: JSON, fail_if_exists: Optional[bool] = True) -
|
|
|
174
183
|
Time Series data to be stored.
|
|
175
184
|
fail_if_exists: Boolean Defualt = True
|
|
176
185
|
Create will fail if provided ID already exists.
|
|
186
|
+
ignore_nulls: Boolean Default = True
|
|
187
|
+
Ignore null values in the request body. If fail_if_exists is False
|
|
188
|
+
and ignore_nulls is False, an existing group's description or
|
|
189
|
+
assigned time series list may be replaced with null/empty values
|
|
190
|
+
from the request body.
|
|
177
191
|
|
|
178
192
|
Returns
|
|
179
193
|
-------
|
|
@@ -184,7 +198,7 @@ def store_timeseries_groups(data: JSON, fail_if_exists: Optional[bool] = True) -
|
|
|
184
198
|
raise ValueError("Cannot store a standard text without timeseries group JSON")
|
|
185
199
|
|
|
186
200
|
endpoint = "timeseries/group"
|
|
187
|
-
params = {"fail-if-exists": fail_if_exists}
|
|
201
|
+
params = {"fail-if-exists": fail_if_exists, "ignore-nulls": ignore_nulls}
|
|
188
202
|
|
|
189
203
|
return api.post(endpoint, data, params, api_version=1)
|
|
190
204
|
|
|
@@ -227,7 +241,12 @@ def update_timeseries_groups(
|
|
|
227
241
|
api.patch(endpoint=endpoint, data=data, params=params, api_version=1)
|
|
228
242
|
|
|
229
243
|
|
|
230
|
-
def delete_timeseries_group(
|
|
244
|
+
def delete_timeseries_group(
|
|
245
|
+
group_id: str,
|
|
246
|
+
category_id: str,
|
|
247
|
+
office_id: str,
|
|
248
|
+
cascade_delete: Optional[bool] = False,
|
|
249
|
+
) -> None:
|
|
231
250
|
"""Deletes requested time series group
|
|
232
251
|
|
|
233
252
|
Parameters
|
|
@@ -238,6 +257,8 @@ def delete_timeseries_group(group_id: str, category_id: str, office_id: str) ->
|
|
|
238
257
|
Specifies the time series category of the time series group to be deleted
|
|
239
258
|
office_id: string
|
|
240
259
|
Specifies the owning office of the time series group to be deleted
|
|
260
|
+
cascade_delete: Boolean Default = False
|
|
261
|
+
Specifies whether to unassign time series in this group before deleting.
|
|
241
262
|
|
|
242
263
|
Returns
|
|
243
264
|
-------
|
|
@@ -248,6 +269,7 @@ def delete_timeseries_group(group_id: str, category_id: str, office_id: str) ->
|
|
|
248
269
|
params = {
|
|
249
270
|
"office": office_id,
|
|
250
271
|
"category-id": category_id,
|
|
272
|
+
"cascade-delete": cascade_delete,
|
|
251
273
|
}
|
|
252
274
|
|
|
253
275
|
return api.delete(endpoint, params=params, api_version=1)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
name = "cwms-python"
|
|
3
3
|
repository = "https://github.com/HydrologicEngineeringCenter/cwms-python"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Managed by Release Please; runtime versions come from package metadata.
|
|
6
|
+
version = "1.0.9"
|
|
6
7
|
|
|
7
8
|
packages = [
|
|
8
9
|
{ include = "cwms" },
|
|
@@ -15,16 +16,31 @@ authors = ["Eric Novotny <eric.v.novotny@usace.army.mil>"]
|
|
|
15
16
|
|
|
16
17
|
[tool.poetry.dependencies]
|
|
17
18
|
python = "^3.9"
|
|
18
|
-
pandas = "^2.
|
|
19
|
+
pandas = "^2.3.3"
|
|
20
|
+
# Require NumPy wheels for newer Python versions while retaining Python 3.9.
|
|
21
|
+
numpy = [
|
|
22
|
+
{version = ">=1.23.5,<3", python = ">=3.9,<3.13"},
|
|
23
|
+
{version = ">=2.1.0,<3", python = ">=3.13,<3.14"},
|
|
24
|
+
{version = ">=2.3.3,<3", python = ">=3.14"},
|
|
25
|
+
]
|
|
19
26
|
requests-toolbelt = "^1.0.0"
|
|
20
|
-
requests =
|
|
27
|
+
requests = [
|
|
28
|
+
{version = "^2.32.4", python = ">=3.9,<3.10"},
|
|
29
|
+
{version = "^2.33.0", python = ">=3.10"},
|
|
30
|
+
]
|
|
21
31
|
|
|
22
32
|
[tool.poetry.group.dev.dependencies]
|
|
23
|
-
black =
|
|
33
|
+
black = [
|
|
34
|
+
{version = "^25.1.0", python = ">=3.9,<3.10"},
|
|
35
|
+
{version = "^26.3.1", python = ">=3.10"},
|
|
36
|
+
]
|
|
24
37
|
isort = "^5.13.2"
|
|
25
38
|
mypy = "^1.9.0"
|
|
26
39
|
pre-commit = "^3.6.2"
|
|
27
|
-
pytest =
|
|
40
|
+
pytest = [
|
|
41
|
+
{version = "^8.3.5", python = ">=3.9,<3.10"},
|
|
42
|
+
{version = "^9.0.3", python = ">=3.10"},
|
|
43
|
+
]
|
|
28
44
|
requests-mock = "^1.11.0"
|
|
29
45
|
pytest-cov = "^4.1.0"
|
|
30
46
|
pandas-stubs = "^2.2.1.240316"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|