cwms-python 0.3.0__tar.gz → 0.5.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.3.0 → cwms_python-0.5.0}/PKG-INFO +2 -2
- {cwms_python-0.3.0 → cwms_python-0.5.0}/README.md +1 -1
- cwms_python-0.5.0/cwms/__init__.py +27 -0
- {cwms_python-0.3.0 → cwms_python-0.5.0}/cwms/api.py +110 -21
- cwms_python-0.5.0/cwms/catalog/catalog.py +128 -0
- cwms_python-0.5.0/cwms/cwms_types.py +107 -0
- cwms_python-0.5.0/cwms/forecast/forecast_instance.py +208 -0
- cwms_python-0.5.0/cwms/forecast/forecast_spec.py +181 -0
- cwms_python-0.5.0/cwms/levels/location_levels.py +221 -0
- cwms_python-0.5.0/cwms/levels/specified_levels.py +126 -0
- cwms_python-0.5.0/cwms/locations/physical_locations.py +181 -0
- cwms_python-0.5.0/cwms/outlets/outlets.py +195 -0
- cwms_python-0.5.0/cwms/outlets/virtual_outlets.py +164 -0
- cwms_python-0.5.0/cwms/projects/project_lock_rights.py +151 -0
- cwms_python-0.5.0/cwms/projects/project_locks.py +239 -0
- cwms_python-0.5.0/cwms/projects/projects.py +309 -0
- cwms_python-0.5.0/cwms/ratings/ratings.py +378 -0
- cwms_python-0.5.0/cwms/ratings/ratings_spec.py +154 -0
- cwms_python-0.5.0/cwms/ratings/ratings_template.py +148 -0
- cwms_python-0.5.0/cwms/standard_text/standard_text.py +201 -0
- cwms_python-0.5.0/cwms/timeseries/timerseries_identifier.py +135 -0
- cwms_python-0.5.0/cwms/timeseries/timeseries.py +397 -0
- {cwms_python-0.3.0 → cwms_python-0.5.0}/cwms/timeseries/timeseries_bin.py +1 -8
- {cwms_python-0.3.0 → cwms_python-0.5.0}/cwms/timeseries/timeseries_txt.py +1 -168
- {cwms_python-0.3.0 → cwms_python-0.5.0}/pyproject.toml +1 -1
- cwms_python-0.3.0/cwms/__init__.py +0 -13
- cwms_python-0.3.0/cwms/_constants.py +0 -33
- cwms_python-0.3.0/cwms/core.py +0 -26
- cwms_python-0.3.0/cwms/exceptions.py +0 -131
- cwms_python-0.3.0/cwms/forecast/forecast_instance.py +0 -260
- cwms_python-0.3.0/cwms/forecast/forecast_spec.py +0 -227
- cwms_python-0.3.0/cwms/levels/location_levels.py +0 -484
- cwms_python-0.3.0/cwms/locations/physical_locations.py +0 -47
- cwms_python-0.3.0/cwms/timeseries/timeseries.py +0 -208
- cwms_python-0.3.0/cwms/types.py +0 -67
- cwms_python-0.3.0/cwms/utils.py +0 -85
- {cwms_python-0.3.0 → cwms_python-0.5.0}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cwms-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Corps water managerment systems (CWMS) REST API for Data Retrieval of USACE water data
|
|
5
5
|
License: LICENSE
|
|
6
6
|
Keywords: USACE,water data
|
|
@@ -49,7 +49,7 @@ from datetime import datetime, timedelta
|
|
|
49
49
|
|
|
50
50
|
end = datetime.now()
|
|
51
51
|
begin = end - timedelta(days = 10)
|
|
52
|
-
data = cwms.get_timeseries(
|
|
52
|
+
data = cwms.get_timeseries(ts_id='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
|
|
53
53
|
|
|
54
54
|
#a cwms data object will be provided this object containes both the JSON as well
|
|
55
55
|
#as the values converted into a dataframe
|
|
@@ -28,7 +28,7 @@ from datetime import datetime, timedelta
|
|
|
28
28
|
|
|
29
29
|
end = datetime.now()
|
|
30
30
|
begin = end - timedelta(days = 10)
|
|
31
|
-
data = cwms.get_timeseries(
|
|
31
|
+
data = cwms.get_timeseries(ts_id='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
|
|
32
32
|
|
|
33
33
|
#a cwms data object will be provided this object containes both the JSON as well
|
|
34
34
|
#as the values converted into a dataframe
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from cwms.api import *
|
|
4
|
+
from cwms.catalog.catalog import *
|
|
5
|
+
from cwms.forecast.forecast_instance import *
|
|
6
|
+
from cwms.forecast.forecast_spec import *
|
|
7
|
+
from cwms.levels.location_levels import *
|
|
8
|
+
from cwms.levels.specified_levels import *
|
|
9
|
+
from cwms.locations.physical_locations import *
|
|
10
|
+
from cwms.outlets.outlets import *
|
|
11
|
+
from cwms.outlets.virtual_outlets import *
|
|
12
|
+
from cwms.projects.project_lock_rights import *
|
|
13
|
+
from cwms.projects.project_locks import *
|
|
14
|
+
from cwms.projects.projects import *
|
|
15
|
+
from cwms.ratings.ratings import *
|
|
16
|
+
from cwms.ratings.ratings_spec import *
|
|
17
|
+
from cwms.ratings.ratings_template import *
|
|
18
|
+
from cwms.standard_text.standard_text import *
|
|
19
|
+
from cwms.timeseries.timerseries_identifier import *
|
|
20
|
+
from cwms.timeseries.timeseries import *
|
|
21
|
+
from cwms.timeseries.timeseries_bin import *
|
|
22
|
+
from cwms.timeseries.timeseries_txt import *
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
__version__ = version("cwms-python")
|
|
26
|
+
except PackageNotFoundError:
|
|
27
|
+
__version__ = "version-unknown"
|
|
@@ -29,20 +29,22 @@ the error.
|
|
|
29
29
|
import json
|
|
30
30
|
import logging
|
|
31
31
|
from json import JSONDecodeError
|
|
32
|
-
from typing import Optional, cast
|
|
32
|
+
from typing import Any, Optional, cast
|
|
33
33
|
|
|
34
|
-
from requests import Response
|
|
34
|
+
from requests import Response, adapters
|
|
35
35
|
from requests_toolbelt import sessions # type: ignore
|
|
36
36
|
from requests_toolbelt.sessions import BaseUrlSession # type: ignore
|
|
37
37
|
|
|
38
|
-
from cwms.
|
|
38
|
+
from cwms.cwms_types import JSON, RequestParams
|
|
39
39
|
|
|
40
40
|
# Specify the default API root URL and version.
|
|
41
41
|
API_ROOT = "https://cwms-data.usace.army.mil/cwms-data/"
|
|
42
42
|
API_VERSION = 2
|
|
43
43
|
|
|
44
|
-
# Initialize a non-authenticated session with the default root URL.
|
|
44
|
+
# Initialize a non-authenticated session with the default root URL and set default pool connections.
|
|
45
45
|
SESSION = sessions.BaseUrlSession(base_url=API_ROOT)
|
|
46
|
+
adapter = adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100)
|
|
47
|
+
SESSION.mount("https://", adapter)
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
class InvalidVersion(Exception):
|
|
@@ -91,7 +93,10 @@ class ApiError(Exception):
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def init_session(
|
|
94
|
-
*,
|
|
96
|
+
*,
|
|
97
|
+
api_root: Optional[str] = None,
|
|
98
|
+
api_key: Optional[str] = None,
|
|
99
|
+
pool_connections: int = 100,
|
|
95
100
|
) -> BaseUrlSession:
|
|
96
101
|
"""Specify a root URL and authentication key for the CWMS Data API.
|
|
97
102
|
|
|
@@ -112,7 +117,10 @@ def init_session(
|
|
|
112
117
|
if api_root:
|
|
113
118
|
logging.debug(f"Initializing root URL: api_root={api_root}")
|
|
114
119
|
SESSION = sessions.BaseUrlSession(base_url=api_root)
|
|
115
|
-
|
|
120
|
+
adapter = adapters.HTTPAdapter(
|
|
121
|
+
pool_connections=pool_connections, pool_maxsize=pool_connections
|
|
122
|
+
)
|
|
123
|
+
SESSION.mount("https://", adapter)
|
|
116
124
|
if api_key:
|
|
117
125
|
logging.debug(f"Setting authorization key: api_key={api_key}")
|
|
118
126
|
SESSION.headers.update({"Authorization": api_key})
|
|
@@ -150,12 +158,51 @@ def api_version_text(api_version: int) -> str:
|
|
|
150
158
|
version = "application/json"
|
|
151
159
|
elif api_version == 2:
|
|
152
160
|
version = "application/json;version=2"
|
|
161
|
+
elif api_version == 102:
|
|
162
|
+
version = "application/xml;version=2"
|
|
153
163
|
else:
|
|
154
164
|
raise InvalidVersion(f"API version {api_version} is not supported.")
|
|
155
165
|
|
|
156
166
|
return version
|
|
157
167
|
|
|
158
168
|
|
|
169
|
+
def get_xml(
|
|
170
|
+
endpoint: str,
|
|
171
|
+
params: Optional[RequestParams] = None,
|
|
172
|
+
*,
|
|
173
|
+
api_version: int = API_VERSION,
|
|
174
|
+
) -> Any:
|
|
175
|
+
"""Make a GET request to the CWMS Data API.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
endpoint: The CDA endpoint for the record(s).
|
|
179
|
+
params (optional): Query parameters for the request.
|
|
180
|
+
|
|
181
|
+
Keyword Args:
|
|
182
|
+
api_version (optional): The CDA version to use for the request. If not specified,
|
|
183
|
+
the default API_VERSION will be used.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The deserialized JSON response data.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ApiError: If an error response is return by the API.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
headers = {"Accept": api_version_text(api_version)}
|
|
193
|
+
response = SESSION.get(endpoint, params=params, headers=headers)
|
|
194
|
+
|
|
195
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
196
|
+
logging.error(f"CDA Error: response={response}")
|
|
197
|
+
raise ApiError(response)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
return response.content.decode("utf-8")
|
|
201
|
+
except JSONDecodeError as error:
|
|
202
|
+
logging.error(f"Error decoding CDA response as xml: {error}")
|
|
203
|
+
return {}
|
|
204
|
+
|
|
205
|
+
|
|
159
206
|
def get(
|
|
160
207
|
endpoint: str,
|
|
161
208
|
params: Optional[RequestParams] = None,
|
|
@@ -181,21 +228,60 @@ def get(
|
|
|
181
228
|
|
|
182
229
|
headers = {"Accept": api_version_text(api_version)}
|
|
183
230
|
response = SESSION.get(endpoint, params=params, headers=headers)
|
|
184
|
-
|
|
185
|
-
if response.status_code != 200:
|
|
231
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
186
232
|
logging.error(f"CDA Error: response={response}")
|
|
187
233
|
raise ApiError(response)
|
|
188
234
|
|
|
189
235
|
try:
|
|
190
236
|
return cast(JSON, response.json())
|
|
191
237
|
except JSONDecodeError as error:
|
|
192
|
-
logging.error(f"Error decoding CDA response: {error}")
|
|
238
|
+
logging.error(f"Error decoding CDA response as json: {error}")
|
|
193
239
|
return {}
|
|
194
240
|
|
|
195
241
|
|
|
242
|
+
def get_with_paging(
|
|
243
|
+
selector: str,
|
|
244
|
+
endpoint: str,
|
|
245
|
+
params: RequestParams,
|
|
246
|
+
*,
|
|
247
|
+
api_version: int = API_VERSION,
|
|
248
|
+
) -> JSON:
|
|
249
|
+
"""Make a GET request to the CWMS Data API with paging.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
endpoint: The CDA endpoint for the record(s).
|
|
253
|
+
selector: The json key that will be merged though each page call
|
|
254
|
+
params (optional): Query parameters for the request.
|
|
255
|
+
|
|
256
|
+
Keyword Args:
|
|
257
|
+
api_version (optional): The CDA version to use for the request. If not specified,
|
|
258
|
+
the default API_VERSION will be used.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The deserialized JSON response data.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ApiError: If an error response is return by the API.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
first_pass = True
|
|
268
|
+
while (params["page"] is not None) or first_pass:
|
|
269
|
+
temp = get(endpoint, params, api_version=api_version)
|
|
270
|
+
if first_pass:
|
|
271
|
+
response = temp
|
|
272
|
+
else:
|
|
273
|
+
response[selector] = response[selector] + temp[selector]
|
|
274
|
+
if "next-page" in temp.keys():
|
|
275
|
+
params["page"] = temp["next-page"]
|
|
276
|
+
else:
|
|
277
|
+
params["page"] = None
|
|
278
|
+
first_pass = False
|
|
279
|
+
return response
|
|
280
|
+
|
|
281
|
+
|
|
196
282
|
def post(
|
|
197
283
|
endpoint: str,
|
|
198
|
-
data:
|
|
284
|
+
data: Any,
|
|
199
285
|
params: Optional[RequestParams] = None,
|
|
200
286
|
*,
|
|
201
287
|
api_version: int = API_VERSION,
|
|
@@ -221,18 +307,19 @@ def post(
|
|
|
221
307
|
# post requires different headers than get for
|
|
222
308
|
headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)}
|
|
223
309
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
310
|
+
if isinstance(data, dict):
|
|
311
|
+
data = json.dumps(data)
|
|
227
312
|
|
|
228
|
-
|
|
313
|
+
response = SESSION.post(endpoint, params=params, headers=headers, data=data)
|
|
314
|
+
|
|
315
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
229
316
|
logging.error(f"CDA Error: response={response}")
|
|
230
317
|
raise ApiError(response)
|
|
231
318
|
|
|
232
319
|
|
|
233
320
|
def patch(
|
|
234
321
|
endpoint: str,
|
|
235
|
-
data:
|
|
322
|
+
data: Optional[Any] = None,
|
|
236
323
|
params: Optional[RequestParams] = None,
|
|
237
324
|
*,
|
|
238
325
|
api_version: int = API_VERSION,
|
|
@@ -256,12 +343,14 @@ def patch(
|
|
|
256
343
|
"""
|
|
257
344
|
|
|
258
345
|
headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)}
|
|
346
|
+
if data is None:
|
|
347
|
+
response = SESSION.patch(endpoint, params=params, headers=headers)
|
|
348
|
+
else:
|
|
349
|
+
if isinstance(data, dict):
|
|
350
|
+
data = json.dumps(data)
|
|
351
|
+
response = SESSION.patch(endpoint, params=params, headers=headers, data=data)
|
|
259
352
|
|
|
260
|
-
response
|
|
261
|
-
endpoint, params=params, headers=headers, data=json.dumps(data)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
if response.status_code != 200:
|
|
353
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
265
354
|
logging.error(f"CDA Error: response={response}")
|
|
266
355
|
raise ApiError(response)
|
|
267
356
|
|
|
@@ -289,6 +378,6 @@ def delete(
|
|
|
289
378
|
headers = {"Accept": api_version_text(api_version)}
|
|
290
379
|
response = SESSION.delete(endpoint, params=params, headers=headers)
|
|
291
380
|
|
|
292
|
-
if response.status_code
|
|
381
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
293
382
|
logging.error(f"CDA Error: response={response}")
|
|
294
383
|
raise ApiError(response)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import cwms.api as api
|
|
4
|
+
from cwms.cwms_types import Data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_locations_catalog(
|
|
8
|
+
office_id: str,
|
|
9
|
+
page: Optional[str] = None,
|
|
10
|
+
page_size: Optional[int] = 5000,
|
|
11
|
+
unit_system: Optional[str] = None,
|
|
12
|
+
like: Optional[str] = None,
|
|
13
|
+
location_category_like: Optional[str] = None,
|
|
14
|
+
location_group_like: Optional[str] = None,
|
|
15
|
+
bounding_office_like: Optional[str] = None,
|
|
16
|
+
location_kind_like: Optional[str] = None,
|
|
17
|
+
) -> Data:
|
|
18
|
+
"""Retrieves filters for a locations catalog
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
page: string
|
|
23
|
+
The endpoint used to identify where the request is located.
|
|
24
|
+
page_size: integer
|
|
25
|
+
The entries per page returned. The default value is 5000.
|
|
26
|
+
unit_system: string
|
|
27
|
+
The unit system desired in response. Valid values for this
|
|
28
|
+
field are:
|
|
29
|
+
1. SI
|
|
30
|
+
2. EN
|
|
31
|
+
office_id: string
|
|
32
|
+
The owning office of the timeseries group.
|
|
33
|
+
like: string
|
|
34
|
+
The regex for matching against the id
|
|
35
|
+
location_category_like: string
|
|
36
|
+
The regex for matching against the location category id
|
|
37
|
+
location_group_like: string
|
|
38
|
+
The regex for matching against the location group id
|
|
39
|
+
bounding_office_like: string
|
|
40
|
+
The regex for matching against the location bounding office
|
|
41
|
+
location_kind_like: string
|
|
42
|
+
Posix regular expression matching against the location kind. The location-kind is typically unset or one of the following: {"SITE", "EMBANKMENT", "OVERFLOW", "TURBINE", "STREAM", "PROJECT", "STREAMGAGE", "BASIN", "OUTLET", "LOCK", "GATE"}. Multiple kinds can be matched by using Regular Expression OR clauses. For example: "(SITE|STREAM)"
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
cwms data type
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# CHECKS
|
|
50
|
+
if office_id is None:
|
|
51
|
+
raise ValueError("Retrieve locations catalog requires an office")
|
|
52
|
+
|
|
53
|
+
dataset = "LOCATIONS"
|
|
54
|
+
endpoint = f"catalog/{dataset}"
|
|
55
|
+
params = {
|
|
56
|
+
"page": page,
|
|
57
|
+
"page-size": page_size,
|
|
58
|
+
"units": unit_system,
|
|
59
|
+
"office": office_id,
|
|
60
|
+
"like": like,
|
|
61
|
+
"location-category-like": location_category_like,
|
|
62
|
+
"location-group-like": location_group_like,
|
|
63
|
+
"bounding-office-like": bounding_office_like,
|
|
64
|
+
"location-kind-like": location_kind_like,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
response = api.get(endpoint=endpoint, params=params, api_version=2)
|
|
68
|
+
return Data(response, selector="entries")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_timeseries_catalog(
|
|
72
|
+
office_id: str,
|
|
73
|
+
page: Optional[str] = None,
|
|
74
|
+
page_size: Optional[int] = 5000,
|
|
75
|
+
unit_system: Optional[str] = None,
|
|
76
|
+
like: Optional[str] = None,
|
|
77
|
+
timeseries_category_like: Optional[str] = None,
|
|
78
|
+
timeseries_group_like: Optional[str] = "DMZ Include List",
|
|
79
|
+
bounding_office_like: Optional[str] = None,
|
|
80
|
+
) -> Data:
|
|
81
|
+
"""Retrieves filters for the timeseries catalog
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
page: string
|
|
86
|
+
The endpoint used to identify where the request is located.
|
|
87
|
+
page_size: integer
|
|
88
|
+
The entries per page returned. The default value is 500.
|
|
89
|
+
unit_system: string
|
|
90
|
+
The unit system desired in response. Valid values for this
|
|
91
|
+
field are:
|
|
92
|
+
1. SI
|
|
93
|
+
2. EN
|
|
94
|
+
office_id: string
|
|
95
|
+
The owning office of the timeseries group.
|
|
96
|
+
like: string
|
|
97
|
+
The regex for matching against the id
|
|
98
|
+
timeseries_category_like: string
|
|
99
|
+
The regex for matching against the category id
|
|
100
|
+
timeseries_group_like: string
|
|
101
|
+
The regex for matching against the timeseries group id. This will default to pull only public datasets
|
|
102
|
+
bounding_office_like: string
|
|
103
|
+
The regex for matching against the location bounding office
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
cwms data type
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# CHECKS
|
|
111
|
+
if office_id is None:
|
|
112
|
+
raise ValueError("Retrieve timeseries catalog requires an office")
|
|
113
|
+
|
|
114
|
+
dataset = "TIMESERIES"
|
|
115
|
+
endpoint = f"catalog/{dataset}"
|
|
116
|
+
params = {
|
|
117
|
+
"page": page,
|
|
118
|
+
"page-size": page_size,
|
|
119
|
+
"unit-system": unit_system,
|
|
120
|
+
"office": office_id,
|
|
121
|
+
"like": like,
|
|
122
|
+
"timeseries-category-like": timeseries_category_like,
|
|
123
|
+
"timeseries-group-like": timeseries_group_like,
|
|
124
|
+
"bounding-office-like": bounding_office_like,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
response = api.get(endpoint=endpoint, params=params, api_version=2)
|
|
128
|
+
return Data(response, selector="entries")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from pandas import DataFrame, Index, json_normalize, to_datetime
|
|
6
|
+
|
|
7
|
+
# Describes generic JSON serializable data.
|
|
8
|
+
JSON = dict[str, Any]
|
|
9
|
+
|
|
10
|
+
# Describes request parameters.
|
|
11
|
+
RequestParams = dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeleteMethod(Enum):
|
|
15
|
+
DELETE_ALL = auto()
|
|
16
|
+
DELETE_KEY = auto()
|
|
17
|
+
DELETE_DATA = auto()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RatingMethod(Enum):
|
|
21
|
+
EAGER = auto()
|
|
22
|
+
LAZY = auto()
|
|
23
|
+
REFERENCE = auto()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Data:
|
|
27
|
+
"""Wrapper for CWMS API data."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, json: JSON, *, selector: Optional[str] = None):
|
|
30
|
+
"""Wrap CWMS API Data.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data:
|
|
34
|
+
selector:
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
self.json = json
|
|
38
|
+
self.selector = selector
|
|
39
|
+
|
|
40
|
+
self._df: Optional[DataFrame] = None
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def to_df(json: JSON, selector: Optional[str]) -> DataFrame:
|
|
44
|
+
"""Create a data frame from JSON data.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
json: JSON data returned in the API response.
|
|
48
|
+
selector: Dot separated string of keys used to extract data for data frame.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A data frame containing the data located
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def get_df_data(data: JSON, selector: str) -> JSON:
|
|
55
|
+
# get the data that will be stored in the dataframe using the selectors
|
|
56
|
+
df_data = data
|
|
57
|
+
for key in selector.split("."):
|
|
58
|
+
if key in df_data.keys():
|
|
59
|
+
df_data = df_data[key]
|
|
60
|
+
return df_data
|
|
61
|
+
|
|
62
|
+
def rating_type(data: JSON) -> DataFrame:
|
|
63
|
+
# grab the correct point values for a rating table
|
|
64
|
+
df = DataFrame(data["point"]) if data["point"] else DataFrame()
|
|
65
|
+
return df
|
|
66
|
+
|
|
67
|
+
def timeseries_type(orig_json: JSON, value_json: JSON) -> DataFrame:
|
|
68
|
+
# if timeseries values are present then grab the values and put into
|
|
69
|
+
# dataframe else create empty dataframe
|
|
70
|
+
columns = Index([sub["name"] for sub in orig_json["value-columns"]])
|
|
71
|
+
if value_json:
|
|
72
|
+
df = DataFrame(value_json)
|
|
73
|
+
df.columns = columns
|
|
74
|
+
else:
|
|
75
|
+
df = DataFrame(columns=columns)
|
|
76
|
+
|
|
77
|
+
if "date-time" in df.columns:
|
|
78
|
+
df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
|
|
79
|
+
return df
|
|
80
|
+
|
|
81
|
+
data = deepcopy(json)
|
|
82
|
+
|
|
83
|
+
if selector:
|
|
84
|
+
df_data = get_df_data(data, selector)
|
|
85
|
+
|
|
86
|
+
# if the dataframe is for a rating table
|
|
87
|
+
if ("rating-points" in selector) and ("point" in df_data.keys()):
|
|
88
|
+
df = rating_type(df_data)
|
|
89
|
+
|
|
90
|
+
elif selector == "values":
|
|
91
|
+
df = timeseries_type(data, df_data)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
df = json_normalize(df_data) if df_data else DataFrame()
|
|
95
|
+
else:
|
|
96
|
+
df = json_normalize(data)
|
|
97
|
+
|
|
98
|
+
return df
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def df(self) -> DataFrame:
|
|
102
|
+
"""Return the data frame."""
|
|
103
|
+
|
|
104
|
+
if not isinstance(self._df, DataFrame):
|
|
105
|
+
self._df = Data.to_df(self.json, self.selector)
|
|
106
|
+
|
|
107
|
+
return self._df
|