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.
- {cwms_python-0.7.0 → cwms_python-0.8.0}/PKG-INFO +1 -1
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/__init__.py +1 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/api.py +3 -2
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/cwms_types.py +43 -2
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/measurements/measurements.py +34 -2
- cwms_python-0.8.0/cwms/projects/water_supply/accounting.py +145 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries.py +39 -34
- {cwms_python-0.7.0 → cwms_python-0.8.0}/pyproject.toml +1 -1
- {cwms_python-0.7.0 → cwms_python-0.8.0}/LICENSE +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/README.md +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/blobs.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/catalog.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/catalog/clobs.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/forecast/forecast_instance.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/forecast/forecast_spec.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/levels/location_levels.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/levels/specified_levels.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/gate_changes.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/location_groups.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/locations/physical_locations.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/outlets/outlets.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/outlets/virtual_outlets.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/project_lock_rights.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/project_locks.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/projects/projects.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings_spec.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/ratings/ratings_template.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/standard_text/standard_text.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_bin.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_group.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_identifier.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile_instance.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_profile_parser.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/timeseries/timeseries_txt.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/turbines/turbines.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/utils/__init__.py +0 -0
- {cwms_python-0.7.0 → cwms_python-0.8.0}/cwms/utils/checks.py +0 -0
|
@@ -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
|
-
|
|
66
|
-
return
|
|
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,
|
|
120
|
-
raise ValueError(
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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(
|
|
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
|