cwms-python 0.4.4__tar.gz → 0.6.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.4.4 → cwms_python-0.6.0}/PKG-INFO +16 -5
- {cwms_python-0.4.4 → cwms_python-0.6.0}/README.md +11 -1
- cwms_python-0.6.0/cwms/__init__.py +37 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/api.py +61 -11
- cwms_python-0.6.0/cwms/catalog/blobs.py +85 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/catalog/catalog.py +1 -1
- cwms_python-0.6.0/cwms/catalog/clobs.py +158 -0
- cwms_python-0.4.4/cwms/types.py → cwms_python-0.6.0/cwms/cwms_types.py +33 -12
- cwms_python-0.6.0/cwms/datafile_imports/shef_critfile_import.py +130 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/forecast/forecast_instance.py +1 -1
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/forecast/forecast_spec.py +1 -1
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/levels/location_levels.py +2 -2
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/levels/specified_levels.py +1 -1
- cwms_python-0.6.0/cwms/locations/gate_changes.py +185 -0
- cwms_python-0.6.0/cwms/locations/location_groups.py +166 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/locations/physical_locations.py +29 -13
- cwms_python-0.6.0/cwms/outlets/outlets.py +195 -0
- cwms_python-0.6.0/cwms/outlets/virtual_outlets.py +164 -0
- cwms_python-0.6.0/cwms/projects/project_lock_rights.py +151 -0
- cwms_python-0.6.0/cwms/projects/project_locks.py +239 -0
- cwms_python-0.6.0/cwms/projects/projects.py +309 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/ratings/ratings.py +40 -5
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/ratings/ratings_spec.py +51 -1
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/ratings/ratings_template.py +1 -1
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/standard_text/standard_text.py +1 -1
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/timeseries/timeseries.py +142 -37
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/timeseries/timeseries_bin.py +1 -13
- cwms_python-0.6.0/cwms/timeseries/timeseries_group.py +253 -0
- cwms_python-0.4.4/cwms/timeseries/timerseries_identifier.py → cwms_python-0.6.0/cwms/timeseries/timeseries_identifier.py +1 -1
- cwms_python-0.6.0/cwms/timeseries/timeseries_profile.py +166 -0
- cwms_python-0.6.0/cwms/timeseries/timeseries_profile_instance.py +237 -0
- cwms_python-0.6.0/cwms/timeseries/timeseries_profile_parser.py +210 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/cwms/timeseries/timeseries_txt.py +1 -15
- cwms_python-0.6.0/cwms/turbines/turbines.py +242 -0
- {cwms_python-0.4.4 → cwms_python-0.6.0}/pyproject.toml +4 -3
- cwms_python-0.4.4/cwms/__init__.py +0 -22
- {cwms_python-0.4.4 → cwms_python-0.6.0}/LICENSE +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: cwms-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Corps water managerment systems (CWMS) REST API for Data Retrieval of USACE water data
|
|
5
5
|
License: LICENSE
|
|
6
|
-
Keywords: USACE,water data
|
|
6
|
+
Keywords: USACE,water data,CWMS
|
|
7
7
|
Author: Eric Novotny
|
|
8
8
|
Author-email: eric.v.novotny@usace.army.mil
|
|
9
9
|
Requires-Python: >=3.9,<4.0
|
|
@@ -13,10 +13,11 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Requires-Dist: pandas (>=2.1.3,<3.0.0)
|
|
18
18
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
19
19
|
Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
|
|
20
|
+
Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-python
|
|
20
21
|
Description-Content-Type: text/markdown
|
|
21
22
|
|
|
22
23
|
# CWMSpy
|
|
@@ -49,7 +50,7 @@ from datetime import datetime, timedelta
|
|
|
49
50
|
|
|
50
51
|
end = datetime.now()
|
|
51
52
|
begin = end - timedelta(days = 10)
|
|
52
|
-
data = cwms.get_timeseries(
|
|
53
|
+
data = cwms.get_timeseries(ts_id='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
|
|
53
54
|
|
|
54
55
|
#a cwms data object will be provided this object containes both the JSON as well
|
|
55
56
|
#as the values converted into a dataframe
|
|
@@ -93,3 +94,13 @@ print(json)
|
|
|
93
94
|
'version-date': None}
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
## TimeSeries Profile API Compatibility Warning
|
|
98
|
+
|
|
99
|
+
Currently, the TimeSeries Profile API may not be fully supported
|
|
100
|
+
until a new version of cwms-data-access is released with the updated
|
|
101
|
+
endpoint implementation.
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Please view the contribution documentation here: [CONTRIBUTING.md]
|
|
106
|
+
|
|
@@ -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
|
|
@@ -71,3 +71,13 @@ print(json)
|
|
|
71
71
|
['2024-04-23T10:00:00', 86.57999999999997, 3]],
|
|
72
72
|
'version-date': None}
|
|
73
73
|
```
|
|
74
|
+
|
|
75
|
+
## TimeSeries Profile API Compatibility Warning
|
|
76
|
+
|
|
77
|
+
Currently, the TimeSeries Profile API may not be fully supported
|
|
78
|
+
until a new version of cwms-data-access is released with the updated
|
|
79
|
+
endpoint implementation.
|
|
80
|
+
|
|
81
|
+
## Contributing
|
|
82
|
+
|
|
83
|
+
Please view the contribution documentation here: [CONTRIBUTING.md]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from cwms.api import *
|
|
4
|
+
from cwms.catalog.blobs import *
|
|
5
|
+
from cwms.catalog.catalog import *
|
|
6
|
+
from cwms.catalog.clobs import *
|
|
7
|
+
from cwms.datafile_imports.shef_critfile_import import *
|
|
8
|
+
from cwms.forecast.forecast_instance import *
|
|
9
|
+
from cwms.forecast.forecast_spec import *
|
|
10
|
+
from cwms.levels.location_levels import *
|
|
11
|
+
from cwms.levels.specified_levels import *
|
|
12
|
+
from cwms.locations.gate_changes import *
|
|
13
|
+
from cwms.locations.location_groups import *
|
|
14
|
+
from cwms.locations.physical_locations import *
|
|
15
|
+
from cwms.outlets.outlets import *
|
|
16
|
+
from cwms.outlets.virtual_outlets import *
|
|
17
|
+
from cwms.projects.project_lock_rights import *
|
|
18
|
+
from cwms.projects.project_locks import *
|
|
19
|
+
from cwms.projects.projects import *
|
|
20
|
+
from cwms.ratings.ratings import *
|
|
21
|
+
from cwms.ratings.ratings_spec import *
|
|
22
|
+
from cwms.ratings.ratings_template import *
|
|
23
|
+
from cwms.standard_text.standard_text import *
|
|
24
|
+
from cwms.timeseries.timeseries import *
|
|
25
|
+
from cwms.timeseries.timeseries_bin import *
|
|
26
|
+
from cwms.timeseries.timeseries_group import *
|
|
27
|
+
from cwms.timeseries.timeseries_identifier import *
|
|
28
|
+
from cwms.timeseries.timeseries_profile import *
|
|
29
|
+
from cwms.timeseries.timeseries_profile_instance import *
|
|
30
|
+
from cwms.timeseries.timeseries_profile_parser import *
|
|
31
|
+
from cwms.timeseries.timeseries_txt import *
|
|
32
|
+
from cwms.turbines.turbines import *
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
__version__ = version("cwms-python")
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
__version__ = "version-unknown"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Session management and REST functions for CWMS Data API.
|
|
2
2
|
|
|
3
3
|
This module provides functions for making REST calls to the CWMS Data API (CDA). These
|
|
4
4
|
functions should be used internally to interact with the API. The user should not have to
|
|
@@ -31,18 +31,20 @@ import logging
|
|
|
31
31
|
from json import JSONDecodeError
|
|
32
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})
|
|
@@ -183,6 +191,7 @@ def get_xml(
|
|
|
183
191
|
|
|
184
192
|
headers = {"Accept": api_version_text(api_version)}
|
|
185
193
|
response = SESSION.get(endpoint, params=params, headers=headers)
|
|
194
|
+
response.close()
|
|
186
195
|
|
|
187
196
|
if response.status_code < 200 or response.status_code >= 300:
|
|
188
197
|
logging.error(f"CDA Error: response={response}")
|
|
@@ -220,7 +229,7 @@ def get(
|
|
|
220
229
|
|
|
221
230
|
headers = {"Accept": api_version_text(api_version)}
|
|
222
231
|
response = SESSION.get(endpoint, params=params, headers=headers)
|
|
223
|
-
|
|
232
|
+
response.close()
|
|
224
233
|
if response.status_code < 200 or response.status_code >= 300:
|
|
225
234
|
logging.error(f"CDA Error: response={response}")
|
|
226
235
|
raise ApiError(response)
|
|
@@ -232,6 +241,46 @@ def get(
|
|
|
232
241
|
return {}
|
|
233
242
|
|
|
234
243
|
|
|
244
|
+
def get_with_paging(
|
|
245
|
+
selector: str,
|
|
246
|
+
endpoint: str,
|
|
247
|
+
params: RequestParams,
|
|
248
|
+
*,
|
|
249
|
+
api_version: int = API_VERSION,
|
|
250
|
+
) -> JSON:
|
|
251
|
+
"""Make a GET request to the CWMS Data API with paging.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
endpoint: The CDA endpoint for the record(s).
|
|
255
|
+
selector: The json key that will be merged though each page call
|
|
256
|
+
params (optional): Query parameters for the request.
|
|
257
|
+
|
|
258
|
+
Keyword Args:
|
|
259
|
+
api_version (optional): The CDA version to use for the request. If not specified,
|
|
260
|
+
the default API_VERSION will be used.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The deserialized JSON response data.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ApiError: If an error response is return by the API.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
first_pass = True
|
|
270
|
+
while (params["page"] is not None) or first_pass:
|
|
271
|
+
temp = get(endpoint, params, api_version=api_version)
|
|
272
|
+
if first_pass:
|
|
273
|
+
response = temp
|
|
274
|
+
else:
|
|
275
|
+
response[selector] = response[selector] + temp[selector]
|
|
276
|
+
if "next-page" in temp.keys():
|
|
277
|
+
params["page"] = temp["next-page"]
|
|
278
|
+
else:
|
|
279
|
+
params["page"] = None
|
|
280
|
+
first_pass = False
|
|
281
|
+
return response
|
|
282
|
+
|
|
283
|
+
|
|
235
284
|
def post(
|
|
236
285
|
endpoint: str,
|
|
237
286
|
data: Any,
|
|
@@ -260,10 +309,11 @@ def post(
|
|
|
260
309
|
# post requires different headers than get for
|
|
261
310
|
headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)}
|
|
262
311
|
|
|
263
|
-
if isinstance(data, dict):
|
|
312
|
+
if isinstance(data, dict) or isinstance(data, list):
|
|
264
313
|
data = json.dumps(data)
|
|
265
314
|
|
|
266
315
|
response = SESSION.post(endpoint, params=params, headers=headers, data=data)
|
|
316
|
+
response.close()
|
|
267
317
|
|
|
268
318
|
if response.status_code < 200 or response.status_code >= 300:
|
|
269
319
|
logging.error(f"CDA Error: response={response}")
|
|
@@ -299,10 +349,10 @@ def patch(
|
|
|
299
349
|
if data is None:
|
|
300
350
|
response = SESSION.patch(endpoint, params=params, headers=headers)
|
|
301
351
|
else:
|
|
302
|
-
if isinstance(data, dict):
|
|
352
|
+
if isinstance(data, dict) or isinstance(data, list):
|
|
303
353
|
data = json.dumps(data)
|
|
304
354
|
response = SESSION.patch(endpoint, params=params, headers=headers, data=data)
|
|
305
|
-
|
|
355
|
+
response.close()
|
|
306
356
|
if response.status_code < 200 or response.status_code >= 300:
|
|
307
357
|
logging.error(f"CDA Error: response={response}")
|
|
308
358
|
raise ApiError(response)
|
|
@@ -330,7 +380,7 @@ def delete(
|
|
|
330
380
|
|
|
331
381
|
headers = {"Accept": api_version_text(api_version)}
|
|
332
382
|
response = SESSION.delete(endpoint, params=params, headers=headers)
|
|
333
|
-
|
|
383
|
+
response.close()
|
|
334
384
|
if response.status_code < 200 or response.status_code >= 300:
|
|
335
385
|
logging.error(f"CDA Error: response={response}")
|
|
336
386
|
raise ApiError(response)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import cwms.api as api
|
|
4
|
+
from cwms.cwms_types import JSON, Data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_blob(blob_id: str, office_id: str) -> Data:
|
|
8
|
+
"""Get a single clob.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
blob_id: string
|
|
13
|
+
Specifies the id of the blob
|
|
14
|
+
office_id: string
|
|
15
|
+
Specifies the office of the blob.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
endpoint = f"blobs/{blob_id}"
|
|
24
|
+
params = {"office": office_id}
|
|
25
|
+
response = api.get(endpoint, params, api_version=1)
|
|
26
|
+
return Data(response)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_blobs(
|
|
30
|
+
office_id: Optional[str] = None,
|
|
31
|
+
page_size: Optional[int] = 100,
|
|
32
|
+
blob_id_like: Optional[str] = None,
|
|
33
|
+
) -> Data:
|
|
34
|
+
"""Get a subset of Blobs
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
office_id: Optional[string]
|
|
39
|
+
Specifies the office of the blob.
|
|
40
|
+
page_sie: Optional[Integer]
|
|
41
|
+
How many entries per page returned. Default 100.
|
|
42
|
+
blob_id_like: Optional[string]
|
|
43
|
+
Posix regular expression matching against the clob id
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
endpoint = "blobs"
|
|
51
|
+
params = {"office": office_id, "page-size": page_size, "like": blob_id_like}
|
|
52
|
+
|
|
53
|
+
response = api.get(endpoint, params, api_version=1)
|
|
54
|
+
return Data(response, selector="blobs")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def store_blobs(data: JSON, fail_if_exists: Optional[bool] = True) -> None:
|
|
58
|
+
"""Create New Blob
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
Data: JSON dictionary
|
|
63
|
+
JSON containing information of Blob to be updated
|
|
64
|
+
{
|
|
65
|
+
"office-id": "string",
|
|
66
|
+
"id": "string",
|
|
67
|
+
"description": "string",
|
|
68
|
+
"media-type-id": "string",
|
|
69
|
+
"value": "string"
|
|
70
|
+
}
|
|
71
|
+
fail_if_exists: Boolean
|
|
72
|
+
Create will fail if provided ID already exists. Default: true
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
None
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if not isinstance(data, dict):
|
|
80
|
+
raise ValueError("Cannot store a Blob without a JSON data dictionary")
|
|
81
|
+
|
|
82
|
+
endpoint = "blobs"
|
|
83
|
+
params = {"fail-if-exists": fail_if_exists}
|
|
84
|
+
|
|
85
|
+
return api.post(endpoint, data, params, api_version=1)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import cwms.api as api
|
|
4
|
+
from cwms.cwms_types import JSON, Data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_clob(clob_id: str, office_id: str, clob_id_query: Optional[str] = None) -> Data:
|
|
8
|
+
"""Get a single clob.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
clob_id: string
|
|
13
|
+
Specifies the id of the clob
|
|
14
|
+
office_id: string
|
|
15
|
+
Specifies the office of the clob.
|
|
16
|
+
clob_id_query: string
|
|
17
|
+
If this query parameter is provided the id path parameter is ignored and the
|
|
18
|
+
value of the query parameter is used. Note: this query parameter is necessary
|
|
19
|
+
for id's that contain '/' or other special characters. Because of abuse even
|
|
20
|
+
properly escaped '/' in url paths are blocked. When using this query parameter
|
|
21
|
+
a valid path parameter must still be provided for the request to be properly
|
|
22
|
+
routed. If your clob id contains '/' you can't specify the clob-id query
|
|
23
|
+
parameter and also specify the id path parameter because firewall and/or server
|
|
24
|
+
rules will deny the request even though you are specifying this override. "ignored"
|
|
25
|
+
is suggested.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
endpoint = f"clobs/{clob_id}"
|
|
34
|
+
params = {
|
|
35
|
+
"office": office_id,
|
|
36
|
+
"clob-id-query": clob_id_query,
|
|
37
|
+
}
|
|
38
|
+
response = api.get(endpoint, params)
|
|
39
|
+
return Data(response)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_clobs(
|
|
43
|
+
office_id: Optional[str] = None,
|
|
44
|
+
page_size: Optional[int] = 100,
|
|
45
|
+
include_values: Optional[bool] = False,
|
|
46
|
+
clob_id_like: Optional[str] = None,
|
|
47
|
+
) -> Data:
|
|
48
|
+
"""Get a subset of Clobs
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
office_id: Optional[string]
|
|
53
|
+
Specifies the office of the clob.
|
|
54
|
+
page_sie: Optional[Integer]
|
|
55
|
+
How many entries per page returned. Default 100.
|
|
56
|
+
include_values: Optional[Boolean]
|
|
57
|
+
Do you want the value associated with this particular clob (default: false)
|
|
58
|
+
clob_id_like: Optional[string]
|
|
59
|
+
Posix regular expression matching against the clob id
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
endpoint = "clobs"
|
|
67
|
+
params = {
|
|
68
|
+
"office": office_id,
|
|
69
|
+
"page-size": page_size,
|
|
70
|
+
"include-values": include_values,
|
|
71
|
+
"like": clob_id_like,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
response = api.get(endpoint, params)
|
|
75
|
+
return Data(response, selector="clobs")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def delete_clob(clob_id: str, office_id: str) -> None:
|
|
79
|
+
"""Deletes requested clob
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
clob_id: string
|
|
84
|
+
Specifies the id of the clob to be deleted
|
|
85
|
+
office_id: string
|
|
86
|
+
Specifies the office of the clob.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
None
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
endpoint = f"clobs/{clob_id}"
|
|
94
|
+
params = {"office": office_id}
|
|
95
|
+
|
|
96
|
+
return api.delete(endpoint, params=params, api_version=1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def update_clob(data: JSON, clob_id: str, ignore_nulls: Optional[bool] = True) -> None:
|
|
100
|
+
"""Updates clob
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
Data: JSON dictionary
|
|
105
|
+
JSON containing information of Clob to be updated
|
|
106
|
+
{
|
|
107
|
+
"office-id": "string",
|
|
108
|
+
"id": "string",
|
|
109
|
+
"description": "string",
|
|
110
|
+
"value": "string"
|
|
111
|
+
}
|
|
112
|
+
clob_id: string
|
|
113
|
+
Specifies the id of the clob to be deleted
|
|
114
|
+
ignore_nulls: Boolean
|
|
115
|
+
If true, null and empty fields in the provided clob will be ignored and the existing value of those fields left in place. Default: true
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
None
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if not isinstance(data, dict):
|
|
123
|
+
raise ValueError("Cannot store a Clob without a JSON data dictionary")
|
|
124
|
+
|
|
125
|
+
endpoint = f"clobs/{clob_id}"
|
|
126
|
+
params = {"ignore-nulls": ignore_nulls}
|
|
127
|
+
|
|
128
|
+
return api.patch(endpoint, data, params, api_version=1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def store_clobs(data: JSON, fail_if_exists: Optional[bool] = True) -> None:
|
|
132
|
+
"""Create New Clob
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
Data: JSON dictionary
|
|
137
|
+
JSON containing information of Clob to be updated
|
|
138
|
+
{
|
|
139
|
+
"office-id": "string",
|
|
140
|
+
"id": "string",
|
|
141
|
+
"description": "string",
|
|
142
|
+
"value": "string"
|
|
143
|
+
}
|
|
144
|
+
fail_if_exists: Boolean
|
|
145
|
+
Create will fail if provided ID already exists. Default: true
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
None
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if not isinstance(data, dict):
|
|
153
|
+
raise ValueError("Cannot store a Clob without a JSON data dictionary")
|
|
154
|
+
|
|
155
|
+
endpoint = "clobs"
|
|
156
|
+
params = {"fail-if-exists": fail_if_exists}
|
|
157
|
+
|
|
158
|
+
return api.post(endpoint, data, params, api_version=1)
|
|
@@ -2,7 +2,7 @@ from copy import deepcopy
|
|
|
2
2
|
from enum import Enum, auto
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
|
-
from pandas import DataFrame, Index, json_normalize, to_datetime
|
|
5
|
+
from pandas import DataFrame, Index, json_normalize, to_datetime, to_numeric
|
|
6
6
|
|
|
7
7
|
# Describes generic JSON serializable data.
|
|
8
8
|
JSON = dict[str, Any]
|
|
@@ -51,27 +51,48 @@ class Data:
|
|
|
51
51
|
A data frame containing the data located
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
|
-
data
|
|
55
|
-
|
|
56
|
-
if selector:
|
|
54
|
+
def get_df_data(data: JSON, selector: str) -> JSON:
|
|
55
|
+
# get the data that will be stored in the dataframe using the selectors
|
|
57
56
|
df_data = data
|
|
58
57
|
for key in selector.split("."):
|
|
59
58
|
if key in df_data.keys():
|
|
60
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
|
+
df = df.apply(to_numeric)
|
|
66
|
+
return df
|
|
67
|
+
|
|
68
|
+
def timeseries_type(orig_json: JSON, value_json: JSON) -> DataFrame:
|
|
69
|
+
# if timeseries values are present then grab the values and put into
|
|
70
|
+
# dataframe else create empty dataframe
|
|
71
|
+
columns = Index([sub["name"] for sub in orig_json["value-columns"]])
|
|
72
|
+
if value_json:
|
|
73
|
+
df = DataFrame(value_json)
|
|
74
|
+
df.columns = columns
|
|
75
|
+
else:
|
|
76
|
+
df = DataFrame(columns=columns)
|
|
77
|
+
|
|
78
|
+
if "date-time" in df.columns:
|
|
79
|
+
df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
|
|
80
|
+
return df
|
|
81
|
+
|
|
82
|
+
data = deepcopy(json)
|
|
83
|
+
|
|
84
|
+
if selector:
|
|
85
|
+
df_data = get_df_data(data, selector)
|
|
61
86
|
|
|
62
87
|
# if the dataframe is for a rating table
|
|
63
88
|
if ("rating-points" in selector) and ("point" in df_data.keys()):
|
|
64
|
-
df =
|
|
89
|
+
df = rating_type(df_data)
|
|
65
90
|
|
|
66
91
|
elif selector == "values":
|
|
67
|
-
df =
|
|
68
|
-
# if timeseries values are present then grab the values and put into dataframe
|
|
69
|
-
df.columns = Index([sub["name"] for sub in data["value-columns"]])
|
|
92
|
+
df = timeseries_type(data, df_data)
|
|
70
93
|
|
|
71
|
-
if "date-time" in df.columns:
|
|
72
|
-
df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
|
|
73
94
|
else:
|
|
74
|
-
df = json_normalize(df_data)
|
|
95
|
+
df = json_normalize(df_data) if df_data else DataFrame()
|
|
75
96
|
else:
|
|
76
97
|
df = json_normalize(data)
|
|
77
98
|
|
|
@@ -81,7 +102,7 @@ class Data:
|
|
|
81
102
|
def df(self) -> DataFrame:
|
|
82
103
|
"""Return the data frame."""
|
|
83
104
|
|
|
84
|
-
if
|
|
105
|
+
if not isinstance(self._df, DataFrame):
|
|
85
106
|
self._df = Data.to_df(self.json, self.selector)
|
|
86
107
|
|
|
87
108
|
return self._df
|