cwms-python 0.5.0__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.
Files changed (36) hide show
  1. {cwms_python-0.5.0 → cwms_python-0.6.0}/PKG-INFO +15 -4
  2. {cwms_python-0.5.0 → cwms_python-0.6.0}/README.md +10 -0
  3. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/__init__.py +11 -1
  4. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/api.py +8 -5
  5. cwms_python-0.6.0/cwms/catalog/blobs.py +85 -0
  6. cwms_python-0.6.0/cwms/catalog/clobs.py +158 -0
  7. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/cwms_types.py +2 -1
  8. cwms_python-0.6.0/cwms/datafile_imports/shef_critfile_import.py +130 -0
  9. cwms_python-0.6.0/cwms/locations/gate_changes.py +185 -0
  10. cwms_python-0.6.0/cwms/locations/location_groups.py +166 -0
  11. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/locations/physical_locations.py +0 -8
  12. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/ratings/ratings.py +39 -4
  13. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/ratings/ratings_spec.py +50 -0
  14. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/timeseries/timeseries.py +14 -33
  15. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/timeseries/timeseries_bin.py +0 -12
  16. cwms_python-0.6.0/cwms/timeseries/timeseries_group.py +253 -0
  17. cwms_python-0.6.0/cwms/timeseries/timeseries_profile.py +166 -0
  18. cwms_python-0.6.0/cwms/timeseries/timeseries_profile_instance.py +237 -0
  19. cwms_python-0.6.0/cwms/timeseries/timeseries_profile_parser.py +210 -0
  20. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/timeseries/timeseries_txt.py +0 -14
  21. cwms_python-0.6.0/cwms/turbines/turbines.py +242 -0
  22. {cwms_python-0.5.0 → cwms_python-0.6.0}/pyproject.toml +4 -3
  23. {cwms_python-0.5.0 → cwms_python-0.6.0}/LICENSE +0 -0
  24. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/catalog/catalog.py +0 -0
  25. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/forecast/forecast_instance.py +0 -0
  26. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/forecast/forecast_spec.py +0 -0
  27. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/levels/location_levels.py +0 -0
  28. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/levels/specified_levels.py +0 -0
  29. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/outlets/outlets.py +0 -0
  30. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/outlets/virtual_outlets.py +0 -0
  31. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/projects/project_lock_rights.py +0 -0
  32. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/projects/project_locks.py +0 -0
  33. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/projects/projects.py +0 -0
  34. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/ratings/ratings_template.py +0 -0
  35. {cwms_python-0.5.0 → cwms_python-0.6.0}/cwms/standard_text/standard_text.py +0 -0
  36. /cwms_python-0.5.0/cwms/timeseries/timerseries_identifier.py → /cwms_python-0.6.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
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
- 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,11 +1,16 @@
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 *
7
+ from cwms.datafile_imports.shef_critfile_import import *
5
8
  from cwms.forecast.forecast_instance import *
6
9
  from cwms.forecast.forecast_spec import *
7
10
  from cwms.levels.location_levels import *
8
11
  from cwms.levels.specified_levels import *
12
+ from cwms.locations.gate_changes import *
13
+ from cwms.locations.location_groups import *
9
14
  from cwms.locations.physical_locations import *
10
15
  from cwms.outlets.outlets import *
11
16
  from cwms.outlets.virtual_outlets 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
