cwms-python 1.0.7__tar.gz → 1.0.8__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.8}/PKG-INFO +1 -1
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries.py +52 -29
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_group.py +30 -8
- {cwms_python-1.0.7 → cwms_python-1.0.8}/pyproject.toml +1 -1
- {cwms_python-1.0.7 → cwms_python-1.0.8}/LICENSE +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/README.md +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/__init__.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/api.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/blobs.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/catalog.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/clobs.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/cwms_types.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/forecast/forecast_instance.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/forecast/forecast_spec.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/levels/location_levels.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/levels/specified_levels.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/gate_changes.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/location_groups.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/physical_locations.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/measurements/measurements.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/outlets/outlets.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/outlets/virtual_outlets.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/project_lock_rights.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/project_locks.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/projects.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/water_supply/accounting.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings_spec.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings_template.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/standard_text/standard_text.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_bin.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_identifier.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile_instance.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile_parser.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_txt.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/turbines/turbines.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/users/users.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/utils/__init__.py +0 -0
- {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/utils/checks.py +0 -0
|
@@ -154,6 +154,23 @@ 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
|
+
if i == attempts - 1:
|
|
170
|
+
raise
|
|
171
|
+
logging.warning(f"chunk attempt {i + 1}/{attempts} failed: {e}")
|
|
172
|
+
|
|
173
|
+
|
|
157
174
|
def fetch_timeseries_chunks(
|
|
158
175
|
chunks: List[Tuple[datetime, datetime]],
|
|
159
176
|
params: Dict[str, Any],
|
|
@@ -161,14 +178,13 @@ def fetch_timeseries_chunks(
|
|
|
161
178
|
endpoint: str,
|
|
162
179
|
max_workers: int,
|
|
163
180
|
) -> List[Data]:
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
results: List[Data] = []
|
|
182
|
+
errors: List[str] = []
|
|
166
183
|
|
|
167
|
-
# Create a ThreadPoolExecutor to manage multithreading
|
|
168
184
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
169
|
-
# Submit tasks for each chunk to the api
|
|
170
185
|
future_to_chunk = {
|
|
171
186
|
executor.submit(
|
|
187
|
+
_call_with_retry,
|
|
172
188
|
get_timeseries_chunk,
|
|
173
189
|
selector,
|
|
174
190
|
endpoint,
|
|
@@ -179,18 +195,23 @@ def fetch_timeseries_chunks(
|
|
|
179
195
|
for chunk_start, chunk_end in chunks
|
|
180
196
|
}
|
|
181
197
|
|
|
182
|
-
# Process completed threads as they finish
|
|
183
198
|
for future in concurrent.futures.as_completed(future_to_chunk):
|
|
199
|
+
chunk_start, chunk_end = future_to_chunk[future]
|
|
184
200
|
try:
|
|
185
|
-
|
|
186
|
-
result = future.result()
|
|
187
|
-
results.append(result)
|
|
201
|
+
results.append(future.result())
|
|
188
202
|
except Exception as e:
|
|
189
|
-
|
|
190
|
-
# Log or handle any errors that occur during execution
|
|
191
|
-
logging.error(
|
|
203
|
+
error_msg = (
|
|
192
204
|
f"Failed to fetch data from {chunk_start} to {chunk_end}: {e}"
|
|
193
205
|
)
|
|
206
|
+
logging.error(error_msg)
|
|
207
|
+
errors.append(error_msg)
|
|
208
|
+
|
|
209
|
+
if errors:
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
f"{len(errors)} of {len(chunks)} chunk(s) failed to fetch:\n"
|
|
212
|
+
+ "\n".join(errors)
|
|
213
|
+
)
|
|
214
|
+
|
|
194
215
|
return results
|
|
195
216
|
|
|
196
217
|
|
|
@@ -669,29 +690,31 @@ def store_timeseries(
|
|
|
669
690
|
|
|
670
691
|
# Store chunks concurrently
|
|
671
692
|
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
|
|
693
|
+
errors: List[str] = []
|
|
684
694
|
|
|
685
|
-
|
|
695
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
|
696
|
+
future_to_chunk = {
|
|
697
|
+
executor.submit(_call_with_retry, api.post, endpoint, chunk, params): chunk
|
|
698
|
+
for chunk in chunks
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
for future in concurrent.futures.as_completed(future_to_chunk):
|
|
702
|
+
chunk = future_to_chunk[future]
|
|
686
703
|
try:
|
|
687
|
-
responses.append({"success
|
|
704
|
+
responses.append({"success": future.result()})
|
|
688
705
|
except Exception as e:
|
|
689
706
|
start_time = chunk["values"][0][0]
|
|
690
707
|
end_time = chunk["values"][-1][0]
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
responses.append({"error":
|
|
708
|
+
error_msg = f"Error storing chunk from {start_time} to {end_time}: {e}"
|
|
709
|
+
logging.error(error_msg)
|
|
710
|
+
errors.append(error_msg)
|
|
711
|
+
responses.append({"error": error_msg})
|
|
712
|
+
|
|
713
|
+
if errors:
|
|
714
|
+
raise RuntimeError(
|
|
715
|
+
f"{len(errors)} of {len(chunks)} chunk(s) failed to store:\n"
|
|
716
|
+
+ "\n".join(errors)
|
|
717
|
+
)
|
|
695
718
|
|
|
696
719
|
return
|
|
697
720
|
|
|
@@ -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)
|
|
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
|