cwms-python 0.5.0__tar.gz → 0.7.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.5.0 → cwms_python-0.7.0}/PKG-INFO +16 -5
- {cwms_python-0.5.0 → cwms_python-0.7.0}/README.md +10 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/__init__.py +11 -1
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/api.py +64 -48
- cwms_python-0.7.0/cwms/catalog/blobs.py +99 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/catalog/catalog.py +4 -0
- cwms_python-0.7.0/cwms/catalog/clobs.py +158 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/cwms_types.py +2 -1
- cwms_python-0.7.0/cwms/locations/gate_changes.py +185 -0
- cwms_python-0.7.0/cwms/locations/location_groups.py +166 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/locations/physical_locations.py +5 -9
- cwms_python-0.7.0/cwms/measurements/measurements.py +177 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings.py +46 -11
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings_spec.py +53 -3
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries.py +108 -88
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries_bin.py +0 -12
- cwms_python-0.7.0/cwms/timeseries/timeseries_group.py +253 -0
- cwms_python-0.7.0/cwms/timeseries/timeseries_profile.py +166 -0
- cwms_python-0.7.0/cwms/timeseries/timeseries_profile_instance.py +237 -0
- cwms_python-0.7.0/cwms/timeseries/timeseries_profile_parser.py +210 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries_txt.py +0 -14
- cwms_python-0.7.0/cwms/turbines/turbines.py +242 -0
- cwms_python-0.7.0/cwms/utils/__init__.py +0 -0
- cwms_python-0.7.0/cwms/utils/checks.py +10 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/pyproject.toml +7 -4
- {cwms_python-0.5.0 → cwms_python-0.7.0}/LICENSE +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/forecast/forecast_instance.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/forecast/forecast_spec.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/levels/location_levels.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/levels/specified_levels.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/outlets/outlets.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/outlets/virtual_outlets.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/project_lock_rights.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/project_locks.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/projects.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings_template.py +0 -0
- {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/standard_text/standard_text.py +0 -0
- /cwms_python-0.5.0/cwms/timeseries/timerseries_identifier.py → /cwms_python-0.7.0/cwms/timeseries/timeseries_identifier.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: cwms-python
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Corps water
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Corps water management 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
|
|
@@ -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
|
+
|
|
@@ -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]
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
from cwms.api import *
|
|
4
|
+
from cwms.catalog.blobs import *
|
|
4
5
|
from cwms.catalog.catalog import *
|
|
6
|
+
from cwms.catalog.clobs import *
|
|
5
7
|
from cwms.forecast.forecast_instance import *
|
|
6
8
|
from cwms.forecast.forecast_spec import *
|
|
7
9
|
from cwms.levels.location_levels import *
|
|
8
10
|
from cwms.levels.specified_levels import *
|
|
11
|
+
from cwms.locations.gate_changes import *
|
|
12
|
+
from cwms.locations.location_groups import *
|
|
9
13
|
from cwms.locations.physical_locations import *
|
|
14
|
+
from cwms.measurements.measurements import *
|
|
10
15
|
from cwms.outlets.outlets import *
|
|
11
16
|
from cwms.outlets.virtual_outlets import *
|
|
12
17
|
from cwms.projects.project_lock_rights import *
|
|
@@ -16,10 +21,15 @@ from cwms.ratings.ratings import *
|
|
|
16
21
|
from cwms.ratings.ratings_spec import *
|
|
17
22
|
from cwms.ratings.ratings_template import *
|
|
18
23
|
from cwms.standard_text.standard_text import *
|
|
19
|
-
from cwms.timeseries.timerseries_identifier import *
|
|
20
24
|
from cwms.timeseries.timeseries import *
|
|
21
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 *
|
|
22
31
|
from cwms.timeseries.timeseries_txt import *
|
|
32
|
+
from cwms.turbines.turbines import *
|
|
23
33
|
|
|
24
34
|
try:
|
|
25
35
|
__version__ = version("cwms-python")
|
|
@@ -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
|
|
@@ -26,6 +26,7 @@ which includes the response object and provides some hints to the user on how to
|
|
|
26
26
|
the error.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
+
import base64
|
|
29
30
|
import json
|
|
30
31
|
import logging
|
|
31
32
|
from json import JSONDecodeError
|
|
@@ -34,6 +35,7 @@ from typing import Any, Optional, cast
|
|
|
34
35
|
from requests import Response, adapters
|
|
35
36
|
from requests_toolbelt import sessions # type: ignore
|
|
36
37
|
from requests_toolbelt.sessions import BaseUrlSession # type: ignore
|
|
38
|
+
from urllib3.util.retry import Retry
|
|
37
39
|
|
|
38
40
|
from cwms.cwms_types import JSON, RequestParams
|
|
39
41
|
|
|
@@ -42,8 +44,24 @@ API_ROOT = "https://cwms-data.usace.army.mil/cwms-data/"
|
|
|
42
44
|
API_VERSION = 2
|
|
43
45
|
|
|
44
46
|
# Initialize a non-authenticated session with the default root URL and set default pool connections.
|
|
47
|
+
|
|
48
|
+
retry_strategy = Retry(
|
|
49
|
+
total=6,
|
|
50
|
+
backoff_factor=0.5,
|
|
51
|
+
status_forcelist=[
|
|
52
|
+
403,
|
|
53
|
+
429,
|
|
54
|
+
500,
|
|
55
|
+
502,
|
|
56
|
+
503,
|
|
57
|
+
504,
|
|
58
|
+
], # Example: also retry on these HTTP status codes
|
|
59
|
+
allowed_methods=["GET", "PUT", "POST", "PATCH", "DELETE"], # Methods to retry
|
|
60
|
+
)
|
|
45
61
|
SESSION = sessions.BaseUrlSession(base_url=API_ROOT)
|
|
46
|
-
adapter = adapters.HTTPAdapter(
|
|
62
|
+
adapter = adapters.HTTPAdapter(
|
|
63
|
+
pool_connections=100, pool_maxsize=100, max_retries=retry_strategy
|
|
64
|
+
)
|
|
47
65
|
SESSION.mount("https://", adapter)
|
|
48
66
|
|
|
49
67
|
|
|
@@ -118,7 +136,9 @@ def init_session(
|
|
|
118
136
|
logging.debug(f"Initializing root URL: api_root={api_root}")
|
|
119
137
|
SESSION = sessions.BaseUrlSession(base_url=api_root)
|
|
120
138
|
adapter = adapters.HTTPAdapter(
|
|
121
|
-
pool_connections=pool_connections,
|
|
139
|
+
pool_connections=pool_connections,
|
|
140
|
+
pool_maxsize=pool_connections,
|
|
141
|
+
max_retries=retry_strategy,
|
|
122
142
|
)
|
|
123
143
|
SESSION.mount("https://", adapter)
|
|
124
144
|
if api_key:
|
|
@@ -188,19 +208,8 @@ def get_xml(
|
|
|
188
208
|
Raises:
|
|
189
209
|
ApiError: If an error response is return by the API.
|
|
190
210
|
"""
|
|
191
|
-
|
|
192
|
-
|
|
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 {}
|
|
211
|
+
# Wrap the primary get for backwards compatibility
|
|
212
|
+
return get(endpoint=endpoint, params=params, api_version=api_version)
|
|
204
213
|
|
|
205
214
|
|
|
206
215
|
def get(
|
|
@@ -208,7 +217,7 @@ def get(
|
|
|
208
217
|
params: Optional[RequestParams] = None,
|
|
209
218
|
*,
|
|
210
219
|
api_version: int = API_VERSION,
|
|
211
|
-
) ->
|
|
220
|
+
) -> Any:
|
|
212
221
|
"""Make a GET request to the CWMS Data API.
|
|
213
222
|
|
|
214
223
|
Args:
|
|
@@ -227,16 +236,28 @@ def get(
|
|
|
227
236
|
"""
|
|
228
237
|
|
|
229
238
|
headers = {"Accept": api_version_text(api_version)}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
with SESSION.get(endpoint, params=params, headers=headers) as response:
|
|
240
|
+
if not response.ok:
|
|
241
|
+
logging.error(f"CDA Error: response={response}")
|
|
242
|
+
raise ApiError(response)
|
|
243
|
+
try:
|
|
244
|
+
# Avoid case sensitivity issues with the content type header
|
|
245
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
246
|
+
# Most CDA content is JSON
|
|
247
|
+
if "application/json" in content_type or not content_type:
|
|
248
|
+
return cast(JSON, response.json())
|
|
249
|
+
# Use automatic charset detection with .text
|
|
250
|
+
if "text/plain" in content_type or "text/" in content_type:
|
|
251
|
+
return response.text
|
|
252
|
+
if content_type.startswith("image/"):
|
|
253
|
+
return base64.b64encode(response.content).decode("utf-8")
|
|
254
|
+
# Fallback for remaining content types
|
|
255
|
+
return response.content.decode("utf-8")
|
|
256
|
+
except JSONDecodeError as error:
|
|
257
|
+
logging.error(
|
|
258
|
+
f"Error decoding CDA response as JSON: {error} on line {error.lineno}\n\tFalling back to text"
|
|
259
|
+
)
|
|
260
|
+
return response.text
|
|
240
261
|
|
|
241
262
|
|
|
242
263
|
def get_with_paging(
|
|
@@ -245,7 +266,7 @@ def get_with_paging(
|
|
|
245
266
|
params: RequestParams,
|
|
246
267
|
*,
|
|
247
268
|
api_version: int = API_VERSION,
|
|
248
|
-
) ->
|
|
269
|
+
) -> Any:
|
|
249
270
|
"""Make a GET request to the CWMS Data API with paging.
|
|
250
271
|
|
|
251
272
|
Args:
|
|
@@ -307,14 +328,13 @@ def post(
|
|
|
307
328
|
# post requires different headers than get for
|
|
308
329
|
headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)}
|
|
309
330
|
|
|
310
|
-
if isinstance(data, dict):
|
|
331
|
+
if isinstance(data, dict) or isinstance(data, list):
|
|
311
332
|
data = json.dumps(data)
|
|
312
333
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
raise ApiError(response)
|
|
334
|
+
with SESSION.post(endpoint, params=params, headers=headers, data=data) as response:
|
|
335
|
+
if not response.ok:
|
|
336
|
+
logging.error(f"CDA Error: response={response}")
|
|
337
|
+
raise ApiError(response)
|
|
318
338
|
|
|
319
339
|
|
|
320
340
|
def patch(
|
|
@@ -343,16 +363,13 @@ def patch(
|
|
|
343
363
|
"""
|
|
344
364
|
|
|
345
365
|
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)
|
|
352
366
|
|
|
353
|
-
if
|
|
354
|
-
|
|
355
|
-
|
|
367
|
+
if data and isinstance(data, dict) or isinstance(data, list):
|
|
368
|
+
data = json.dumps(data)
|
|
369
|
+
with SESSION.patch(endpoint, params=params, headers=headers, data=data) as response:
|
|
370
|
+
if not response.ok:
|
|
371
|
+
logging.error(f"CDA Error: response={response}")
|
|
372
|
+
raise ApiError(response)
|
|
356
373
|
|
|
357
374
|
|
|
358
375
|
def delete(
|
|
@@ -376,8 +393,7 @@ def delete(
|
|
|
376
393
|
"""
|
|
377
394
|
|
|
378
395
|
headers = {"Accept": api_version_text(api_version)}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
raise ApiError(response)
|
|
396
|
+
with SESSION.delete(endpoint, params=params, headers=headers) as response:
|
|
397
|
+
if not response.ok:
|
|
398
|
+
logging.error(f"CDA Error: response={response}")
|
|
399
|
+
raise ApiError(response)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import cwms.api as api
|
|
5
|
+
from cwms.cwms_types import JSON, Data
|
|
6
|
+
from cwms.utils.checks import is_base64
|
|
7
|
+
|
|
8
|
+
STORE_DICT = """data = {
|
|
9
|
+
"office-id": "SWT",
|
|
10
|
+
"id": "MYFILE_OR_BLOB_ID.TXT",
|
|
11
|
+
"description": "Your description here",
|
|
12
|
+
"media-type-id": "application/octet-stream",
|
|
13
|
+
"value": "STRING of content or BASE64_ENCODED_STRING"
|
|
14
|
+
}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_blob(blob_id: str, office_id: str) -> str:
|
|
19
|
+
"""Get a single BLOB (Binary Large Object).
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
blob_id: string
|
|
24
|
+
Specifies the id of the blob. ALL blob ids are UPPERCASE.
|
|
25
|
+
office_id: string
|
|
26
|
+
Specifies the office of the blob.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
str: the value returned based on the content-type it was stored with as a string
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
endpoint = f"blobs/{blob_id}"
|
|
35
|
+
params = {"office": office_id}
|
|
36
|
+
response = api.get(endpoint, params, api_version=1)
|
|
37
|
+
return str(response)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_blobs(
|
|
41
|
+
office_id: Optional[str] = None,
|
|
42
|
+
page_size: Optional[int] = 100,
|
|
43
|
+
blob_id_like: Optional[str] = None,
|
|
44
|
+
) -> Data:
|
|
45
|
+
"""Get a subset of Blobs
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
office_id: Optional[string]
|
|
50
|
+
Specifies the office of the blob.
|
|
51
|
+
page_sie: Optional[Integer]
|
|
52
|
+
How many entries per page returned. Default 100.
|
|
53
|
+
blob_id_like: Optional[string]
|
|
54
|
+
Posix regular expression matching against the clob id
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
cwms data type. data.json will return the JSON output and data.df will return a dataframe
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
endpoint = "blobs"
|
|
62
|
+
params = {"office": office_id, "page-size": page_size, "like": blob_id_like}
|
|
63
|
+
|
|
64
|
+
response = api.get(endpoint, params, api_version=2)
|
|
65
|
+
return Data(response, selector="blobs")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def store_blobs(data: JSON, fail_if_exists: Optional[bool] = True) -> None:
|
|
69
|
+
f"""Create New Blob
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
**Note**: The "id" field is automatically cast to uppercase.
|
|
74
|
+
|
|
75
|
+
Data: JSON dictionary
|
|
76
|
+
JSON containing information of Blob to be updated.
|
|
77
|
+
|
|
78
|
+
{STORE_DICT}
|
|
79
|
+
fail_if_exists: Boolean
|
|
80
|
+
Create will fail if the provided ID already exists. Default: True
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
None
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Cannot store a Blob without a JSON data dictionary:\n{STORE_DICT}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Encode value if it's not already Base64-encoded
|
|
93
|
+
if "value" in data and not is_base64(data["value"]):
|
|
94
|
+
# Encode to bytes, then Base64, then decode to string for storing
|
|
95
|
+
data["value"] = base64.b64encode(data["value"].encode("utf-8")).decode("utf-8")
|
|
96
|
+
|
|
97
|
+
endpoint = "blobs"
|
|
98
|
+
params = {"fail-if-exists": fail_if_exists}
|
|
99
|
+
return api.post(endpoint, data, params, api_version=1)
|
|
@@ -77,6 +77,7 @@ def get_timeseries_catalog(
|
|
|
77
77
|
timeseries_category_like: Optional[str] = None,
|
|
78
78
|
timeseries_group_like: Optional[str] = "DMZ Include List",
|
|
79
79
|
bounding_office_like: Optional[str] = None,
|
|
80
|
+
include_extents: Optional[bool] = False,
|
|
80
81
|
) -> Data:
|
|
81
82
|
"""Retrieves filters for the timeseries catalog
|
|
82
83
|
|
|
@@ -101,6 +102,8 @@ def get_timeseries_catalog(
|
|
|
101
102
|
The regex for matching against the timeseries group id. This will default to pull only public datasets
|
|
102
103
|
bounding_office_like: string
|
|
103
104
|
The regex for matching against the location bounding office
|
|
105
|
+
include_extents: bool
|
|
106
|
+
Whether to include the time series extents in the catalog
|
|
104
107
|
|
|
105
108
|
Returns
|
|
106
109
|
-------
|
|
@@ -122,6 +125,7 @@ def get_timeseries_catalog(
|
|
|
122
125
|
"timeseries-category-like": timeseries_category_like,
|
|
123
126
|
"timeseries-group-like": timeseries_group_like,
|
|
124
127
|
"bounding-office-like": bounding_office_like,
|
|
128
|
+
"include-extents": include_extents,
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
response = api.get(endpoint=endpoint, params=params, api_version=2)
|
|
@@ -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]
|
|
@@ -62,6 +62,7 @@ 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)
|
|
65
66
|
return df
|
|
66
67
|
|
|
67
68
|
def timeseries_type(orig_json: JSON, value_json: JSON) -> DataFrame:
|