cwms-python 0.4.4__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {cwms_python-0.4.4 → cwms_python-0.5.0}/PKG-INFO +2 -2
  2. {cwms_python-0.4.4 → cwms_python-0.5.0}/README.md +1 -1
  3. cwms_python-0.5.0/cwms/__init__.py +27 -0
  4. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/api.py +53 -6
  5. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/catalog/catalog.py +1 -1
  6. cwms_python-0.4.4/cwms/types.py → cwms_python-0.5.0/cwms/cwms_types.py +31 -11
  7. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/forecast/forecast_instance.py +1 -1
  8. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/forecast/forecast_spec.py +1 -1
  9. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/levels/location_levels.py +2 -2
  10. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/levels/specified_levels.py +1 -1
  11. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/locations/physical_locations.py +29 -5
  12. cwms_python-0.5.0/cwms/outlets/outlets.py +195 -0
  13. cwms_python-0.5.0/cwms/outlets/virtual_outlets.py +164 -0
  14. cwms_python-0.5.0/cwms/projects/project_lock_rights.py +151 -0
  15. cwms_python-0.5.0/cwms/projects/project_locks.py +239 -0
  16. cwms_python-0.5.0/cwms/projects/projects.py +309 -0
  17. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/ratings/ratings.py +1 -1
  18. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/ratings/ratings_spec.py +1 -1
  19. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/ratings/ratings_template.py +1 -1
  20. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/standard_text/standard_text.py +1 -1
  21. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/timeseries/timerseries_identifier.py +1 -1
  22. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/timeseries/timeseries.py +138 -14
  23. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/timeseries/timeseries_bin.py +1 -1
  24. {cwms_python-0.4.4 → cwms_python-0.5.0}/cwms/timeseries/timeseries_txt.py +1 -1
  25. {cwms_python-0.4.4 → cwms_python-0.5.0}/pyproject.toml +1 -1
  26. cwms_python-0.4.4/cwms/__init__.py +0 -22
  27. {cwms_python-0.4.4 → cwms_python-0.5.0}/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cwms-python
3
- Version: 0.4.4
3
+ Version: 0.5.0
4
4
  Summary: Corps water managerment systems (CWMS) REST API for Data Retrieval of USACE water data
5
5
  License: LICENSE
6
6
  Keywords: USACE,water data
@@ -49,7 +49,7 @@ from datetime import datetime, timedelta
49
49
 
50
50
  end = datetime.now()
51
51
  begin = end - timedelta(days = 10)
