cwms-python 0.7.0__tar.gz → 0.8.0__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 (39) hide show
  1. {cwms_python-0.7.0 → cwms_python-0.8.0}/PKG-INFO +1 -1
  2. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/__init__.py +1 -0
  3. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/api.py +3 -2
  4. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/cwms_types.py +43 -2
  5. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/measurements/measurements.py +34 -2
  6. cwms_python-0.8.0/cwms/projects/water_supply/accounting.py +145 -0
  7. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries.py +39 -34
  8. {cwms_python-0.7.0 → cwms_python-0.8.0}/pyproject.toml +1 -1
  9. {cwms_python-0.7.0 → cwms_python-0.8.0}/LICENSE +0 -0
  10. {cwms_python-0.7.0 → cwms_python-0.8.0}/README.md +0 -0
  11. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/blobs.py +0 -0
  12. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/catalog.py +0 -0
  13. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/clobs.py +0 -0
  14. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/forecast/forecast_instance.py +0 -0
  15. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/forecast/forecast_spec.py +0 -0
  16. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/levels/location_levels.py +0 -0
  17. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/levels/specified_levels.py +0 -0
  18. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/gate_changes.py +0 -0
  19. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/location_groups.py +0 -0
  20. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/physical_locations.py +0 -0
  21. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/outlets/outlets.py +0 -0
  22. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/outlets/virtual_outlets.py +0 -0
  23. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/project_lock_rights.py +0 -0
  24. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/project_locks.py +0 -0
  25. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/projects.py +0 -0
  26. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings.py +0 -0
  27. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings_spec.py +0 -0
  28. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings_template.py +0 -0
  29. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/standard_text/standard_text.py +0 -0
  30. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_bin.py +0 -0
  31. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_group.py +0 -0
  32. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_identifier.py +0 -0
  33. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile.py +0 -0
  34. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile_instance.py +0 -0
  35. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile_parser.py +0 -0
  36. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_txt.py +0 -0
  37. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/turbines/turbines.py +0 -0
  38. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/utils/__init__.py +0 -0
  39. {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/utils/checks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cwms-python
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Corps water management systems (CWMS) REST API for Data Retrieval of USACE water data
5
5
  License: LICENSE
6
6
  Keywords: USACE,water data,CWMS
@@ -17,6 +17,7 @@ from cwms.outlets.virtual_outlets import *
17
17
  from cwms.projects.project_lock_rights import *
18
18
  from cwms.projects.project_locks import *
19
19
  from cwms.projects.projects import *
20
+ from cwms.projects.water_supply.accounting import *
20
21
  from cwms.ratings.ratings import *
21
22
  from cwms.ratings.ratings_spec import *
22
23
  from cwms.ratings.ratings_template import *
@@ -131,7 +131,6 @@ def init_session(
131
131
  """
132
132
 
133
133
  global SESSION
134
-
135
134
  if api_root:
136
135
  logging.debug(f"Initializing root URL: api_root={api_root}")
137
136
  SESSION = sessions.BaseUrlSession(base_url=api_root)
@@ -142,8 +141,10 @@ def init_session(
142
141
  )
143
142
  SESSION.mount("https://", adapter)
144
143
  if api_key:
144
+ if api_key.startswith("apikey "):
145
+ api_key = api_key.replace("apikey ", "")
145
146
  logging.debug(f"Setting authorization key: api_key={api_key}")
146
- SESSION.headers.update({"Authorization": api_key})
147
+ SESSION.headers.update({"Authorization": "apikey " + api_key})
147
148
 
148
149
  return SESSION
149
150
 
@@ -62,8 +62,8 @@ class Data:
62
62
  def rating_type(data: JSON) -> DataFrame:
63
63
  # grab the correct point values for a rating table
64
64
  df = DataFrame(data["point"]) if data["point"] else DataFrame()
65
- df = df.apply(to_numeric)
66
- return df
65
+ df_numeric = df.apply(to_numeric, axis=0, result_type="expand")
66
+ return DataFrame(df_numeric)
67
67
 
68
68
  def timeseries_type(orig_json: JSON, value_json: JSON) -> DataFrame:
69
69
  # if timeseries values are present then grab the values and put into
@@ -79,6 +79,44 @@ class Data:
79
79
  df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
80
80
  return df
81
81
 
82
+ def reorder_measurement_cols(df: DataFrame) -> DataFrame:
83
+ # reorders measurement columns for usability
84
+
85
+ # Define the columns to bring to the front
86
+ front_columns = [
87
+ "id.office-id",
88
+ "id.name",
89
+ "number",
90
+ "instant",
91
+ "streamflow-measurement.gage-height",
92
+ "streamflow-measurement.flow",
93
+ "streamflow-measurement.quality",
94
+ "used",
95
+ "agency",
96
+ "wm-comments",
97
+ ]
98
+
99
+ # Identify columns containing 'unit' to be last
100
+ unit_columns = [col for col in df.columns if "unit" in col]
101
+
102
+ # Identify remaining columns (not in front_columns or unit_columns)
103
+ remaining_columns = [
104
+ col
105
+ for col in df.columns
106
+ if col not in front_columns and col not in unit_columns
107
+ ]
108
+
109
+ # Construct the new column order
110
+ new_column_order = front_columns + remaining_columns + unit_columns
111
+
112
+ # Filter out columns that might not actually exist in the DataFrame.
113
+ existing_columns = [col for col in new_column_order if col in df.columns]
114
+
115
+ # Reorder the DataFrame
116
+ df = df[existing_columns]
117
+
118
+ return df
119
+
82
120
  data = deepcopy(json)
83
121
 
84
122
  if selector:
@@ -95,6 +133,9 @@ class Data:
95
133
  df = json_normalize(df_data) if df_data else DataFrame()
96
134
  else:
97
135
  df = json_normalize(data)
136
+ # if streamflow-measurement reorder columns
137
+ if "streamflow-measurement.flow" in df.columns:
138
+ df = reorder_measurement_cols(df)
98
139
 
99
140
  return df
100
141
 
@@ -116,8 +116,15 @@ def store_measurements(
116
116
  "fail-if-exists": fail_if_exists,
117
117
  }
118
118
 
119
- if not isinstance(data, dict):
120
- raise ValueError("Cannot store a timeseries without a JSON data dictionary")
119
+ if not isinstance(data, list):
120
+ raise ValueError(
121
+ "Cannot store a measurement without a JSON list, object is not a list of dictionaries"
122
+ )
123
+ for item in data:
124
+ if not isinstance(item, dict):
125
+ raise ValueError(
126
+ "Cannot store a measurement without a JSON list: a non-dictionary object was found"
127
+ )
121
128
 
122
129
  return api.post(endpoint, data, params, api_version=1)
123
130
 
@@ -175,3 +182,28 @@ def delete_measurements(
175
182
  }
176
183
 
177
184
  return api.delete(endpoint, params, api_version=1)
185
+
186
+
187
+ def get_measurements_extents(
188
+ office_mask: Optional[str] = None,
189
+ ) -> Data:
190
+ """Get time extents of streamflow measurements
191
+
192
+ Parameters
193
+ ----------
194
+ office_mask: string
195
+ Office Id used to filter the results.
196
+
197
+ Returns
198
+ -------
199
+ cwms data type. data.json will return the JSON output and data.df will return a dataframe. Dates returned are all in UTC.
200
+
201
+ """
202
+ endpoint = "measurements/time-extents"
203
+
204
+ params = {
205
+ "office-mask": office_mask,
206
+ }
207
+
208
+ response = api.get(endpoint, params, api_version=1)
209
+ return Data(response) # , selector=selector)
@@ -0,0 +1,145 @@
1
+ # Copyright (c) 2024
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
+ import cwms.api as api
6
+ from cwms.cwms_types import JSON, Data
7
+
8
+
9
+ def get_pump_accounting(
10
+ office_id: str,
11
+ project_id: str,
12
+ water_user: str,
13
+ contract_name: str,
14
+ start: str,
15
+ end: str,
16
+ timezone: str = "UTC",
17
+ unit: str = "cms",
18
+ start_time_inclusive: bool = True,
19
+ end_time_inclusive: bool = True,
20
+ ascending: bool = True,
21
+ row_limit: int = 0,
22
+ ) -> Data:
23
+ """
24
+ Retrieves pump accounting entries associated with a water supply contract.
25
+
26
+ Parameters
27
+ ----------
28
+ office_id : str
29
+ The office ID the pump accounting is associated with. (Path)
30
+ project_id : str
31
+ The project ID the pump accounting is associated with. (Path)
32
+ water_user : str
33
+ The water user the pump accounting is associated with. (Path)
34
+ contract_name : str
35
+ The name of the contract associated with the pump accounting. (Path)
36
+ start : str
37
+ The start time of the time window for pump accounting entries to retrieve.
38
+ Format: ISO 8601 extended, with optional offset and timezone. (Query)
39
+ end : str
40
+ The end time of the time window for pump accounting entries to retrieve.
41
+ Format: ISO 8601 extended, with optional offset and timezone. (Query)
42
+ timezone : str, optional
43
+ The default timezone to use if `start` or `end` lacks offset/timezone info.
44
+ Defaults to "UTC". (Query)
45
+ unit : str, optional
46
+ Unit of flow rate for accounting entries. Defaults to "cms". (Query)
47
+ start_time_inclusive : bool, optional
48
+ Whether the start time is inclusive. Defaults to True. (Query)
49
+ end_time_inclusive : bool, optional
50
+ Whether the end time is inclusive. Defaults to True. (Query)
51
+ ascending : bool, optional
52
+ Whether entries should be returned in ascending order. Defaults to True. (Query)
53
+ row_limit : int, optional
54
+ Maximum number of rows to return. Defaults to 0, meaning no limit. (Query)
55
+
56
+ Returns
57
+ -------
58
+ Data
59
+ The JSON response from CWMS Data API wrapped in a Data object.
60
+
61
+ Raises
62
+ ------
63
+ ValueError
64
+ If any required path parameters are None.
65
+ ClientError
66
+ If a 400-level error occurs.
67
+ NoDataFoundError
68
+ If a 404-level error occurs.
69
+ ServerError
70
+ If a 500-level error occurs.
71
+ """
72
+ if not all([office_id, project_id, water_user, contract_name, start, end]):
73
+ raise ValueError("All required parameters must be provided.")
74
+
75
+ endpoint = f"projects/{office_id}/{project_id}/water-user/{water_user}/contracts/{contract_name}/accounting"
76
+
77
+ params: dict[str, str | int] = {
78
+ "start": start,
79
+ "end": end,
80
+ "timezone": timezone,
81
+ "unit": unit,
82
+ "start-time-inclusive": str(start_time_inclusive).lower(),
83
+ "end-time-inclusive": str(end_time_inclusive).lower(),
84
+ "ascending": str(ascending).lower(),
85
+ "row-limit": row_limit,
86
+ }
87
+
88
+ response = api.get(endpoint, params, api_version=1)
89
+ return Data(response)
90
+
91
+
92
+ def store_pump_accounting(
93
+ office: str,
94
+ project_id: str,
95
+ water_user: str,
96
+ contract_name: str,
97
+ data: JSON,
98
+ ) -> None:
99
+ """
100
+ Creates a new pump accounting entry associated with a water supply contract.
101
+
102
+ Parameters
103
+ ----------
104
+ office : str
105
+ The office ID the accounting is associated with. (Path)
106
+ project_id : str
107
+ The project ID the accounting is associated with. (Path)
108
+ water_user : str
109
+ The water user the accounting is associated with. (Path)
110
+ contract_name : str
111
+ The name of the contract associated with the accounting. (Path)
112
+ data : dict
113
+ A dictionary representing the JSON data to be stored. This should match the
114
+ WaterSupplyAccounting structure as defined by the API.
115
+
116
+ Returns
117
+ -------
118
+ None
119
+
120
+ Raises
121
+ ------
122
+ ValueError
123
+ If any required argument is missing.
124
+ ClientError
125
+ If a 400 range error code response is returned from the server.
126
+ NoDataFoundError
127
+ If a 404 range error code response is returned from the server.
128
+ ServerError
129
+ If a 500 range error code response is returned from the server.
130
+ """
131
+ if not all([office, project_id, water_user, contract_name]):
132
+ raise ValueError(
133
+ "Office, project_id, water_user, and contract_name must be provided."
134
+ )
135
+ if not data:
136
+ raise ValueError("Data must be provided and cannot be empty.")
137
+
138
+ endpoint = f"projects/{office}/{project_id}/water-user/{water_user}/contracts/{contract_name}/accounting"
139
+ params = {
140
+ "office": office,
141
+ "project-id": project_id,
142
+ "water-user": water_user,
143
+ "contract-name": contract_name,
144
+ }
145
+ api.post(endpoint, data, params)
@@ -247,64 +247,69 @@ def timeseries_df_to_json(
247
247
  df = df.reindex(columns=["date-time", "value", "quality-code"])
248
248
  if df.isnull().values.any():
249
249
  raise ValueError("Null/NaN data must be removed from the dataframe")
250
-
250
+ if version_date:
251
+ version_date_iso = version_date.isoformat()
252
+ else:
253
+ version_date_iso = None
251
254
  ts_dict = {
252
255
  "name": ts_id,
253
256
  "office-id": office_id,
254
257
  "units": units,
255
258
  "values": df.values.tolist(),
256
- "version-date": version_date,
259
+ "version-date": version_date_iso,
257
260
  }
258
261
 
259
262
  return ts_dict
260
263
 
261
264
 
262
265
  def store_multi_timeseries_df(
263
- ts_data: pd.DataFrame, office_id: str, max_workers: Optional[int] = 30
266
+ data: pd.DataFrame, office_id: str, max_workers: Optional[int] = 30
264
267
  ) -> None:
265
-
266
268
  def store_ts_ids(
267
269
  data: pd.DataFrame,
268
270
  ts_id: str,
269
271
  office_id: str,
270
272
  version_date: Optional[datetime] = None,
271
273
  ) -> None:
272
- units = data["units"].iloc[0]
273
- data_json = timeseries_df_to_json(
274
- data=data,
275
- ts_id=ts_id,
276
- units=units,
277
- office_id=office_id,
278
- version_date=version_date,
279
- )
280
- store_timeseries(data=data_json)
274
+ try:
275
+ units = data["units"].iloc[0]
276
+ data_json = timeseries_df_to_json(
277
+ data=data,
278
+ ts_id=ts_id,
279
+ units=units,
280
+ office_id=office_id,
281
+ version_date=version_date,
282
+ )
283
+ store_timeseries(data=data_json)
284
+ except Exception as e:
285
+ print(f"Error processing {ts_id}: {e}")
281
286
  return None
282
287
 
288
+ ts_data_all = data.copy()
289
+ if "version_date" not in ts_data_all.columns:
290
+ ts_data_all = ts_data_all.assign(version_date=pd.to_datetime(pd.Series([])))
283
291
  unique_tsids = (
284
- ts_data["ts_id"].astype(str) + ":" + ts_data["version_date"].astype(str)
292
+ ts_data_all["ts_id"].astype(str) + ":" + ts_data_all["version_date"].astype(str)
285
293
  ).unique()
286
294
 
287
295
  with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
288
- for ts_id_all in unique_tsids:
289
- try:
290
- ts_id, version_date = ts_id_all.split(":", 1)
291
- if version_date != "NaT":
292
- version_date_dt = pd.to_datetime(version_date)
293
- data = ts_data[
294
- (ts_data["ts_id"] == ts_id)
295
- & (ts_data["version_date"] == version_date_dt)
296
- ]
297
- else:
298
- version_date_dt = None
299
- data = ts_data[
300
- (ts_data["ts_id"] == ts_id) & ts_data["version_date"].isna()
301
- ]
302
- if not data.empty:
303
- executor.submit(
304
- store_ts_ids, data, ts_id, office_id, version_date_dt
305
- )
306
- except Exception as e:
307
- print(f"Error processing {ts_id}: {e}")
296
+ for unique_tsid in unique_tsids:
297
+ ts_id, version_date = unique_tsid.split(":", 1)
298
+ if version_date != "NaT":
299
+ version_date_dt = pd.to_datetime(version_date)
300
+ ts_data = ts_data_all[
301
+ (ts_data_all["ts_id"] == ts_id)
302
+ & (ts_data_all["version_date"] == version_date_dt)
303
+ ]
304
+ else:
305
+ version_date_dt = None
306
+ ts_data = ts_data_all[
307
+ (ts_data_all["ts_id"] == ts_id) & ts_data_all["version_date"].isna()
308
+ ]
309
+ if not data.empty:
310
+ executor.submit(
311
+ store_ts_ids, ts_data, ts_id, office_id, version_date_dt
312
+ )
308
313
 
309
314
 
310
315
  def store_timeseries(
@@ -2,7 +2,7 @@
2
2
  name = "cwms-python"
3
3
  repository = "https://github.com/HydrologicEngineeringCenter/cwms-python"
4
4
 
5
- version = "0.7.0"
5
+ version = "0.8.0"
6
6
 
7
7
 
8
8
  packages = [
File without changes
File without changes