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.
Files changed (40) hide show
  1. {cwms_python-1.0.7 → cwms_python-1.0.8}/PKG-INFO +1 -1
  2. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries.py +52 -29
  3. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_group.py +30 -8
  4. {cwms_python-1.0.7 → cwms_python-1.0.8}/pyproject.toml +1 -1
  5. {cwms_python-1.0.7 → cwms_python-1.0.8}/LICENSE +0 -0
  6. {cwms_python-1.0.7 → cwms_python-1.0.8}/README.md +0 -0
  7. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/__init__.py +0 -0
  8. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/api.py +0 -0
  9. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/blobs.py +0 -0
  10. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/catalog.py +0 -0
  11. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/catalog/clobs.py +0 -0
  12. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/cwms_types.py +0 -0
  13. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/forecast/forecast_instance.py +0 -0
  14. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/forecast/forecast_spec.py +0 -0
  15. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/levels/location_levels.py +0 -0
  16. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/levels/specified_levels.py +0 -0
  17. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/gate_changes.py +0 -0
  18. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/location_groups.py +0 -0
  19. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/locations/physical_locations.py +0 -0
  20. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/measurements/measurements.py +0 -0
  21. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/outlets/outlets.py +0 -0
  22. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/outlets/virtual_outlets.py +0 -0
  23. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/project_lock_rights.py +0 -0
  24. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/project_locks.py +0 -0
  25. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/projects.py +0 -0
  26. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/projects/water_supply/accounting.py +0 -0
  27. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings.py +0 -0
  28. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings_spec.py +0 -0
  29. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/ratings/ratings_template.py +0 -0
  30. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/standard_text/standard_text.py +0 -0
  31. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_bin.py +0 -0
  32. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_identifier.py +0 -0
  33. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile.py +0 -0
  34. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile_instance.py +0 -0
  35. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_profile_parser.py +0 -0
  36. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/timeseries/timeseries_txt.py +0 -0
  37. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/turbines/turbines.py +0 -0
  38. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/users/users.py +0 -0
  39. {cwms_python-1.0.7 → cwms_python-1.0.8}/cwms/utils/__init__.py +0 -0
  40. {cwms_python-1.0.7 → cwms_python-1.0.8}/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.7
3
+ Version: 1.0.8
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
@@ -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
- # Initialize an empty list to store results
165
- results = []
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
- # Retrieve the result of the completed future
186
- result = future.result()
187
- results.append(result)
201
+ results.append(future.result())
188
202
  except Exception as e:
189
- chunk_start, chunk_end = future_to_chunk[future]
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
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
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
- for future in concurrent.futures.as_completed(futures):
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:": future.result()})
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
- logging.error(
692
- f"Error storing chunk from {start_time} to {end_time}: {e}"
693
- )
694
- responses.append({"error": str(e)})
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
- category_id: string
64
- The category id that contains the timeseries group.
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
- "timeseries_group_like": timeseries_group_like,
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(data: JSON, fail_if_exists: Optional[bool] = True) -> None:
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(group_id: str, category_id: str, office_id: str) -> None:
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,7 @@
2
2
  name = "cwms-python"
3
3
  repository = "https://github.com/HydrologicEngineeringCenter/cwms-python"
4
4
 
5
- version = "1.0.7"
5
+ version = "1.0.8"
6
6
 
7
7
  packages = [
8
8
  { include = "cwms" },
File without changes
File without changes
File without changes