cwms-python 1.0.10__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cwms_python-1.0.10 → cwms_python-1.1.1}/PKG-INFO +1 -1
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/__init__.py +3 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/lookups.py +2 -0
- cwms_python-1.1.1/cwms/locks/locks.py +198 -0
- cwms_python-1.1.1/cwms/projects/water_supply/water_contracts.py +234 -0
- cwms_python-1.1.1/cwms/projects/water_supply/water_users.py +198 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries.py +86 -7
- {cwms_python-1.0.10 → cwms_python-1.1.1}/pyproject.toml +1 -1
- {cwms_python-1.0.10 → cwms_python-1.1.1}/LICENSE +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/README.md +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/api.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/blobs.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/catalog.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/clobs.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/cwms_types.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/forecast/forecast_instance.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/forecast/forecast_spec.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/levels/location_levels.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/levels/specified_levels.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/gate_changes.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/location_groups.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/physical_locations.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/measurements/measurements.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/outlets/outlets.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/outlets/virtual_outlets.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/project_lock_rights.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/project_locks.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/projects.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/water_supply/accounting.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/properties/properties.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings_spec.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings_template.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/standard_text/standard_text.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_bin.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_group.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_identifier.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile_instance.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile_parser.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_txt.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/turbines/turbines.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/users/users.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/utils/__init__.py +0 -0
- {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/utils/checks.py +0 -0
|
@@ -12,6 +12,7 @@ from cwms.locations.gate_changes import *
|
|
|
12
12
|
from cwms.locations.location_groups import *
|
|
13
13
|
from cwms.locations.lookups import *
|
|
14
14
|
from cwms.locations.physical_locations import *
|
|
15
|
+
from cwms.locks.locks import *
|
|
15
16
|
from cwms.measurements.measurements import *
|
|
16
17
|
from cwms.outlets.outlets import *
|
|
17
18
|
from cwms.outlets.virtual_outlets import *
|
|
@@ -19,6 +20,8 @@ from cwms.projects.project_lock_rights import *
|
|
|
19
20
|
from cwms.projects.project_locks import *
|
|
20
21
|
from cwms.projects.projects import *
|
|
21
22
|
from cwms.projects.water_supply.accounting import *
|
|
23
|
+
from cwms.projects.water_supply.water_contracts import *
|
|
24
|
+
from cwms.projects.water_supply.water_users import *
|
|
22
25
|
from cwms.properties.properties import *
|
|
23
26
|
from cwms.ratings.ratings import *
|
|
24
27
|
from cwms.ratings.ratings_spec import *
|
|
@@ -0,0 +1,198 @@
|
|
|
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, DeleteMethod
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_lock(name: str, office_id: str, unit: Optional[str] = "SI") -> Data:
|
|
13
|
+
"""
|
|
14
|
+
Get a specified lock with the given office and name.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
office_id : str
|
|
19
|
+
The office ID of the lock to retrieve. (Query)
|
|
20
|
+
name : str
|
|
21
|
+
The name of the lock to retrieve. (Query)
|
|
22
|
+
unit : str
|
|
23
|
+
The unit system to use for the response. Defaults to "SI". (Query)
|
|
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 path parameters are None.
|
|
34
|
+
ClientError
|
|
35
|
+
If a 400-level error occurs.
|
|
36
|
+
NoDataFoundError
|
|
37
|
+
If a 404-level error occurs.
|
|
38
|
+
ServerError
|
|
39
|
+
If a 500-level error occurs.
|
|
40
|
+
"""
|
|
41
|
+
if not all([office_id, name]):
|
|
42
|
+
raise ValueError("Office and Name must be provided.")
|
|
43
|
+
|
|
44
|
+
endpoint = f"projects/locks/{name}"
|
|
45
|
+
|
|
46
|
+
params = {"office": office_id, "unit": unit}
|
|
47
|
+
|
|
48
|
+
response = api.get(endpoint, params, api_version=1)
|
|
49
|
+
return Data(response)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_locks(office_id: str, project_id: str) -> Data:
|
|
53
|
+
"""
|
|
54
|
+
Get all locks for the given office and project.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
office_id : str
|
|
59
|
+
The office ID of the locks to retrieve. (Query)
|
|
60
|
+
project_id : str
|
|
61
|
+
The project ID of the locks to retrieve. (Query)
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
Data
|
|
66
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
ValueError
|
|
71
|
+
If any required path parameters are None.
|
|
72
|
+
ClientError
|
|
73
|
+
If a 400-level error occurs.
|
|
74
|
+
NoDataFoundError
|
|
75
|
+
If a 404-level error occurs.
|
|
76
|
+
ServerError
|
|
77
|
+
If a 500-level error occurs.
|
|
78
|
+
"""
|
|
79
|
+
if not all([office_id, project_id]):
|
|
80
|
+
raise ValueError("Office and Project ID must be provided.")
|
|
81
|
+
|
|
82
|
+
endpoint = "projects/locks"
|
|
83
|
+
|
|
84
|
+
params = {"office": office_id, "project-id": project_id}
|
|
85
|
+
|
|
86
|
+
response = api.get(endpoint, params, api_version=1)
|
|
87
|
+
return Data(response)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_lock(data: JSON, fail_if_exists: bool = True) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Create CWMS Lock.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
data : JSON
|
|
97
|
+
Lock successfully stored to CWMS. (Body)
|
|
98
|
+
fail_if_exists : bool, optional
|
|
99
|
+
Create will fail if provided ID already exists. Default: True. (Query)
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
None
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
ValueError
|
|
108
|
+
If any required parameters are None.
|
|
109
|
+
ClientError
|
|
110
|
+
If a 400-level error occurs.
|
|
111
|
+
NoDataFoundError
|
|
112
|
+
If a 404-level error occurs.
|
|
113
|
+
ServerError
|
|
114
|
+
If a 500-level error occurs.
|
|
115
|
+
"""
|
|
116
|
+
if not data:
|
|
117
|
+
raise ValueError("Data must be provided and cannot be empty.")
|
|
118
|
+
|
|
119
|
+
endpoint = "projects/locks"
|
|
120
|
+
params = {"fail-if-exists": fail_if_exists}
|
|
121
|
+
|
|
122
|
+
api.post(endpoint, data, params, api_version=1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def delete_lock(
|
|
126
|
+
name: str, office_id: str, method: DeleteMethod = DeleteMethod.DELETE_KEY
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Delete CWMS Lock
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
name : str
|
|
134
|
+
Specifies the name of the lock to be deleted. (Path)
|
|
135
|
+
office_id : str
|
|
136
|
+
Specifies the owning office of the lock to be deleted. (Query)
|
|
137
|
+
method : DeleteMethod, optional
|
|
138
|
+
Specifies the delete method used. Defaults to "DELETE_KEY". (Query)
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
None
|
|
143
|
+
|
|
144
|
+
Raises
|
|
145
|
+
------
|
|
146
|
+
ValueError
|
|
147
|
+
If any required parameters are None.
|
|
148
|
+
ClientError
|
|
149
|
+
If a 400-level error occurs.
|
|
150
|
+
NoDataFoundError
|
|
151
|
+
If a 404-level error occurs.
|
|
152
|
+
ServerError
|
|
153
|
+
If a 500-level error occurs.
|
|
154
|
+
"""
|
|
155
|
+
if not all([name, office_id]):
|
|
156
|
+
raise ValueError("Name and Office ID must be provided.")
|
|
157
|
+
|
|
158
|
+
endpoint = f"projects/locks/{name}"
|
|
159
|
+
params = {"office": office_id, "method": method.name}
|
|
160
|
+
|
|
161
|
+
api.delete(endpoint, params, api_version=1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def update_lock(name: str, office_id: str, new_name: str) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Rename CWMS Lock
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
name : str
|
|
171
|
+
Specifies the name of the lock to be renamed. (Path)
|
|
172
|
+
office_id : str
|
|
173
|
+
Specifies the owning office of the lock to be renamed. (Query)
|
|
174
|
+
new_name : str
|
|
175
|
+
Specifies the new lock name. (Query)
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
None
|
|
180
|
+
|
|
181
|
+
Raises
|
|
182
|
+
------
|
|
183
|
+
ValueError
|
|
184
|
+
If any required parameters are None.
|
|
185
|
+
ClientError
|
|
186
|
+
If a 400-level error occurs.
|
|
187
|
+
NoDataFoundError
|
|
188
|
+
If a 404-level error occurs.
|
|
189
|
+
ServerError
|
|
190
|
+
If a 500-level error occurs.
|
|
191
|
+
"""
|
|
192
|
+
if not all([name, office_id, new_name]):
|
|
193
|
+
raise ValueError("Name, Office ID, and New Name must be provided.")
|
|
194
|
+
|
|
195
|
+
endpoint = f"projects/locks/{name}"
|
|
196
|
+
params = {"office": office_id, "name": new_name}
|
|
197
|
+
|
|
198
|
+
api.patch(endpoint, None, params, api_version=1)
|
|
@@ -0,0 +1,234 @@
|
|
|
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, DeleteMethod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_water_contract(
|
|
11
|
+
office_id: str, project_id: str, water_user: str, contract_name: str
|
|
12
|
+
) -> Data:
|
|
13
|
+
"""
|
|
14
|
+
Return a specified water contract
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
office_id : str
|
|
19
|
+
The office Id the contract is associated with. (Path)
|
|
20
|
+
project_id : str
|
|
21
|
+
The project Id the contract is associated with. (Path)
|
|
22
|
+
water_user : str
|
|
23
|
+
The water user the contract is associated with. (Path)
|
|
24
|
+
contract_name : str
|
|
25
|
+
The name of the contract to retrieve. (Path)
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Data
|
|
30
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If any required path parameters are None.
|
|
36
|
+
ClientError
|
|
37
|
+
If a 400-level error occurs.
|
|
38
|
+
NoDataFoundError
|
|
39
|
+
If a 404-level error occurs.
|
|
40
|
+
ServerError
|
|
41
|
+
If a 500-level error occurs.
|
|
42
|
+
"""
|
|
43
|
+
if not all([office_id, project_id, water_user, contract_name]):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"Office, project_id, water_user, and contract_name must be provided."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts/{contract_name}"
|
|
49
|
+
|
|
50
|
+
response = api.get(endpoint, api_version=1)
|
|
51
|
+
return Data(response)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_water_contracts(office_id: str, project_id: str, water_user: str) -> Data:
|
|
55
|
+
"""
|
|
56
|
+
Return all water contracts for the specified water user, project, and office
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
office_id : str
|
|
61
|
+
The office Id the contract is associated with. (Path)
|
|
62
|
+
project_id : str
|
|
63
|
+
The project Id the contract is associated with. (Path)
|
|
64
|
+
water_user : str
|
|
65
|
+
The water user the contract is associated with. (Path)
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
Data
|
|
70
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
ValueError
|
|
75
|
+
If any required path parameters are None.
|
|
76
|
+
ClientError
|
|
77
|
+
If a 400-level error occurs.
|
|
78
|
+
NoDataFoundError
|
|
79
|
+
If a 404-level error occurs.
|
|
80
|
+
ServerError
|
|
81
|
+
If a 500-level error occurs.
|
|
82
|
+
"""
|
|
83
|
+
if not all([office_id, project_id, water_user]):
|
|
84
|
+
raise ValueError("Office, project_id, and water_user must be provided.")
|
|
85
|
+
|
|
86
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts"
|
|
87
|
+
|
|
88
|
+
response = api.get(endpoint, api_version=1)
|
|
89
|
+
return Data(response)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_water_contract(
|
|
93
|
+
water_user: str,
|
|
94
|
+
data: JSON,
|
|
95
|
+
fail_if_exists: bool = True,
|
|
96
|
+
ignore_nulls: bool = False,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Create a new water contract
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
water_user : str
|
|
104
|
+
The water user the contract is associated with. (Path)
|
|
105
|
+
data : JSON
|
|
106
|
+
Water contract successfully stored to CWMS. (Body)
|
|
107
|
+
fail_if_exists : bool, optional
|
|
108
|
+
If true, the contract will not be stored if it already exists.
|
|
109
|
+
Default: True (Query)
|
|
110
|
+
ignore_nulls : bool, optional
|
|
111
|
+
If true, null fields will be ignored when storing the contract.
|
|
112
|
+
Default: False (Query)
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
None
|
|
117
|
+
|
|
118
|
+
Raises
|
|
119
|
+
------
|
|
120
|
+
ValueError
|
|
121
|
+
If any required path parameters are None.
|
|
122
|
+
ClientError
|
|
123
|
+
If a 400-level error occurs.
|
|
124
|
+
NoDataFoundError
|
|
125
|
+
If a 404-level error occurs.
|
|
126
|
+
ServerError
|
|
127
|
+
If a 500-level error occurs.
|
|
128
|
+
"""
|
|
129
|
+
if not water_user:
|
|
130
|
+
raise ValueError("Water User must be provided.")
|
|
131
|
+
if not data:
|
|
132
|
+
raise ValueError("Data must be provided and cannot be empty.")
|
|
133
|
+
|
|
134
|
+
# Note the office ID and project ID are not used by CDA
|
|
135
|
+
endpoint = f"projects/{water_user}/{water_user}/water-user/{water_user}/contracts"
|
|
136
|
+
params = {
|
|
137
|
+
"fail-if-exists": fail_if_exists,
|
|
138
|
+
"ignore-nulls": ignore_nulls,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
api.post(endpoint, data, params, api_version=1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def delete_water_contract(
|
|
145
|
+
office_id: str,
|
|
146
|
+
project_id: str,
|
|
147
|
+
water_user: str,
|
|
148
|
+
contract_name: str,
|
|
149
|
+
method: DeleteMethod = DeleteMethod.DELETE_KEY,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Delete a specified water contract
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
office_id : str
|
|
157
|
+
The office Id the contract is associated with. (Path)
|
|
158
|
+
project_id : str
|
|
159
|
+
The project Id the contract is associated with. (Path)
|
|
160
|
+
water_user : str
|
|
161
|
+
The water user the contract is associated with. (Path)
|
|
162
|
+
contract_name : str
|
|
163
|
+
The name of the contract to be deleted. (Path)
|
|
164
|
+
method : DeleteMethod, optional
|
|
165
|
+
Specifies the delete method used. Defaults to DELETE_KEY. (Query)
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
None
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
ValueError
|
|
174
|
+
If any required path parameters are None.
|
|
175
|
+
ClientError
|
|
176
|
+
If a 400-level error occurs.
|
|
177
|
+
NoDataFoundError
|
|
178
|
+
If a 404-level error occurs.
|
|
179
|
+
ServerError
|
|
180
|
+
If a 500-level error occurs.
|
|
181
|
+
"""
|
|
182
|
+
if not all([office_id, project_id, water_user, contract_name]):
|
|
183
|
+
raise ValueError(
|
|
184
|
+
"Office, Project ID, Water User, and Contract Name must be provided."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts/{contract_name}"
|
|
188
|
+
params = {"method": method.name}
|
|
189
|
+
|
|
190
|
+
api.delete(endpoint, params, api_version=1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def update_water_contract(
|
|
194
|
+
contract_name: str,
|
|
195
|
+
new_contract_name: str,
|
|
196
|
+
data: JSON,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Updates a water contract in CWMS.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
contract_name : str
|
|
204
|
+
The name of the contract to be updated. (Path)
|
|
205
|
+
new_contract_name : str
|
|
206
|
+
The new name of the contract. (Query)
|
|
207
|
+
data : JSON
|
|
208
|
+
Water contract successfully updated in CWMS. (Body)
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
None
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
------
|
|
216
|
+
ValueError
|
|
217
|
+
If any required path parameters are None.
|
|
218
|
+
ClientError
|
|
219
|
+
If a 400-level error occurs.
|
|
220
|
+
NoDataFoundError
|
|
221
|
+
If a 404-level error occurs.
|
|
222
|
+
ServerError
|
|
223
|
+
If a 500-level error occurs.
|
|
224
|
+
"""
|
|
225
|
+
if not all([contract_name, new_contract_name]):
|
|
226
|
+
raise ValueError("Contract Name and New Contract Name must be provided.")
|
|
227
|
+
if not data:
|
|
228
|
+
raise ValueError("Data must be provided and cannot be empty.")
|
|
229
|
+
|
|
230
|
+
endpoint = f"projects/{contract_name}/{contract_name}/water-user/{contract_name}/contracts/{contract_name}"
|
|
231
|
+
|
|
232
|
+
params = {"contract-name": new_contract_name}
|
|
233
|
+
|
|
234
|
+
api.patch(endpoint, data, params, api_version=1)
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
|
|
10
|
+
def get_water_user(office_id: str, project_id: str, water_user: str) -> Data:
|
|
11
|
+
"""
|
|
12
|
+
Gets a specified water user.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
office_id : str
|
|
17
|
+
The office Id the contract is associated with. (Path)
|
|
18
|
+
project_id : str
|
|
19
|
+
The project Id the contract is associated with. (Path)
|
|
20
|
+
water_user : str
|
|
21
|
+
The water user the contract is associated with. (Path)
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Data
|
|
26
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
ValueError
|
|
31
|
+
If any required path parameters are None.
|
|
32
|
+
ClientError
|
|
33
|
+
If a 400-level error occurs.
|
|
34
|
+
NoDataFoundError
|
|
35
|
+
If a 404-level error occurs.
|
|
36
|
+
ServerError
|
|
37
|
+
If a 500-level error occurs.
|
|
38
|
+
"""
|
|
39
|
+
if not all([office_id, project_id, water_user]):
|
|
40
|
+
raise ValueError("Office, project_id, and water_user must be provided.")
|
|
41
|
+
|
|
42
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}"
|
|
43
|
+
|
|
44
|
+
response = api.get(endpoint, api_version=1)
|
|
45
|
+
return Data(response)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_water_users(office_id: str, project_id: str) -> Data:
|
|
49
|
+
"""
|
|
50
|
+
Gets a specified water user.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
office_id : str
|
|
55
|
+
The office Id the contract is associated with. (Path)
|
|
56
|
+
project_id : str
|
|
57
|
+
The project Id the contract is associated with. (Path)
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Data
|
|
62
|
+
The JSON response from CWMS Data API wrapped in a Data object.
|
|
63
|
+
|
|
64
|
+
Raises
|
|
65
|
+
------
|
|
66
|
+
ValueError
|
|
67
|
+
If any required path parameters are None.
|
|
68
|
+
ClientError
|
|
69
|
+
If a 400-level error occurs.
|
|
70
|
+
NoDataFoundError
|
|
71
|
+
If a 404-level error occurs.
|
|
72
|
+
ServerError
|
|
73
|
+
If a 500-level error occurs.
|
|
74
|
+
"""
|
|
75
|
+
if not all([office_id, project_id]):
|
|
76
|
+
raise ValueError("Office and Project ID must be provided.")
|
|
77
|
+
|
|
78
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user"
|
|
79
|
+
|
|
80
|
+
response = api.get(endpoint, api_version=1)
|
|
81
|
+
return Data(response)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_water_user(data: JSON, fail_if_exists: bool = True) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Stores a water user to CWMS.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
data : JSON
|
|
91
|
+
Water user successfully stored to CWMS. (Body)
|
|
92
|
+
fail_if_exists : bool, optional
|
|
93
|
+
If true, the operation will fail if the water user already exists.
|
|
94
|
+
Default: true (Query)
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
None
|
|
99
|
+
|
|
100
|
+
Raises
|
|
101
|
+
------
|
|
102
|
+
ValueError
|
|
103
|
+
If any required path parameters are None.
|
|
104
|
+
ClientError
|
|
105
|
+
If a 400-level error occurs.
|
|
106
|
+
NoDataFoundError
|
|
107
|
+
If a 404-level error occurs.
|
|
108
|
+
ServerError
|
|
109
|
+
If a 500-level error occurs.
|
|
110
|
+
"""
|
|
111
|
+
if not data:
|
|
112
|
+
raise ValueError("Data must be provided and cannot be empty.")
|
|
113
|
+
|
|
114
|
+
endpoint = "projects/office/project/water-user"
|
|
115
|
+
params = {"fail-if-exists": fail_if_exists}
|
|
116
|
+
|
|
117
|
+
api.post(endpoint, data, params, api_version=1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def delete_water_user(office_id: str, project_id: str, water_user: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Deletes a specified water user.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
office_id : str
|
|
127
|
+
The office Id the contract is associated with. (Path)
|
|
128
|
+
project_id : str
|
|
129
|
+
The project Id the contract is associated with. (Path)
|
|
130
|
+
water_user : str
|
|
131
|
+
The water user the contract is associated with. (Path)
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
None
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
ValueError
|
|
140
|
+
If any required path parameters are None.
|
|
141
|
+
ClientError
|
|
142
|
+
If a 400-level error occurs.
|
|
143
|
+
NoDataFoundError
|
|
144
|
+
If a 404-level error occurs.
|
|
145
|
+
ServerError
|
|
146
|
+
If a 500-level error occurs.
|
|
147
|
+
"""
|
|
148
|
+
if not all([office_id, project_id, water_user]):
|
|
149
|
+
raise ValueError("Office, Project ID, and Water User must be provided.")
|
|
150
|
+
|
|
151
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}"
|
|
152
|
+
|
|
153
|
+
api.delete(endpoint, api_version=1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def update_water_user(
|
|
157
|
+
office_id: str, project_id: str, water_user: str, data: JSON, name: str
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Updates a water user in CWMS.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
office_id : str
|
|
165
|
+
The office Id the contract is associated with. (Path)
|
|
166
|
+
project_id : str
|
|
167
|
+
The project Id the contract is associated with. (Path)
|
|
168
|
+
water_user : str
|
|
169
|
+
The water user the contract is associated with. (Path)
|
|
170
|
+
data : JSON
|
|
171
|
+
Water user entity data in JSON format. (Body)
|
|
172
|
+
name : str
|
|
173
|
+
Specifies the new name of the water user entity. (Query)
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
None
|
|
178
|
+
|
|
179
|
+
Raises
|
|
180
|
+
------
|
|
181
|
+
ValueError
|
|
182
|
+
If any required path parameters are None.
|
|
183
|
+
ClientError
|
|
184
|
+
If a 400-level error occurs.
|
|
185
|
+
NoDataFoundError
|
|
186
|
+
If a 404-level error occurs.
|
|
187
|
+
ServerError
|
|
188
|
+
If a 500-level error occurs.
|
|
189
|
+
"""
|
|
190
|
+
if not all([office_id, project_id, water_user, name]):
|
|
191
|
+
raise ValueError("Office, Project ID, Water User, and Name must be provided.")
|
|
192
|
+
if not data:
|
|
193
|
+
raise ValueError("Data must be provided and cannot be empty.")
|
|
194
|
+
|
|
195
|
+
endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}"
|
|
196
|
+
params = {"name": name}
|
|
197
|
+
|
|
198
|
+
api.patch(endpoint, data, params, api_version=1)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import concurrent.futures
|
|
2
2
|
import logging
|
|
3
|
+
import re
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
from typing import Any, Dict, List, Optional, Tuple
|
|
5
6
|
|
|
@@ -10,6 +11,74 @@ import cwms.api as api
|
|
|
10
11
|
from cwms.catalog.catalog import get_ts_extents
|
|
11
12
|
from cwms.cwms_types import JSON, Data
|
|
12
13
|
|
|
14
|
+
_DEFAULT_CHUNK_DAYS = 365
|
|
15
|
+
_MIN_INTERVAL_MINUTES = 2
|
|
16
|
+
_FINE_INTERVAL_MINUTES = 15
|
|
17
|
+
_FINE_INTERVAL_CHUNK_DAYS = 365
|
|
18
|
+
_HOURLY_CHUNK_DAYS = 365
|
|
19
|
+
_SIX_HOURLY_CHUNK_DAYS = 1460
|
|
20
|
+
_COARSE_INTERVAL_CHUNK_DAYS = 2920
|
|
21
|
+
_INTERVAL_PATTERN = re.compile(
|
|
22
|
+
r"^(?P<count>\d+)(?P<unit>"
|
|
23
|
+
r"Minute|Minutes|Hour|Hours|Day|Days|"
|
|
24
|
+
r"Week|Weeks|Month|Months|Year|Years)$"
|
|
25
|
+
)
|
|
26
|
+
_INTERVAL_MINUTES = {
|
|
27
|
+
"Minute": 1,
|
|
28
|
+
"Minutes": 1,
|
|
29
|
+
"Hour": 60,
|
|
30
|
+
"Hours": 60,
|
|
31
|
+
"Day": 24 * 60,
|
|
32
|
+
"Days": 24 * 60,
|
|
33
|
+
"Week": 7 * 24 * 60,
|
|
34
|
+
"Weeks": 7 * 24 * 60,
|
|
35
|
+
"Month": 30 * 24 * 60,
|
|
36
|
+
"Months": 30 * 24 * 60,
|
|
37
|
+
"Year": 365 * 24 * 60,
|
|
38
|
+
"Years": 365 * 24 * 60,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_timeseries_chunk_size(ts_id: str) -> timedelta:
|
|
43
|
+
"""Return the default request chunk size for a time series interval.
|
|
44
|
+
|
|
45
|
+
Local regular time series intervals, such as ``~15Minutes``, use the same
|
|
46
|
+
chunk size as their regular interval. Unrecognized intervals retain the
|
|
47
|
+
conservative default used for 15-minute through hourly data.
|
|
48
|
+
"""
|
|
49
|
+
ts_id_parts = ts_id.split(".")
|
|
50
|
+
if len(ts_id_parts) < 4:
|
|
51
|
+
return timedelta(days=_DEFAULT_CHUNK_DAYS)
|
|
52
|
+
|
|
53
|
+
interval = ts_id_parts[3].removeprefix("~")
|
|
54
|
+
match = _INTERVAL_PATTERN.fullmatch(interval)
|
|
55
|
+
if match is None:
|
|
56
|
+
return timedelta(days=_DEFAULT_CHUNK_DAYS)
|
|
57
|
+
|
|
58
|
+
interval_minutes = (
|
|
59
|
+
int(match.group("count")) * _INTERVAL_MINUTES[match.group("unit")]
|
|
60
|
+
)
|
|
61
|
+
if interval_minutes < _MIN_INTERVAL_MINUTES:
|
|
62
|
+
return timedelta(days=_DEFAULT_CHUNK_DAYS)
|
|
63
|
+
|
|
64
|
+
# These bands balance request overhead and response size based on production
|
|
65
|
+
# CDA timings. Fine intervals scale toward about 35,000 expected values.
|
|
66
|
+
if interval_minutes < _FINE_INTERVAL_MINUTES:
|
|
67
|
+
chunk_days = max(
|
|
68
|
+
1,
|
|
69
|
+
round(
|
|
70
|
+
_FINE_INTERVAL_CHUNK_DAYS * interval_minutes / _FINE_INTERVAL_MINUTES
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
elif interval_minutes <= 60:
|
|
74
|
+
chunk_days = _HOURLY_CHUNK_DAYS
|
|
75
|
+
elif interval_minutes <= 6 * 60:
|
|
76
|
+
chunk_days = _SIX_HOURLY_CHUNK_DAYS
|
|
77
|
+
else:
|
|
78
|
+
chunk_days = _COARSE_INTERVAL_CHUNK_DAYS
|
|
79
|
+
|
|
80
|
+
return timedelta(days=chunk_days)
|
|
81
|
+
|
|
13
82
|
|
|
14
83
|
def get_multi_timeseries_df(
|
|
15
84
|
ts_ids: list[str],
|
|
@@ -136,6 +205,9 @@ def chunk_timeseries_time_range(
|
|
|
136
205
|
List[Tuple[datetime, datetime]]
|
|
137
206
|
A list of tuples, where each tuple represents the start and end of a chunk.
|
|
138
207
|
"""
|
|
208
|
+
if chunk_size <= timedelta(0):
|
|
209
|
+
raise ValueError("chunk_size must be greater than zero")
|
|
210
|
+
|
|
139
211
|
chunks = []
|
|
140
212
|
current = begin
|
|
141
213
|
while current < end:
|
|
@@ -298,7 +370,7 @@ def get_timeseries(
|
|
|
298
370
|
trim: Optional[bool] = True,
|
|
299
371
|
multithread: Optional[bool] = True,
|
|
300
372
|
max_workers: int = 20,
|
|
301
|
-
max_days_per_chunk: int =
|
|
373
|
+
max_days_per_chunk: Optional[int] = None,
|
|
302
374
|
) -> Data:
|
|
303
375
|
"""Retrieves time series values from a specified time series and time window. Value date-times
|
|
304
376
|
obtained are always in UTC.
|
|
@@ -338,15 +410,18 @@ def get_timeseries(
|
|
|
338
410
|
trim: boolean, optional, default is True
|
|
339
411
|
Specifies whether to trim missing values from the beginning and end of the retrieved values.
|
|
340
412
|
multithread: boolean, optional, default is True
|
|
341
|
-
Specifies whether to
|
|
413
|
+
Specifies whether to retrieve time series chunks concurrently.
|
|
342
414
|
max_workers: integer, default is 20
|
|
343
|
-
The maximum number of worker threads
|
|
344
|
-
max_days_per_chunk: integer, default is
|
|
345
|
-
The maximum number of days
|
|
415
|
+
The maximum number of worker threads used for concurrent requests.
|
|
416
|
+
max_days_per_chunk: integer, optional, default is None
|
|
417
|
+
The maximum number of days included in each request. By default,
|
|
418
|
+
the chunk size is selected from the time series interval.
|
|
346
419
|
Returns
|
|
347
420
|
-------
|
|
348
421
|
cwms data type. data.json will return the JSON output and data.df will return a dataframe. dates are all in UTC
|
|
349
422
|
"""
|
|
423
|
+
if max_days_per_chunk is not None and max_days_per_chunk <= 0:
|
|
424
|
+
raise ValueError("max_days_per_chunk must be greater than zero")
|
|
350
425
|
|
|
351
426
|
selector = "values"
|
|
352
427
|
endpoint = "timeseries"
|
|
@@ -386,8 +461,12 @@ def get_timeseries(
|
|
|
386
461
|
)
|
|
387
462
|
return Data(response, selector=selector)
|
|
388
463
|
|
|
389
|
-
|
|
390
|
-
|
|
464
|
+
chunk_size = (
|
|
465
|
+
timedelta(days=max_days_per_chunk)
|
|
466
|
+
if max_days_per_chunk is not None
|
|
467
|
+
else get_timeseries_chunk_size(ts_id)
|
|
468
|
+
)
|
|
469
|
+
chunks = chunk_timeseries_time_range(begin, end, chunk_size)
|
|
391
470
|
|
|
392
471
|
# find max worker thread
|
|
393
472
|
max_workers = max(min(len(chunks), max_workers), 1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|