Repository: hyriver/pygeohydro (language: python)

Issue:
New release



Discussion / hints:
# [Codecov](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@d3922ad`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `69.16%`.

[![Impacted file tree graph](https://codecov.io/gh/cheginit/hydrodata/pull/27/graphs/tree.svg?width=650&token=sZtuTMk0Pa&height=150&src=pr)](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master      #27   +/-   ##
=========================================
  Coverage          ?   70.16%           
=========================================
  Files             ?        5           
  Lines             ?      828           
  Branches          ?        0           
=========================================
  Hits              ?      581           
  Misses            ?      247           
  Partials          ?        0
```


| [Impacted Files](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [hydrodata/\_\_init\_\_.py](https://codecov.io/gh/cheginit/hydrodata/pull/27/diff?src=pr&el=tree#diff-aHlkcm9kYXRhL19faW5pdF9fLnB5) | `100% <100%> (ø)` | |
| [hydrodata/utils.py](https://codecov.io/gh/cheginit/hydrodata/pull/27/diff?src=pr&el=tree#diff-aHlkcm9kYXRhL3V0aWxzLnB5) | `56.57% <52.5%> (ø)` | |
| [hydrodata/datasets.py](https://codecov.io/gh/cheginit/hydrodata/pull/27/diff?src=pr&el=tree#diff-aHlkcm9kYXRhL2RhdGFzZXRzLnB5) | `70.11% <70.11%> (ø)` | |
| [hydrodata/hydrodata.py](https://codecov.io/gh/cheginit/hydrodata/pull/27/diff?src=pr&el=tree#diff-aHlkcm9kYXRhL2h5ZHJvZGF0YS5weQ==) | `75% <75%> (ø)` | |
| [hydrodata/plot.py](https://codecov.io/gh/cheginit/hydrodata/pull/27/diff?src=pr&el=tree#diff-aHlkcm9kYXRhL3Bsb3QucHk=) | `84.37% <84.37%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=footer). Last update [d3922ad...46413f7](https://codecov.io/gh/cheginit/hydrodata/pull/27?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Fix patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 54a4727..104540f 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,22 @@
 History
 =======
 
+0.15.1 (2023-0X-XX)
+-------------------
+Contributor: [Fernando Aristizabal](https://github.com/fernando-aristizabal)
+
+From release 0.15 onward, all minor versions of HyRiver packages
+will be pinned. This ensures that previous minor versions of HyRiver
+packages cannot be installed with later minor releases. For example,
+if you have ``py3dep==0.14.x`` installed, you cannot install
+``pydaymet==0.15.x``. This is to ensure that the API is
+consistent across all minor versions.
+
+New Features
+~~~~~~~~~~~~
+- Added new `stnfloodevents` module with `STNFloodEventData` class for
+  retrieving flood event data from the [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/) RESTful Service. This Python API abstracts away RESTful principles and produces analysis ready data in geo-referenced GeoDataFrames, DataFrames, lists, or dictionaries as desired. The core class methods available are `data_dictionary`, `get_all_data`, and `get_filtered_data`. These class methods retrieve the data dictionaries by type, get all the available data by type, and make filtered requests for data by type as well, respectively. The four types of data include "instruments", "peaks", "hwms", and "sites".
+
 0.15.0 (2023-05-07)
 -------------------
 From release 0.15 onward, all minor versions of HyRiver packages
diff --git a/pygeohydro/__init__.py b/pygeohydro/__init__.py
index 2f5476b..20bb1fe 100644
--- a/pygeohydro/__init__.py
+++ b/pygeohydro/__init__.py
@@ -31,6 +31,7 @@
     ssebopeta_bycoords,
     ssebopeta_bygeom,
 )
+from pygeohydro.stnfloodevents import STNFloodEventData
 from pygeohydro.waterdata import SensorThings, WaterQuality
 from pygeohydro.watershed import WBD, huc_wb_full, irrigation_withdrawals
 
@@ -51,6 +52,7 @@
     "huc_wb_full",
     "irrigation_withdrawals",
     "SensorThings",
+    "STNFloodEventData",
     "interactive_map",
     "nlcd_bygeom",
     "nlcd_bycoords",
diff --git a/pygeohydro/stnfloodevents.py b/pygeohydro/stnfloodevents.py
new file mode 100644
index 0000000..4bfcdee
--- /dev/null
+++ b/pygeohydro/stnfloodevents.py
@@ -0,0 +1,536 @@
+"""
+Access USGS Short-Term Network (STN) via Restful API.
+
+References
+----------
+ .. [1] [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/)
+ .. [2] [USGS Flood Event Viewer: Providing Hurricane and Flood Response Data](https://www.usgs.gov/mission-areas/water-resources/science/usgs-flood-event-viewer-providing-hurricane-and-flood)
+ .. [3] [A USGS guide for finding and interpreting high-water marks](https://www.usgs.gov/media/videos/a-usgs-guide-finding-and-interpreting-high-water-marks)
+ .. [4] [High-Water Marks and Flooding ](https://www.usgs.gov/special-topics/water-science-school/science/high-water-marks-and-flooding)
+ .. [5] [Identifying and preserving high-water mark data](https://doi.org/10.3133/tm3A24)
+"""
+
+from __future__ import annotations
+
+from io import StringIO
+from typing import TYPE_CHECKING, Union
+
+import geopandas as gpd
+import pandas as pd
+from numpy import nan
+from pyproj import CRS
+
+import async_retriever as ar
+from pygeohydro.exceptions import InputValueError
+from pygeoogc import ServiceURL
+
+if TYPE_CHECKING:
+    CRSTYPE = Union[int, str, CRS]
+
+__all__ = ["STNFloodEventData"]
+
+
+class STNFloodEventData:
+    """
+    Python client to retrieve data from the STN Flood Event Data RESTFUL Service API.
+
+    Advantages of using this client are:
+        - The user does not need to know the details of RESTFUL in general and of this API specifically.
+        - Parses the data and returns Python objects (e.g., pandas.DataFrame, geopandas.GeoDataFrame) instead of JSON.
+        - Convenience functions are offered for data dictionaries.
+        - Geo-references the data where applicable.
+
+    Attributes
+    ----------
+    service_url : str
+        The service url of the STN Flood Event Data RESTFUL Service API.
+    data_dictionary_url : str
+        The data dictionary url of the STN Flood Event Data RESTFUL Service API.
+    service_crs : CRS, default = "EPSG:4326"
+        The coordinate reference system of the data from the service.
+    instruments_query_params : Set of str
+        The accepted query parameters for the instruments data type.
+    peaks_query_params : Set of str
+        The accepted query parameters for the peaks data type.
+    hwms_query_params : Set of str
+        The accepted query parameters for the hwms data type.
+    sites_query_params : Set of str
+        The accepted query parameters for the sites data type.
+
+    Methods
+    -------
+    data_dictionary
+        Retrieves the data dictionary for a given data type.
+    get_all_data
+        Retrieves all data for a given data type.
+    get_filtered_data
+        Retrieves filtered data for a given data type.
+
+    References
+    ----------
+    .. [1] [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/)
+    .. [2] [USGS Flood Event Viewer: Providing Hurricane and Flood Response Data](https://www.usgs.gov/mission-areas/water-resources/science/usgs-flood-event-viewer-providing-hurricane-and-flood)
+    .. [3] [A USGS guide for finding and interpreting high-water marks](https://www.usgs.gov/media/videos/a-usgs-guide-finding-and-interpreting-high-water-marks)
+    .. [4] [High-Water Marks and Flooding ](https://www.usgs.gov/special-topics/water-science-school/science/high-water-marks-and-flooding)
+    .. [5] [Identifying and preserving high-water mark data](https://doi.org/10.3133/tm3A24)
+
+    Notes
+    -----
+    - Point data from the service is assumed to be in the WGS84 coordinate reference system (EPSG:4326).
+    """
+
+    # Per Athena Clark, Lauren Privette, and Hans Vargas at USGS
+    # this is the CRS used for visualization on STN front-end.
+    service_crs = "EPSG:4326"
+
+    # pygeoogc's RESTfulURLs
+    service_url = ServiceURL().restful.stnflood
+    data_dictionary_url = ServiceURL().restful.stnflood_dd
+
+    # accepted query parameters for instruments data type
+    instruments_query_params = {
+        "Event",
+        "EventType",
+        "EventStatus",
+        "States",
+        "County",
+        "CurrentStatus",
+        "CollectionCondition",
+        "SensorType",
+        "DeploymentType",
+    }
+
+    # accepted query parameters for peaks data type
+    peaks_query_params = {
+        "Event",
+        "EventType",
+        "EventStatus",
+        "States",
+        "County",
+        "StartDate",
+        "EndDate",
+    }
+
+    # accepted query parameters for hwms data type
+    hwms_query_params = {
+        "Event",
+        "EventType",
+        "EventStatus",
+        "States",
+        "County",
+        "StartDate",
+        "EndDate",
+    }
+
+    # accepted query parameters for sites data type
+    sites_query_params = {
+        "Event",
+        "State",
+        "SensorType",
+        "NetworkName",
+        "OPDefined",
+        "HWMOnly",
+        "HWMSurveyed",
+        "SensorOnly",
+        "RDGOnly",
+        "HousingTypeOne",
+        "HousingTypeSeven",
+    }
+
+    @classmethod
+    def _geopandify(
+        cls,
+        input_list: list[dict],
+        crs: CRSTYPE | None = service_crs,
+        x_column: str = "longitude_dd",
+        y_column: str = "latitude_dd",
+    ) -> gpd.GeoDataFrame:
+        """
+        Georeference a list of dictionaries to a GeoDataFrame.
+
+        Parameters
+        ----------
+        input_list : list of dict
+            The list of dictionaries to be converted to a geodataframe.
+        crs : CRSTYPE | None, default =  STNFloodEventData.service_crs
+            Desired the coordinate reference system.
+        x_column : str, default = 'longitude'
+            The column name of the x-coordinate.
+        y_column : str, default = 'latitude'
+            The column name of the y-coordinate.
+
+        Returns
+        -------
+        geopandas.GeoDataFrame
+            The geo-referenced GeoDataFrame.
+        """
+        df = pd.DataFrame(input_list)
+
+        if crs is None:
+            crs = cls.service_crs
+
+        df["geometry"] = gpd.points_from_xy(df[x_column], df[y_column], crs=cls.service_crs)
+
+        return gpd.GeoDataFrame(df, crs=cls.service_crs).to_crs(crs)
+
+    @classmethod
+    def _delist_dict(cls, d):
+        """De-lists all unit length lists in a dictionary."""
+
+        def delist(x):
+            if isinstance(x, list) and len(x) == 1:
+                return x[0]
+            elif isinstance(x, list) and len(x) == 0:
+                return nan
+            return x
+
+        return {k: delist(v) for k, v in d.items()}
+
+    @classmethod
+    def data_dictionary(
+        cls, data_type: str, as_dict: bool = False, async_retriever_kwargs: dict | None = None
+    ) -> pd.DataFrame | dict:
+        """
+        Retrieve data dictionaries from the STN Flood Event Data API.
+
+        Parameters
+        ----------
+        data_type : str
+            Type of the data to retrieve. It can be 'instruments', 'peaks', 'hwms', or 'sites'.
+        as_dict : bool, default = False
+            If True, return the data dictionary as a dictionary. Otherwise, it returns as pd.DataFrame.
+        async_retriever_kwargs : dict | None, default = None
+            Additional keyword arguments to pass to `async_retriever.retrieve_text()`. URL is already set.
+
+        Returns
+        -------
+        pandas.DataFrame | dict
+            The retrieved data dictionary as pd.DataFrame or dict.
+
+        References
+        ----------
+        .. [1] [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/)
+        .. [2] [USGS Flood Event Viewer: Providing Hurricane and Flood Response Data](https://www.usgs.gov/mission-areas/water-resources/science/usgs-flood-event-viewer-providing-hurricane-and-flood)
+        .. [3] [A USGS guide for finding and interpreting high-water marks](https://www.usgs.gov/media/videos/a-usgs-guide-finding-and-interpreting-high-water-marks)
+        .. [4] [High-Water Marks and Flooding ](https://www.usgs.gov/special-topics/water-science-school/science/high-water-marks-and-flooding)
+        .. [5] [Identifying and preserving high-water mark data](https://doi.org/10.3133/tm3A24)
+
+        See Also
+        --------
+        `STNFloodEventData.get_all_data` : Retrieves all data for a given data type.
+        `STNFloodEventData.get_filtered_data` : Retrieves filtered data for a given data type.
+
+        Examples
+        --------
+        >>> from pygeohydro.stnfloodevents import STNFloodEventData
+        >>> data = STNFloodEventData.data_dictionary(data_type="instruments", as_dict=False)
+        >>> data.shape[1]
+        2
+        >>> data.columns
+        Index(['Field', 'Definition'], dtype='object')
+        """
+        dtype_dict = {
+            "instruments": "Instruments.csv",
+            "peaks": "FilteredPeaks.csv",
+            "hwms": "FilteredHWMs.csv",
+            "sites": "sites.csv",
+        }
+
+        try:
+            endpoint = dtype_dict[data_type]
+        except KeyError as ke:
+            raise InputValueError(data_type, list(dtype_dict.keys())) from ke
+
+        if async_retriever_kwargs is None:
+            async_retriever_kwargs = {}
+        else:
+            async_retriever_kwargs.pop("url", None)
+
+        # retrieve
+        response = ar.retrieve_text(
+            [f"{cls.data_dictionary_url}{endpoint}"], **async_retriever_kwargs
+        )[0]
+
+        # convert to DataFrame
+        data = pd.read_csv(StringIO(response))
+
+        if "Field" not in data.columns:
+            data.iloc[0] = data.columns.tolist()
+            data.columns = ["Field", "Definition"]
+
+        data["Definition"] = data["Definition"].apply(lambda x: x.replace("\r\n", "  "))
+
+        # concatenate definitions corresponding to NaN fields until a non-NaN field is encountered
+        data_dict = {"Field": [], "Definition": []}
+
+        for _, row in data.iterrows():
+            if pd.isna(row["Field"]):
+                data_dict["Definition"][-1] += " " + row["Definition"]
+            else:
+                data_dict["Field"].append(row["Field"])
+                data_dict["Definition"].append(row["Definition"])
+
+        if as_dict:
+            return data_dict
+        return pd.DataFrame(data_dict)
+
+    @classmethod
+    def get_all_data(
+        cls,
+        data_type: str,
+        as_list: bool | None = False,
+        crs: str | None = service_crs,
+        async_retriever_kwargs: dict | None = None,
+    ) -> gpd.GeoDataFrame | pd.DataFrame | list[dict]:
+        """
+        Retrieve all data from the STN Flood Event Data API for instruments, peaks, hwms, and sites.
+
+        Parameters
+        ----------
+        data_type : str
+            The data source from STN Flood Event Data API. It can be 'instruments', 'peaks', 'hwms', or 'sites'.
+        as_list : bool | None, default = False
+            If True, return the data as a list.
+        crs : str | None, default =  STNFloodEventData.service_crs
+            Desired Coordinate reference system (CRS) of output. Only used for GeoDataFrames with hwms and sites data types.
+        async_retriever_kwargs : dict | None, default = None
+            Additional keyword arguments to pass to `async_retriever.retrieve_json()`. URL is already set.
+
+        Returns
+        -------
+        geopandas.GeoDataFrame | pandas.DataFrame | list of dict
+            The retrieved data as a GeoDataFrame, DataFrame, or a list of dictionaries.
+
+        Raises
+        ------
+        InputValueError
+            If the input data_type is not one of 'instruments', 'peaks', 'hwms', or 'sites'.
+
+        References
+        ----------
+        .. [1] [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/)
+        .. [2] [All Sensors API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/Sensor/AllSensors)
+        .. [3] [All Peak Summary API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/PeakSummary/AllPeakSummaries)
+        .. [4] [All HWM API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/HWM/AllHWMs)
+        .. [5] [All Sites API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/Site/AllSites)
+        .. [6] [USGS Flood Event Viewer: Providing Hurricane and Flood Response Data](https://www.usgs.gov/mission-areas/water-resources/science/usgs-flood-event-viewer-providing-hurricane-and-flood)
+        .. [7] [A USGS guide for finding and interpreting high-water marks](https://www.usgs.gov/media/videos/a-usgs-guide-finding-and-interpreting-high-water-marks)
+        .. [8] [High-Water Marks and Flooding ](https://www.usgs.gov/special-topics/water-science-school/science/high-water-marks-and-flooding)
+        .. [9] [Identifying and preserving high-water mark data](https://doi.org/10.3133/tm3A24)
+
+        See Also
+        --------
+        `STNFloodEventData.get_filtered_data` : Retrieves filtered data for a given data type.
+        `STNFloodEventData.get_data_dictionary` : Retrieves the data dictionary for a given data type.
+
+        Notes
+        -----
+        - Notice schema differences between the data dictionaries, filtered data queries, and all data queries. This is a known issue and is being addressed by USGS.
+
+        Examples
+        --------
+        >>> from pygeohydro.stnfloodevents import STNFloodEventData
+        >>> data = STNFloodEventData.get_all_data(data_type="instruments")
+        >>> data.shape[1]
+        18
+        >>> data.columns
+        Index(['instrument_id', 'sensor_type_id', 'deployment_type_id',
+               'location_description', 'serial_number', 'interval', 'site_id',
+               'event_id', 'inst_collection_id', 'housing_type_id', 'sensor_brand_id',
+               'vented', 'instrument_status', 'data_files', 'files', 'last_updated',
+               'last_updated_by', 'housing_serial_number'],
+               dtype='object')
+        """
+        # non-filtered endpoints
+        endpoint_dict = {
+            "instruments": "Instruments.json",
+            "peaks": "PeakSummaries.json",
+            "hwms": "HWMs.json",
+            "sites": "Sites.json",
+        }
+
+        try:
+            endpoint = endpoint_dict[data_type]
+        except KeyError as ke:
+            raise InputValueError(data_type, list(endpoint_dict.keys())) from ke
+
+        if async_retriever_kwargs is None:
+            async_retriever_kwargs = {}
+        else:
+            async_retriever_kwargs.pop("url", None)
+
+        # retrieve data
+        data = ar.retrieve_json([f"{cls.service_url}{endpoint}"], **async_retriever_kwargs)[0]
+
+        # delists all unit length lists in a dictionary
+        data = [cls._delist_dict(d) for d in data]
+
+        # denotes the fields that are considered as x and y coordinates by data type, use None if no coordinates are available
+        x_and_y_columns = {
+            "instruments": None,
+            "peaks": None,
+            "hwms": ("longitude_dd", "latitude_dd"),
+            "sites": ("longitude_dd", "latitude_dd"),
+        }
+
+        # if list is desired
+        if as_list:
+            return data
+
+        # when no x and y columns are present, convert to DataFrame
+        if x_and_y_columns[data_type] is None:
+            return pd.DataFrame(data)
+
+        # when x and y columns are present, convert to GeoDataFrame
+        x_column, y_column = x_and_y_columns[data_type]
+
+        return cls._geopandify(data, crs=crs, x_column=x_column, y_column=y_column)
+
+    @classmethod
+    def get_filtered_data(
+        cls,
+        data_type: str,
+        query_params: dict | None = None,
+        as_list: bool | None = False,
+        crs: str | None = service_crs,
+        async_retriever_kwargs: dict | None = None,
+    ) -> gpd.GeoDataFrame | pd.DataFrame | list[dict]:
+        """
+        Retrieve filtered data from the STN Flood Event Data API for instruments, peaks, hwms, and sites.
+
+        Parameters
+        ----------
+        data_type : str
+            The data source from STN Flood Event Data API. It can be 'instruments', 'peaks', 'hwms', or 'sites'.
+        query_params : dict | None, default = None
+            RESTFUL API query parameters. For accepted values, see the STNFloodEventData class attributes instruments_accepted_params, peaks_accepted_params, hwms_accepted_params, and sites_accepted_params for available values.
+
+            Also, see the API documentation for each data type for more information:
+                - [instruments](https://stn.wim.usgs.gov/STNServices/Documentation/Sensor/FilteredSensors)
+                - [peaks](https://stn.wim.usgs.gov/STNServices/Documentation/PeakSummary/FilteredPeakSummaries)
+                - [hwms](https://stn.wim.usgs.gov/STNServices/Documentation/HWM/FilteredHWMs)
+                - [sites](https://stn.wim.usgs.gov/STNServices/Documentation/Site/FilteredSites)
+        as_list : bool | None, default = False
+            If True, return the data as a list.
+        crs : str | None, default =  STNFloodEventData.service_crs
+            Desired Coordinate reference system (CRS) of output. Only used for GeoDataFrames outputs.
+        async_retriever_kwargs : dict | None, default = None
+            Additional keyword arguments to pass to `async_retriever.retrieve_json()`. URL and request_kwds are already set.
+
+        Returns
+        -------
+        geopandas.GeoDataFrame | pandas.DataFrame | list of dict
+            The retrieved data as a GeoDataFrame, DataFrame, or a list of dictionaries.
+
+        Raises
+        ------
+        InputValueError
+            If the input data_type is not one of 'instruments', 'peaks', 'hwms', or 'sites'.
+        InputValueError
+            If any of the input query_params are not in accepted parameters (See `STNFloodEventData.instruments_accepted_params`, `STNFloodEventData.peaks_accepted_params`, `STNFloodEventData.hwms_accepted_params`, or `STNFloodEventData.sites_accepted_params`).
+
+        References
+        ----------
+        .. [1] [USGS Short-Term Network (STN)](https://stn.wim.usgs.gov/STNWeb/#/)
+        .. [2] [Filtered Sensors API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/Sensor/FilteredSensors)
+        .. [3] [Peak Summary API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/PeakSummary/FilteredPeakSummaries)
+        .. [4] [Filtered HWM API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/HWM/FilteredHWMs)
+        .. [5] [Filtered Sites API Documentation](https://stn.wim.usgs.gov/STNServices/Documentation/Site/FilteredSites)
+        .. [6] [USGS Flood Event Viewer: Providing Hurricane and Flood Response Data](https://www.usgs.gov/mission-areas/water-resources/science/usgs-flood-event-viewer-providing-hurricane-and-flood)
+        .. [7] [A USGS guide for finding and interpreting high-water marks](https://www.usgs.gov/media/videos/a-usgs-guide-finding-and-interpreting-high-water-marks)
+        .. [8] [High-Water Marks and Flooding ](https://www.usgs.gov/special-topics/water-science-school/science/high-water-marks-and-flooding)
+        .. [9] [Identifying and preserving high-water mark data](https://doi.org/10.3133/tm3A24)
+
+        See Also
+        --------
+        `STNFloodEventData.get_all_data` : Retrieves all data for a given data type.
+        `STNFloodEventData.get_data_dictionary` : Retrieves the data dictionary for a given data type.
+
+        Notes
+        -----
+        - Notice schema differences between the data dictionaries, filtered data queries, and all data queries. This is a known issue and is being addressed by USGS.
+
+        Examples
+        --------
+        >>> from pygeohydro.stnfloodevents import STNFloodEventData
+        >>> query_params = {"States": "SC, CA"}
+        >>> data = STNFloodEventData.get_filtered_data(data_type="instruments", query_params=query_params)
+        >>> data.shape[1]
+        34
+        >>> data.columns
+        Index(['sensorType', 'deploymentType', 'eventName', 'collectionCondition',
+            'housingType', 'sensorBrand', 'statusId', 'timeStamp', 'site_no',
+            'latitude', 'longitude', 'siteDescription', 'networkNames', 'stateName',
+            'countyName', 'siteWaterbody', 'siteHDatum', 'sitePriorityName',
+            'siteZone', 'siteHCollectMethod', 'sitePermHousing', 'instrument_id',
+            'sensor_type_id', 'deployment_type_id', 'location_description',
+            'serial_number', 'housing_serial_number', 'interval', 'site_id',
+            'vented', 'instrument_status', 'data_files', 'files', 'geometry'],
+            dtype='object')
+        """
+        # filtered endpoints
+        endpoint_dict = {
+            "instruments": "Instruments/FilteredInstruments.json",
+            "peaks": "PeakSummaries/FilteredPeaks.json",
+            "hwms": "HWMs/FilteredHWMs.json",
+            "sites": "Sites/FilteredSites.json",
+        }
+
+        try:
+            endpoint = endpoint_dict[data_type]
+        except KeyError as ke:
+            raise InputValueError(data_type, list(endpoint_dict.keys())) from ke
+
+        allowed_query_param_dict = {
+            "instruments": cls.instruments_query_params,
+            "peaks": cls.peaks_query_params,
+            "hwms": cls.hwms_query_params,
+            "sites": cls.sites_query_params,
+        }
+
+        allowed_query_params = allowed_query_param_dict[data_type]
+
+        if query_params is None:
+            query_params = {}
+
+        # check if query_params are valid
+        if not set(query_params.keys()).issubset(allowed_query_params):
+            raise InputValueError("query_param", allowed_query_params)
+
+        if async_retriever_kwargs is None:
+            async_retriever_kwargs = {}
+        else:
+            async_retriever_kwargs.pop("url", None)
+            async_retriever_kwargs.pop("request_kwds", None)
+
+        # retrieve data
+        data = ar.retrieve_json(
+            [f"{cls.service_url}{endpoint}"],
+            request_kwds=[{"params": query_params}],
+            **async_retriever_kwargs,
+        )[0]
+
+        # delists all unit length lists in a dictionary
+        data = [cls._delist_dict(d) for d in data]
+
+        # denotes the fields that are considered as x and y coordinates by data type, use None if no coordinates are available
+        x_and_y_columns = {
+            "instruments": ("longitude", "latitude"),
+            "peaks": ("longitude_dd", "latitude_dd"),
+            "hwms": ("longitude", "latitude"),
+            "sites": ("longitude_dd", "latitude_dd"),
+        }
+
+        # return data as a list
+        if as_list:
+            return data
+
+        # when x and y columns are not available, return as a DataFrame
+        # all of the data types can be returned as GeoDataFrames, commenting out unless needed
+        # if x_and_y_columns[data_type] is None:
+        #    return pd.DataFrame(data)
+
+        # when x and y columns are available, return as a GeoDataFrame
+        x_column, y_column = x_and_y_columns[data_type]
+
+        return cls._geopandify(data, crs=crs, x_column=x_column, y_column=y_column)


Test patch:
diff --git a/tests/test_pygeohydro.py b/tests/test_pygeohydro.py
index 490459f..4edde35 100644
--- a/tests/test_pygeohydro.py
+++ b/tests/test_pygeohydro.py
@@ -7,6 +7,8 @@
 import pandas as pd
 import pytest
 import xarray as xr
+from pyproj import CRS
+from pyproj.exceptions import CRSError
 from shapely.geometry import Polygon
 
 import pygeohydro as gh
@@ -324,3 +326,545 @@ def test_show_versions():
     f = io.StringIO()
     gh.show_versions(file=f)
     assert "SYS INFO" in f.getvalue()
+
+
+# write test for STNFloodEvents class
+class TestSTNFloodEventData:
+    stn: gh.STNFloodEventData = gh.STNFloodEventData()
+
+    expected_data_dictionary_schema = ["Field", "Definition"]
+
+    expected_all_data_schemas = {
+        "instruments": [
+            "instrument_id",
+            "sensor_type_id",
+            "deployment_type_id",
+            "location_description",
+            "serial_number",
+            "interval",
+            "site_id",
+            "event_id",
+            "inst_collection_id",
+            "housing_type_id",
+            "sensor_brand_id",
+            "vented",
+            "instrument_status",
+            "data_files",
+            "files",
+            "last_updated",
+            "last_updated_by",
+            "housing_serial_number",
+        ],
+        "peaks": [
+            "peak_summary_id",
+            "member_id",
+            "peak_date",
+            "is_peak_estimated",
+            "is_peak_time_estimated",
+            "peak_stage",
+            "is_peak_stage_estimated",
+            "is_peak_discharge_estimated",
+            "vdatum_id",
+            "time_zone",
+            "calc_notes",
+            "data_file",
+            "hwms",
+            "height_above_gnd",
+            "peak_discharge",
+            "aep",
+            "aep_lowci",
+            "aep_upperci",
+            "aep_range",
+            "is_hag_estimated",
+            "last_updated",
+            "last_updated_by",
+        ],
+        "hwms": [
+            "hwm_id",
+            "waterbody",
+            "site_id",
+            "event_id",
+            "hwm_type_id",
+            "hwm_quality_id",
+            "hwm_locationdescription",
+            "latitude_dd",
+            "longitude_dd",
+            "survey_date",
+            "elev_ft",
+            "vdatum_id",
+            "vcollect_method_id",
+            "bank",
+            "approval_id",
+            "marker_id",
+            "height_above_gnd",
+            "hcollect_method_id",
+            "hwm_notes",
+            "hwm_environment",
+            "flag_date",
+            "hdatum_id",
+            "flag_member_id",
+            "survey_member_id",
+            "hwm_label",
+            "files",
+            "stillwater",
+            "peak_summary_id",
+            "last_updated",
+            "last_updated_by",
+            "uncertainty",
+            "hwm_uncertainty",
+            "geometry",
+        ],
+        "sites": [
+            "site_id",
+            "site_no",
+            "site_name",
+            "site_description",
+            "state",
+            "county",
+            "waterbody",
+            "latitude_dd",
+            "longitude_dd",
+            "hdatum_id",
+            "hcollect_method_id",
+            "member_id",
+            "network_name_site",
+            "network_type_site",
+            "objective_points",
+            "instruments",
+            "files",
+            "site_housing",
+            "hwms",
+            "site_notes",
+            "access_granted",
+            "address",
+            "city",
+            "is_permanent_housing_installed",
+            "safety_notes",
+            "zip",
+            "other_sid",
+            "sensor_not_appropriate",
+            "drainage_area_sqmi",
+            "last_updated",
+            "last_updated_by",
+            "landownercontact_id",
+            "priority_id",
+            "zone",
+            "usgs_sid",
+            "noaa_sid",
+            "geometry",
+        ],
+    }
+
+    expected_filtered_data_schemas = {
+        "instruments": [
+            "sensorType",
+            "deploymentType",
+            "eventName",
+            "collectionCondition",
+            "housingType",
+            "sensorBrand",
+            "statusId",
+            "timeStamp",
+            "site_no",
+            "latitude",
+            "longitude",
+            "siteDescription",
+            "networkNames",
+            "stateName",
+            "countyName",
+            "siteWaterbody",
+            "siteHDatum",
+            "sitePriorityName",
+            "siteZone",
+            "siteHCollectMethod",
+            "sitePermHousing",
+            "instrument_id",
+            "sensor_type_id",
+            "deployment_type_id",
+            "location_description",
+            "serial_number",
+            "interval",
+            "site_id",
+            "vented",
+            "instrument_status",
+            "data_files",
+            "files",
+            "housing_serial_number",
+            "geometry",
+        ],
+        "peaks": [
+            "vdatum",
+            "member_name",
+            "site_id",
+            "site_no",
+            "latitude_dd",
+            "longitude_dd",
+            "description",
+            "networks",
+            "state",
+            "county",
+            "waterbody",
+            "horizontal_datum",
+            "priority",
+            "horizontal_collection_method",
+            "perm_housing_installed",
+            "peak_summary_id",
+            "peak_date",
+            "is_peak_estimated",
+            "is_peak_time_estimated",
+            "peak_stage",
+            "is_peak_stage_estimated",
+            "is_peak_discharge_estimated",
+            "time_zone",
+            "calc_notes",
+            "data_file",
+            "hwms",
+            "peak_discharge",
+            "zone",
+            "height_above_gnd",
+            "is_hag_estimated",
+            "geometry",
+        ],
+        "hwms": [
+            "latitude",
+            "longitude",
+            "eventName",
+            "hwmTypeName",
+            "hwmQualityName",
+            "verticalDatumName",
+            "verticalMethodName",
+            "approvalMember",
+            "markerName",
+            "horizontalMethodName",
+            "horizontalDatumName",
+            "flagMemberName",
+            "surveyMemberName",
+            "site_no",
+            "siteDescription",
+            "sitePriorityName",
+            "networkNames",
+            "stateName",
+            "countyName",
+            "siteZone",
+            "sitePermHousing",
+            "site_latitude",
+            "site_longitude",
+            "hwm_id",
+            "waterbody",
+            "site_id",
+            "event_id",
+            "hwm_type_id",
+            "hwm_quality_id",
+            "hwm_locationdescription",
+            "latitude_dd",
+            "longitude_dd",
+            "survey_date",
+            "elev_ft",
+            "vdatum_id",
+            "vcollect_method_id",
+            "bank",
+            "approval_id",
+            "marker_id",
+            "hcollect_method_id",
+            "hwm_notes",
+            "hwm_environment",
+            "flag_date",
+            "stillwater",
+            "hdatum_id",
+            "flag_member_id",
+            "survey_member_id",
+            "uncertainty",
+            "hwm_label",
+            "files",
+            "height_above_gnd",
+            "hwm_uncertainty",
+            "peak_summary_id",
+            "geometry",
+        ],
+        "sites": [
+            "networkNames",
+            "Events",
+            "site_id",
+            "site_no",
+            "site_name",
+            "site_description",
+            "address",
+            "city",
+            "state",
+            "zip",
+            "other_sid",
+            "county",
+            "waterbody",
+            "latitude_dd",
+            "longitude_dd",
+            "hdatum_id",
+            "zone",
+            "is_permanent_housing_installed",
+            "usgs_sid",
+            "noaa_sid",
+            "hcollect_method_id",
+            "safety_notes",
+            "access_granted",
+            "network_name_site",
+            "network_type_site",
+            "objective_points",
+            "instruments",
+            "files",
+            "site_housing",
+            "hwms",
+            "RecentOP",
+            "priority_id",
+            "member_id",
+            "landownercontact_id",
+            "drainage_area_sqmi",
+            "geometry",
+        ],
+    }
+
+    @pytest.mark.parametrize(
+        "data_type, as_dict, async_retriever_kwargs, expected_shape",
+        [
+            ("instruments", False, None, (26, 2)),
+            ("peaks", False, {"raise_status": True}, (41, 2)),
+            ("hwms", False, None, (51, 2)),
+            ("sites", False, {"disable": True, "expire_after": 2e6}, (16, 2)),
+            ("instruments", True, {}, 26),
+            ("peaks", True, None, 41),
+            ("hwms", True, {"url": "https://www.google.com", "max_workers": 9}, 51),
+            ("sites", True, None, 16),
+        ],
+    )
+    def test_data_dictionary_success(
+        self, data_type, as_dict, async_retriever_kwargs, expected_shape
+    ):
+        """Test the data_dictionary method of the STNFloodEventData class for success cases."""
+        result = self.stn.data_dictionary(
+            data_type, as_dict, async_retriever_kwargs=async_retriever_kwargs
+        )
+
+        if as_dict:
+            assert isinstance(result, dict)
+            assert list(result.keys()) == self.expected_data_dictionary_schema
+
+            for field in self.expected_data_dictionary_schema:
+                assert len(result[field]) == expected_shape
+        else:
+            assert isinstance(result, pd.DataFrame)
+            assert result.shape == expected_shape
+            assert list(result.columns) == self.expected_data_dictionary_schema
+            assert result.dtypes.to_list() == [np.dtype("O"), np.dtype("O")]
+
+    @pytest.mark.parametrize(
+        "data_type, as_dict, async_retriever_kwargs, expected_exception",
+        [
+            ("instrimants", False, None, gh.exceptions.InputValueError),
+            ("peaks", True, {"dummy": "dummy"}, TypeError),
+            ("hwms", False, {"raise_status": True, "anything": 1}, TypeError),
+        ],
+    )
+    def test_data_dictionary_fail(
+        self, data_type, as_dict, async_retriever_kwargs, expected_exception
+    ):
+        """Test the data_dictionary method of the STNFloodEventData class for failure cases."""
+        with pytest.raises(expected_exception):
+            self.stn.data_dictionary(
+                data_type, as_dict, async_retriever_kwargs=async_retriever_kwargs
+            )
+
+    @pytest.mark.parametrize(
+        "data_type, as_list, crs, async_retriever_kwargs, expected_shape",
+        [
+            ("instruments", False, "EPSG:4329", {"raise_status": False}, (4612, 18)),
+            ("peaks", False, None, None, (13159, 22)),
+            ("hwms", False, None, {"url": "https://www.google.com"}, (34694, 33)),
+            (
+                "sites",
+                False,
+                "+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96 +x_0=0 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs",
+                {},
+                (23600, 37),
+            ),
+            ("sites", False, "EPSG:4236", {}, (23600, 37)),
+            (
+                "instruments",
+                True,
+                "EPSG:4326",
+                {"url": "https://www.google.com", "disable": True},
+                4612,
+            ),
+            ("peaks", True, None, {"max_workers": 7, "timeout": 10}, 13159),
+            ("hwms", True, "EPSG:26915", None, 34694),
+            ("sites", True, None, None, 23600),
+        ],
+    )
+    def test_get_all_data_success(
+        self, data_type, as_list, crs, async_retriever_kwargs, expected_shape
+    ):
+        """Test the get_all_data method of the STNFloodEventData class for success cases."""
+        result = self.stn.get_all_data(
+            data_type, as_list=as_list, crs=crs, async_retriever_kwargs=async_retriever_kwargs
+        )
+
+        if as_list:
+            assert isinstance(result, list)
+            assert len(result) >= expected_shape
+            assert isinstance(result[0], dict)
+            assert all(
+                rk in self.expected_all_data_schemas[data_type] for rk in list(result[0].keys())
+            )
+        else:
+            assert isinstance(result, (gpd.GeoDataFrame, pd.DataFrame))
+
+            if isinstance(result, gpd.GeoDataFrame):
+                if crs is None:
+                    crs = self.stn.service_crs
+                assert result.crs == CRS(crs)
+
+            assert result.shape[0] >= expected_shape[0]  # minimum number of rows
+            assert result.shape[1] == expected_shape[1]  # exact number of columns
+
+            assert list(result.columns) == self.expected_all_data_schemas[data_type]
+
+    @pytest.mark.parametrize(
+        "data_type, as_list, crs, async_retriever_kwargs, expected_exception",
+        [
+            ("instruments", False, "EPSG:4329", {"raise_status": False, "anything": 1}, TypeError),
+            ("peekks", False, None, None, gh.exceptions.InputValueError),
+            ("hwms", False, None, {"url": "https://www.google.com", "any": "yes"}, TypeError),
+            ("sites", False, "EBSJ:3829", {}, CRSError),
+        ],
+    )
+    def test_get_all_data_fail(
+        self, data_type, as_list, crs, async_retriever_kwargs, expected_exception
+    ):
+        """Test the get_all_data method of the STNFloodEventData class for failure cases."""
+        with pytest.raises(expected_exception):
+            self.stn.get_all_data(
+                data_type, as_list=as_list, crs=crs, async_retriever_kwargs=async_retriever_kwargs
+            )
+
+    @pytest.mark.parametrize(
+        "data_type, query_params, as_list, crs, async_retriever_kwargs, expected_shape",
+        [
+            (
+                "instruments",
+                {"States": "OR,WA,AK,HI"},
+                False,
+                "EPSG:4329",
+                {"raise_status": False},
+                (1, 32),
+            ),
+            ("peaks", {"States": "CA, FL, SC"}, False, None, None, (885, 31)),
+            (
+                "hwms",
+                {"States": "LA"},
+                False,
+                None,
+                {"url": "https://www.google.com", "request_kwds": {"k": "v"}},
+                (1208, 54),
+            ),
+            ("sites", {"State": "OK, KS, NE, SD, MS, MD, MN, WI"}, False, "EPSG:3829", {}, (1, 36)),
+            (
+                "instruments",
+                {"States": "NE,IL,IA,TX"},
+                True,
+                "EPSG:4326",
+                {"url": "https://www.google.com", "disable": True},
+                143,
+            ),
+            (
+                "peaks",
+                {"States": "NV, AZ, AR, MO, IN"},
+                True,
+                None,
+                {"max_workers": 7, "timeout": 10},
+                205,
+            ),
+            ("hwms", {"States": "KY,WV,NC,GA,TN,PA"}, True, "EPSG:26915", None, 6220),
+            ("sites", {"State": "NY"}, True, None, None, 712),
+            ("instruments", None, True, None, None, 4612),
+        ],
+    )
+    def test_get_filtered_data_success(
+        self, data_type, query_params, as_list, crs, async_retriever_kwargs, expected_shape
+    ):
+        """Test the get_filtered_data method of the STNFloodEventData class for success cases."""
+        result = self.stn.get_filtered_data(
+            data_type,
+            query_params,
+            as_list=as_list,
+            crs=crs,
+            async_retriever_kwargs=async_retriever_kwargs,
+        )
+
+        if as_list:
+            assert isinstance(result, list)
+            assert len(result) >= expected_shape
+            assert isinstance(result[0], dict)
+            assert all(
+                rk in self.expected_filtered_data_schemas[data_type]
+                for rk in list(result[0].keys())
+            )
+        else:
+            assert isinstance(result, gpd.GeoDataFrame)
+            if crs is None:
+                crs = self.stn.service_crs
+            assert result.crs == CRS(crs)
+
+            assert result.shape[0] >= expected_shape[0]
+            assert result.shape[1] == expected_shape[1]
+
+            assert all(
+                rc in self.expected_filtered_data_schemas[data_type] for rc in list(result.columns)
+            )
+
+    @pytest.mark.parametrize(
+        "data_type, query_params, as_list, crs, async_retriever_kwargs, expected_exception",
+        [
+            (
+                "instruments",
+                {"States": "OR,WA,AK,HI"},
+                False,
+                "EPSG:4329",
+                {"raise_status": False, "anything": 1},
+                TypeError,
+            ),
+            (
+                "peaks",
+                {"Storms": "Sandy, Ivan, Harvey"},
+                False,
+                None,
+                None,
+                gh.exceptions.InputValueError,
+            ),
+            (
+                "hwms",
+                {"States": "LA"},
+                False,
+                None,
+                {"url": "https://www.google.com", "any": "yes"},
+                TypeError,
+            ),
+            (
+                "sitessss",
+                {"State": "OK, KS, NE, SD, MS, MD, MN, WI"},
+                False,
+                "EPSG:3829",
+                {},
+                gh.exceptions.InputValueError,
+            ),
+            ("instruments", {}, False, "EPSJ:4326", None, CRSError),
+        ],
+    )
+    def test_get_filtered_data_fail(
+        self, data_type, query_params, as_list, crs, async_retriever_kwargs, expected_exception
+    ):
+        """Test the get_filtered_data method of the STNFloodEventData class for failure cases."""
+        with pytest.raises(expected_exception):
+            self.stn.get_filtered_data(
+                data_type,
+                query_params,
+                as_list=as_list,
+                crs=crs,
+                async_retriever_kwargs=async_retriever_kwargs,
+            )