@@ -191,6 +191,7 @@ def get_xml(
191
191
 
192
192
  headers = {"Accept": api_version_text(api_version)}
193
193
  response = SESSION.get(endpoint, params=params, headers=headers)
194
+ response.close()
194
195
 
195
196
  if response.status_code < 200 or response.status_code >= 300:
196
197
  logging.error(f"CDA Error: response={response}")
@@ -228,6 +229,7 @@ def get(
228
229
 
229
230
  headers = {"Accept": api_version_text(api_version)}
230
231
  response = SESSION.get(endpoint, params=params, headers=headers)
232
+ response.close()
231
233
  if response.status_code < 200 or response.status_code >= 300:
232
234
  logging.error(f"CDA Error: response={response}")
233
235
  raise ApiError(response)
@@ -307,10 +309,11 @@ def post(
307
309
  # post requires different headers than get for
308
310
  headers = {"accept": "*/*", "Content-Type": api_version_text(api_version)}
309
311
 
310
- if isinstance(data, dict):
312
+ if isinstance(data, dict) or isinstance(data, list):
311
313
  data = json.dumps(data)
312
314
 
313
315
  response = SESSION.post(endpoint, params=params, headers=headers, data=data)
316
+ response.close()
314
317
 
315
318
  if response.status_code < 200 or response.status_code >= 300:
316
319
  logging.error(f"CDA Error: response={response}")
@@ -346,10 +349,10 @@ def patch(
346
349
  if data is None:
347
350
  response = SESSION.patch(endpoint, params=params, headers=headers)
348
351
  else:
349
- if isinstance(data, dict):
352
+ if isinstance(data, dict) or isinstance(data, list):
350
353
  data = json.dumps(data)
351
354
  response = SESSION.patch(endpoint, params=params, headers=headers, data=data)
352
-
355
+ response.close()
353
356
  if response.status_code < 200 or response.status_code >= 300:
354
357
  logging.error(f"CDA Error: response={response}")
355
358
  raise ApiError(response)
@@ -377,7 +380,7 @@ def delete(
377
380
 
378
381
  headers = {"Accept": api_version_text(api_version)}
379
382
  response = SESSION.delete(endpoint, params=params, headers=headers)
380
-
383
+ response.close()
381
384
  if response.status_code < 200 or response.status_code >= 300:
382
385
  logging.error(f"CDA Error: response={response}")
383
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]
@@ -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:
@@ -0,0 +1,130 @@
1
+ import re
2
+ from typing import Dict, List
3
+
4
+ import pandas as pd
5
+
6
+ import cwms
7
+
8
+
9
+ def import_critfile_to_ts_group(
10
+ file_path: str,
11
+ office_id: str,
12
+ group_id: str = "SHEF Data Acquisition",
13
+ category_id: str = "Data Acquisition",
14
+ group_office_id: str = "CWMS",
15
+ category_office_id: str = "CWMS",
16
+ replace_assigned_ts: bool = False,
17
+ ) -> None:
18
+ """
19
+ Processes a .crit file and saves the information to the SHEF Data Acquisition time series group.
20
+
21
+ Parameters
22
+ ----------
23
+ file_path : str
24
+ Path to the .crit file.
25
+ office_id : str
26
+ The ID of the office associated with the specified timeseries.
27
+ group_id : str, optional
28
+ The specified group associated with the timeseries data. Defaults to "SHEF Data Acquisition".
29
+ category_id : str, optional
30
+ The category ID that contains the timeseries group. Defaults to "Data Acquisition".
31
+ group_office_id : str, optional
32
+ The specified office group associated with the timeseries data. Defaults to "CWMS".
33
+ replace_assigned_ts : bool, optional
34
+ Specifies whether to unassign all existing time series before assigning new time series specified in the content body. Default is False.
35
+
36
+ Returns
37
+ -------
38
+ None
39
+ """
40
+
41
+ def parse_crit_file(file_path: str) -> List[Dict[str, str]]:
42
+ """
43
+ Parses a .crit file into a dictionary containing timeseries ID and Alias.
44
+
45
+ Parameters
46
+ ----------
47
+ file_path : str
48
+ Path to the .crit file.
49
+
50
+ Returns
51
+ -------
52
+ List[Dict[str, str]]
53
+ A list of dictionaries with "Alias" and "Timeseries ID" as keys.
54
+ """
55
+ parsed_data = []
56
+ with open(file_path, "r") as file:
57
+ for line in file:
58
+ # Ignore comment lines and empty lines
59
+ if line.startswith("#") or not line.strip():
60
+ continue
61
+
62
+ # Extract alias, timeseries ID, and TZ
63
+ match = re.match(r"([^=]+)=([^;]+);(.+)", line.strip())
64
+
65
+ if match:
66
+ alias = match.group(1).strip()
67
+ timeseries_id = match.group(2).strip()
68
+ alias2 = match.group(3).strip()
69
+
70
+ parsed_data.append(
71
+ {
72
+ "Alias": alias + ":" + alias2,
73
+ "Timeseries ID": timeseries_id,
74
+ }
75
+ )
76
+
77
+ return parsed_data
78
+
79
+ def append_df(
80
+ df: pd.DataFrame, office_id: str, ts_id: str, alias: str
81
+ ) -> pd.DataFrame:
82
+ """
83
+ Appends a row to the DataFrame.
84
+
85
+ Parameters
86
+ ----------
87
+ df : pandas.DataFrame
88
+ The DataFrame to append to.
89
+ office_id : str
90
+ The ID of the office associated with the specified timeseries.
91
+ tsId : str
92
+ The timeseries ID from the file.
93
+ alias : str
94
+ The alias from the file.
95
+ Returns
96
+ -------
97
+ pandas.DataFrame
98
+ The updated DataFrame.
99
+ """
100
+ data = {
101
+ "office-id": [office_id],
102
+ "timeseries-id": [ts_id],
103
+ "alias-id": [alias],
104
+ }
105
+ df = pd.concat([df, pd.DataFrame(data)])
106
+ return df
107
+
108
+ # Parse the file and get the parsed data
109
+ parsed_data = parse_crit_file(file_path)
110
+
111
+ df = pd.DataFrame()
112
+ for data in parsed_data:
113
+ # Create DataFrame for the current row
114
+ df = append_df(df, office_id, data["Timeseries ID"], data["Alias"])
115
+
116
+ # Generate JSON dictionary
117
+ json_dict = cwms.timeseries_group_df_to_json(
118
+ data=df,
119
+ group_id=group_id,
120
+ group_office_id=group_office_id,
121
+ category_office_id=category_office_id,
122
+ category_id=category_id,
123
+ )
124
+
125
+ cwms.update_timeseries_groups(
126
+ group_id=group_id,
127
+ office_id=office_id,
128
+ replace_assigned_ts=replace_assigned_ts,
129
+ data=json_dict,
130
+ )