52
- data = cwms.get_timeseries(ts_id_='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
52
+ data = cwms.get_timeseries(ts_id='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
53
53
 
54
54
  #a cwms data object will be provided this object containes both the JSON as well
55
55
  #as the values converted into a dataframe
@@ -28,7 +28,7 @@ from datetime import datetime, timedelta
28
28
 
29
29
  end = datetime.now()
30
30
  begin = end - timedelta(days = 10)
31
- data = cwms.get_timeseries(ts_id_='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
31
+ data = cwms.get_timeseries(ts_id='Some.Fully.Qualified.Ts.Id',office_id='OFFICE1' , begin = begin, end = end)
32
32
 
33
33
  #a cwms data object will be provided this object containes both the JSON as well
34
34
  #as the values converted into a dataframe
@@ -0,0 +1,27 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from cwms.api import *
4
+ from cwms.catalog.catalog import *
5
+ from cwms.forecast.forecast_instance import *
6
+ from cwms.forecast.forecast_spec import *
7
+ from cwms.levels.location_levels import *
8
+ from cwms.levels.specified_levels import *
9
+ from cwms.locations.physical_locations import *
10
+ from cwms.outlets.outlets import *
11
+ from cwms.outlets.virtual_outlets import *
12
+ from cwms.projects.project_lock_rights import *
13
+ from cwms.projects.project_locks import *
14
+ from cwms.projects.projects import *
15
+ from cwms.ratings.ratings import *
16
+ from cwms.ratings.ratings_spec import *
17
+ from cwms.ratings.ratings_template import *
18
+ from cwms.standard_text.standard_text import *
19
+ from cwms.timeseries.timerseries_identifier import *
20
+ from cwms.timeseries.timeseries import *
21
+ from cwms.timeseries.timeseries_bin import *
22
+ from cwms.timeseries.timeseries_txt import *
23
+
24
+ try:
25
+ __version__ = version("cwms-python")
26
+ except PackageNotFoundError:
27
+ __version__ = "version-unknown"
@@ -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.types import JSON, RequestParams
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
- *, api_root: Optional[str] = None, api_key: Optional[str] = None
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})
@@ -220,7 +228,6 @@ def get(
220
228
 
221
229
  headers = {"Accept": api_version_text(api_version)}
222
230
  response = SESSION.get(endpoint, params=params, headers=headers)
223
-
224
231
  if response.status_code < 200 or response.status_code >= 300:
225
232
  logging.error(f"CDA Error: response={response}")
226
233
  raise ApiError(response)
@@ -232,6 +239,46 @@ def get(
232
239
  return {}
233
240
 
234
241
 
242
+ def get_with_paging(
243
+ selector: str,
244
+ endpoint: str,
245
+ params: RequestParams,
246
+ *,
247
+ api_version: int = API_VERSION,
248
+ ) -> JSON:
249
+ """Make a GET request to the CWMS Data API with paging.
250
+
251
+ Args:
252
+ endpoint: The CDA endpoint for the record(s).
253
+ selector: The json key that will be merged though each page call
254
+ params (optional): Query parameters for the request.
255
+
256
+ Keyword Args:
257
+ api_version (optional): The CDA version to use for the request. If not specified,
258
+ the default API_VERSION will be used.
259
+
260
+ Returns:
261
+ The deserialized JSON response data.
262
+
263
+ Raises:
264
+ ApiError: If an error response is return by the API.
265
+ """
266
+
267
+ first_pass = True
268
+ while (params["page"] is not None) or first_pass:
269
+ temp = get(endpoint, params, api_version=api_version)
270
+ if first_pass:
271
+ response = temp
272
+ else:
273
+ response[selector] = response[selector] + temp[selector]
274
+ if "next-page" in temp.keys():
275
+ params["page"] = temp["next-page"]
276
+ else:
277
+ params["page"] = None
278
+ first_pass = False
279
+ return response
280
+
281
+
235
282
  def post(
236
283
  endpoint: str,
237
284
  data: Any,
@@ -1,7 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
3
  import cwms.api as api
4
- from cwms.types import Data
4
+ from cwms.cwms_types import Data
5
5
 
6
6
 
7
7
  def get_locations_catalog(
@@ -51,27 +51,47 @@ class Data:
51
51
  A data frame containing the data located
52
52
  """
53
53
 
54
- data = deepcopy(json)
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
+ return df
66
+
67
+ def timeseries_type(orig_json: JSON, value_json: JSON) -> DataFrame:
68
+ # if timeseries values are present then grab the values and put into
69
+ # dataframe else create empty dataframe
70
+ columns = Index([sub["name"] for sub in orig_json["value-columns"]])
71
+ if value_json:
72
+ df = DataFrame(value_json)
73
+ df.columns = columns
74
+ else:
75
+ df = DataFrame(columns=columns)
76
+
77
+ if "date-time" in df.columns:
78
+ df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
79
+ return df
80
+
81
+ data = deepcopy(json)
82
+
83
+ if selector:
84
+ df_data = get_df_data(data, selector)
61
85
 
62
86
  # if the dataframe is for a rating table
63
87
  if ("rating-points" in selector) and ("point" in df_data.keys()):
64
- df = DataFrame(df_data["point"])
88
+ df = rating_type(df_data)
65
89
 
66
90
  elif selector == "values":
67
- df = DataFrame(df_data)
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"]])
91
+ df = timeseries_type(data, df_data)
70
92
 
71
- if "date-time" in df.columns:
72
- df["date-time"] = to_datetime(df["date-time"], unit="ms", utc=True)
73
93
  else:
74
- df = json_normalize(df_data)
94
+ df = json_normalize(df_data) if df_data else DataFrame()
75
95
  else:
76
96
  df = json_normalize(data)
77
97
 
@@ -81,7 +101,7 @@ class Data:
81
101
  def df(self) -> DataFrame:
82
102
  """Return the data frame."""
83
103
 
84
- if type(self._df) != DataFrame:
104
+ if not isinstance(self._df, DataFrame):
85
105
  self._df = Data.to_df(self.json, self.selector)
86
106
 
87
107
  return self._df
@@ -6,7 +6,7 @@ from datetime import datetime
6
6
  from typing import Optional
7
7
 
8
8
  import cwms.api as api
9
- from cwms.types import JSON, Data
9
+ from cwms.cwms_types import JSON, Data
10
10
 
11
11
 
12
12
  def get_forecast_instances(
@@ -5,7 +5,7 @@
5
5
  from typing import Optional
6
6
 
7
7
  import cwms.api as api
8
- from cwms.types import JSON, Data, DeleteMethod
8
+ from cwms.cwms_types import JSON, Data, DeleteMethod
9
9
 
10
10
 
11
11
  def get_forecast_specs(
@@ -9,7 +9,7 @@ from typing import Optional
9
9
  import pandas as pd
10
10
 
11
11
  import cwms.api as api
12
- from cwms.types import JSON, Data
12
+ from cwms.cwms_types import JSON, Data
13
13
 
14
14
 
15
15
  def get_location_levels(
@@ -218,4 +218,4 @@ def get_level_as_timeseries(
218
218
  "unit": unit,
219
219
  }
220
220
  response = api.get(endpoint, params)
221
- return Data(response)
221
+ return Data(response, selector="values")
@@ -5,7 +5,7 @@
5
5
  from typing import Optional
6
6
 
7
7
  import cwms.api as api
8
- from cwms.types import JSON, Data
8
+ from cwms.cwms_types import JSON, Data
9
9
 
10
10
 
11
11
  def get_specified_levels(
@@ -4,7 +4,7 @@ import pandas as pd
4
4
  from pandas import DataFrame
5
5
 
6
6
  import cwms.api as api
7
- from cwms.types import JSON, Data
7
+ from cwms.cwms_types import JSON, Data
8
8
 
9
9
 
10
10
  def get_location_group(loc_group_id: str, category_id: str, office_id: str) -> Data:
@@ -46,20 +46,44 @@ def get_location(location_id: str, office_id: str, unit: str = "EN") -> Data:
46
46
 
47
47
  def get_locations(
48
48
  office_id: Optional[str] = None,
49
- loc_ids: Optional[str] = None,
50
- units: Optional[str] = None,
49
+ location_ids: Optional[str] = None,
50
+ units: Optional[str] = "EN",
51
51
  datum: Optional[str] = None,
52
52
  ) -> Data:
53
+ """
54
+ Get location data for a single location
55
+
56
+ Parameters
57
+ ----------
58
+ location_id: str
59
+ Specifies the name(s) of the location(s) whose data is to be included in the response. This parameter is a Posix regular expression matching against the id
60
+ office_id : str
61
+ The ID of the office that the locations belongs to.
62
+ unit: string, optional, default is EN
63
+ The unit or unit system of the response. Defaults to EN. Valid values
64
+ for the unit field are:
65
+ 1. EN. English unit system.
66
+ 2. SI. SI unit system.
67
+ 3. Other.
68
+ Datum: string, optional, default is None
69
+ Specifies the elevation datum of the response. This field affects only vertical datum. Valid values for this field are:
70
+ 1.) NAVD88 The elevation values will in the specified or default units above the NAVD-88 datum.
71
+ 2.) NGVD29 The elevation values will be in the specified or default units above the NGVD-29 datum.
72
+ Returns
73
+ -------
74
+ cwms data type. data.json will return the JSON output and data.df will return a dataframe
75
+
76
+ """
53
77
  endpoint = "locations"
54
78
  params = {
55
79
  "office": office_id,
56
- "names": loc_ids,
80
+ "names": location_ids,
57
81
  "units": units,
58
82
  "datum": datum,
59
83
  }
60
84
 
61
85
  response = api.get(endpoint, params)
62
- return Data(response, selector="locations.locations")
86
+ return Data(response)
63
87
 
64
88
 
65
89
  def ExpandLocations(df: DataFrame) -> DataFrame:
@@ -0,0 +1,195 @@
1
+ # Copyright (c) 2024
2
+ # United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC)
3
+ # All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL.
4
+ # Source may not be released without written approval from HEC
5
+ from typing import Optional
6
+
7
+ import cwms.api as api
8
+ from cwms.cwms_types import JSON, Data, DeleteMethod
9
+
10
+
11
+ def get_outlet(office_id: str, name: str) -> Data:
12
+ """
13
+ Parameters
14
+ ----------
15
+ name : str
16
+ The ID of the outlet.
17
+ office_id : str
18
+ The ID of the office.
19
+
20
+ Returns
21
+ -------
22
+ response : dict
23
+ the JSON response from CWMS Data API.
24
+
25
+ Raises
26
+ ------
27
+ ValueError
28
+ If any of name or office_id is None.
29
+ ClientError
30
+ If a 400 range error code response is returned from the server.
31
+ NoDataFoundError
32
+ If a 404 range error code response is returned from the server.
33
+ ServerError
34
+ If a 500 range error code response is returned from the server.
35
+ """
36
+
37
+ if name is None:
38
+ raise ValueError("Retrieve outlet requires a name")
39
+ if office_id is None:
40
+ raise ValueError("Retrieve outlet requires an office")
41
+
42
+ endpoint = f"projects/outlets/{name}"
43
+ params = {"office": office_id}
44
+ response = api.get(endpoint, params)
45
+ return Data(response)
46
+
47
+
48
+ def get_outlets(office_id: str, project_id: str) -> Data:
49
+ """
50
+ Parameters
51
+ ----------
52
+ project_id : str
53
+ The project ID of the outlets.
54
+ office_id : str
55
+ The ID of the project's office.
56
+
57
+ Returns
58
+ -------
59
+ response : dict
60
+ the JSON response from CWMS Data API.
61
+
62
+ Raises
63
+ ------
64
+ ValueError
65
+ If any of project_id or office_id is None.
66
+ ClientError
67
+ If a 400 range error code response is returned from the server.
68
+ NoDataFoundError
69
+ If a 404 range error code response is returned from the server.
70
+ ServerError
71
+ If a 500 range error code response is returned from the server.
72
+ """
73
+
74
+ if project_id is None:
75
+ raise ValueError("Retrieve outlets requires a project id")
76
+ if office_id is None:
77
+ raise ValueError("Retrieve outlets requires an office")
78
+
79
+ endpoint = "projects/outlets"
80
+ params = {"office": office_id, "project-id": project_id}
81
+ response = api.get(endpoint, params)
82
+ return Data(response)
83
+
84
+
85
+ def delete_outlet(office_id: str, name: str, delete_method: DeleteMethod) -> None:
86
+ """
87
+ Parameters
88
+ ----------
89
+ name : str
90
+ The name of the outlets.
91
+ office_id : str
92
+ The ID of the project's office.
93
+ delete_method: DeleteMethod
94
+ The method to use to delete the outlet.
95
+
96
+ Returns
97
+ -------
98
+ None
99
+
100
+ Raises
101
+ ------
102
+ ValueError
103
+ If any of name, delete_method, or office_id is None.
104
+ ClientError
105
+ If a 400 range error code response is returned from the server.
106
+ NoDataFoundError
107
+ If a 404 range error code response is returned from the server.
108
+ ServerError
109
+ If a 500 range error code response is returned from the server.
110
+ """
111
+
112
+ if name is None:
113
+ raise ValueError("Delete outlet requires an outlet name")
114
+ if office_id is None:
115
+ raise ValueError("Delete outlet requires an office")
116
+ if delete_method is None:
117
+ raise ValueError("Delete outlet requires a delete method")
118
+
119
+ endpoint = f"projects/outlets/{name}"
120
+ params = {"office": office_id, "method": delete_method.name}
121
+ api.delete(endpoint, params)
122
+
123
+
124
+ def rename_outlet(office_id: str, old_name: str, new_name: str) -> None:
125
+ """
126
+ Parameters
127
+ ----------
128
+ old_name : str
129
+ The name of the outlet to rename.
130
+ new_name : str
131
+ The new name of the outlet.
132
+ office_id : str
133
+ The ID of the project's office.
134
+
135
+ Returns
136
+ -------
137
+ None
138
+
139
+ Raises
140
+ ------
141
+ ValueError
142
+ If any of old_outlet_name, new_outlet_name, or office_id is None.
143
+ ClientError
144
+ If a 400 range error code response is returned from the server.
145
+ NoDataFoundError
146
+ If a 404 range error code response is returned from the server.
147
+ ServerError
148
+ If a 500 range error code response is returned from the server.
149
+ """
150
+
151
+ if old_name is None:
152
+ raise ValueError("Rename outlet requires the original outlet name")
153
+ if new_name is None:
154
+ raise ValueError("Rename outlet requires a new outlet name")
155
+ if office_id is None:
156
+ raise ValueError("Rename outlet requires an office")
157
+
158
+ endpoint = f"projects/outlets/{old_name}"
159
+ params = {"office": office_id, "name": new_name}
160
+ api.patch(endpoint=endpoint, params=params)
161
+
162
+
163
+ def store_outlet(data: JSON, fail_if_exists: Optional[bool] = True) -> None:
164
+ """
165
+ Parameters
166
+ ----------
167
+ data : dict
168
+ A dictionary representing the JSON data to be stored.
169
+ If the `data` value is None, a `ValueError` will be raised.
170
+ fail_if_exists : str, optional
171
+ A boolean value indicating whether to fail if the outlet already exists.
172
+ Default is True.
173
+
174
+ Returns
175
+ -------
176
+ None
177
+
178
+ Raises
179
+ ------
180
+ ValueError
181
+ If any of data is None.
182
+ ClientError
183
+ If a 400 range error code response is returned from the server.
184
+ NoDataFoundError
185
+ If a 404 range error code response is returned from the server.
186
+ ServerError
187
+ If a 500 range error code response is returned from the server.
188
+ """
189
+
190
+ if data is None:
191
+ raise ValueError("Cannot store an outlet without a JSON data dictionary")
192
+
193
+ endpoint = "projects/outlets"
194
+ params = {"fail-if-exists": fail_if_exists}
195
+ api.post(endpoint, data, params)