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.
Files changed (38) hide show
  1. {cwms_python-0.5.0 → cwms_python-0.7.0}/PKG-INFO +16 -5
  2. {cwms_python-0.5.0 → cwms_python-0.7.0}/README.md +10 -0
  3. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/__init__.py +11 -1
  4. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/api.py +64 -48
  5. cwms_python-0.7.0/cwms/catalog/blobs.py +99 -0
  6. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/catalog/catalog.py +4 -0
  7. cwms_python-0.7.0/cwms/catalog/clobs.py +158 -0
  8. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/cwms_types.py +2 -1
  9. cwms_python-0.7.0/cwms/locations/gate_changes.py +185 -0
  10. cwms_python-0.7.0/cwms/locations/location_groups.py +166 -0
  11. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/locations/physical_locations.py +5 -9
  12. cwms_python-0.7.0/cwms/measurements/measurements.py +177 -0
  13. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings.py +46 -11
  14. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings_spec.py +53 -3
  15. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries.py +108 -88
  16. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries_bin.py +0 -12
  17. cwms_python-0.7.0/cwms/timeseries/timeseries_group.py +253 -0
  18. cwms_python-0.7.0/cwms/timeseries/timeseries_profile.py +166 -0
  19. cwms_python-0.7.0/cwms/timeseries/timeseries_profile_instance.py +237 -0
  20. cwms_python-0.7.0/cwms/timeseries/timeseries_profile_parser.py +210 -0
  21. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/timeseries/timeseries_txt.py +0 -14
  22. cwms_python-0.7.0/cwms/turbines/turbines.py +242 -0
  23. cwms_python-0.7.0/cwms/utils/__init__.py +0 -0
  24. cwms_python-0.7.0/cwms/utils/checks.py +10 -0
  25. {cwms_python-0.5.0 → cwms_python-0.7.0}/pyproject.toml +7 -4
  26. {cwms_python-0.5.0 → cwms_python-0.7.0}/LICENSE +0 -0
  27. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/forecast/forecast_instance.py +0 -0
  28. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/forecast/forecast_spec.py +0 -0
  29. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/levels/location_levels.py +0 -0
  30. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/levels/specified_levels.py +0 -0
  31. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/outlets/outlets.py +0 -0
  32. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/outlets/virtual_outlets.py +0 -0
  33. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/project_lock_rights.py +0 -0
  34. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/project_locks.py +0 -0
  35. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/projects/projects.py +0 -0
  36. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/ratings/ratings_template.py +0 -0
  37. {cwms_python-0.5.0 → cwms_python-0.7.0}/cwms/standard_text/standard_text.py +0 -0
  38. /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
1
+ Metadata-Version: 2.3
2
2
  Name: cwms-python
3
- Version: 0.5.0
4
- Summary: Corps water managerment systems (CWMS) REST API for Data Retrieval of USACE water data
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
- Requires-Dist: numpy (>=1.26.4,<2.0.0)
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
- """ Session management and REST functions for CWMS Data API.
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(pool_connections=100, pool_maxsize=100)
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, pool_maxsize=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
- 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 {}
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
- ) -> JSON:
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
- response = SESSION.get(endpoint, params=params, headers=headers)
231
- if response.status_code < 200 or response.status_code >= 300:
232
- logging.error(f"CDA Error: response={response}")
233
- raise ApiError(response)
234
-
235
- try:
236
- return cast(JSON, response.json())
237
- except JSONDecodeError as error:
238
- logging.error(f"Error decoding CDA response as json: {error}")
239
- return {}
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
- ) -> JSON:
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
- response = SESSION.post(endpoint, params=params, headers=headers, data=data)
314
-
315
- if response.status_code < 200 or response.status_code >= 300:
316
- logging.error(f"CDA Error: response={response}")
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 response.status_code < 200 or response.status_code >= 300:
354
- logging.error(f"CDA Error: response={response}")
355
- raise ApiError(response)
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
- response = SESSION.delete(endpoint, params=params, headers=headers)
380
-
381
- if response.status_code < 200 or response.status_code >= 300:
382
- logging.error(f"CDA Error: response={response}")
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: