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.
Files changed (45) hide show
  1. {cwms_python-1.0.10 → cwms_python-1.1.1}/PKG-INFO +1 -1
  2. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/__init__.py +3 -0
  3. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/lookups.py +2 -0
  4. cwms_python-1.1.1/cwms/locks/locks.py +198 -0
  5. cwms_python-1.1.1/cwms/projects/water_supply/water_contracts.py +234 -0
  6. cwms_python-1.1.1/cwms/projects/water_supply/water_users.py +198 -0
  7. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries.py +86 -7
  8. {cwms_python-1.0.10 → cwms_python-1.1.1}/pyproject.toml +1 -1
  9. {cwms_python-1.0.10 → cwms_python-1.1.1}/LICENSE +0 -0
  10. {cwms_python-1.0.10 → cwms_python-1.1.1}/README.md +0 -0
  11. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/api.py +0 -0
  12. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/blobs.py +0 -0
  13. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/catalog.py +0 -0
  14. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/catalog/clobs.py +0 -0
  15. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/cwms_types.py +0 -0
  16. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/forecast/forecast_instance.py +0 -0
  17. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/forecast/forecast_spec.py +0 -0
  18. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/levels/location_levels.py +0 -0
  19. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/levels/specified_levels.py +0 -0
  20. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/gate_changes.py +0 -0
  21. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/location_groups.py +0 -0
  22. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/locations/physical_locations.py +0 -0
  23. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/measurements/measurements.py +0 -0
  24. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/outlets/outlets.py +0 -0
  25. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/outlets/virtual_outlets.py +0 -0
  26. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/project_lock_rights.py +0 -0
  27. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/project_locks.py +0 -0
  28. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/projects.py +0 -0
  29. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/projects/water_supply/accounting.py +0 -0
  30. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/properties/properties.py +0 -0
  31. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings.py +0 -0
  32. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings_spec.py +0 -0
  33. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/ratings/ratings_template.py +0 -0
  34. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/standard_text/standard_text.py +0 -0
  35. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_bin.py +0 -0
  36. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_group.py +0 -0
  37. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_identifier.py +0 -0
  38. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile.py +0 -0
  39. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile_instance.py +0 -0
  40. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_profile_parser.py +0 -0
  41. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/timeseries/timeseries_txt.py +0 -0
  42. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/turbines/turbines.py +0 -0
  43. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/users/users.py +0 -0
  44. {cwms_python-1.0.10 → cwms_python-1.1.1}/cwms/utils/__init__.py +0 -0
  45. {cwms_python-1.0.10 → cwms_python-1.1.1}/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.10
3
+ Version: 1.1.1
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
@@ -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 *
@@ -131,6 +131,8 @@ def delete_lookup(
131
131
 
132
132
  Parameters
133
133
  ----------
134
+ display_value : str
135
+ Specifies the display value of the lookup type to be deleted.
134
136
  category : str
135
137
  Specifies the category id of the lookup type to be deleted.
136
138
  prefix : str
@@ -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 = 14,
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 trim missing values from the beginning and end of the retrieved values.
413
+ Specifies whether to retrieve time series chunks concurrently.
342
414
  max_workers: integer, default is 20
343
- The maximum number of worker threads that will be spawned for multithreading, If calling more than 3 years of 15 minute data, consider using 30 max_workers
344
- max_days_per_chunk: integer, default is 14
345
- The maximum number of days that would be included in a thread. If calling more than 1 year of 15 minute data, consider using 30 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
- # divide the time range into chunks
390
- chunks = chunk_timeseries_time_range(begin, end, timedelta(days=max_days_per_chunk))
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)
@@ -3,7 +3,7 @@ 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.10"
6
+ version = "1.1.1"
7
7
 
8
8
  packages = [
9
9
  { include = "cwms" },
File without changes
File without changes
File without changes