class NID: """Retrieve data from the National Inventory of Dams.""" def __init__(self) -> None: self.session = RetrySession() self.base_url = "https://nid.sec.usace.army.mil/ords" self.headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", "DNT": "1", } def get_xlsx(self) -> io.BytesIO: """Get the excel file that containes the dam data.""" self.headers.update({"Cookie": "ORA_WWV_APP_105=ORA_WWV-QM2rrHvxwzROBqNYVD0WIlg2"}) payload = {"InFileName": "NID2019_U.xlsx"} r = self.session.get( f"{self.base_url}/NID_R.DOWNLOADFILE", payload=payload, headers=self.headers ) return io.BytesIO(r.content) def get_attrs(self, variables: List[str]) -> Dict[str, str]: """Get descriptions of the NID variables.""" self.headers.update({"Cookie": "ORA_WWV_APP_105=ORA_WWV-iaBJjzLW1v3a1s1mXEub0S7R"}) desc: Dict[str, str] = {} for v in variables: payload = {"p": f"105:10:10326760693796::NO::P10_COLUMN_NAME:{v}"} page = self.session.get(f"{self.base_url}/f", payload=payload, headers=self.headers) tables = pd.read_html(page.text) desc[v] = tables[0]["Field Definition"].values[0] return desc def get_codes(self) -> str: """Get the definitions of letter codes in NID database.""" self.headers.update({"Cookie": "ORA_WWV_APP_105=ORA_WWV-Bk16kg_4BwSK2anC36B4XBQn"}) payload = {"p": "105:21:16137342922753::NO:::"} page = self.session.get(f"{self.base_url}/f", payload=payload, headers=self.headers) return page.text
def test_ipv4(): url = ( "https://edcintl.cr.usgs.gov/downloads/sciweb1/shared/uswem/web/conus" + "/eta/modis_eta/daily/downloads/det2004003.modisSSEBopETactual.zip") session = RetrySession() with session.onlyipv4(): r = session.get(url) z = zipfile.ZipFile(io.BytesIO(r.content)) fname = z.read(z.filelist[0].filename) assert sys.getsizeof(fname) == 4361682
class NLDI: """Access the Hydro Network-Linked Data Index (NLDI) service.""" def __init__(self) -> None: self.base_url = ServiceURL().restful.nldi self.session = RetrySession() resp = self.session.get("/".join([self.base_url, "linked-data"])).json() self.valid_fsources = {r["source"]: r["sourceName"] for r in resp} resp = self.session.get("/".join([self.base_url, "lookups"])).json() self.valid_chartypes = {r["type"]: r["typeName"] for r in resp} def getfeature_byid(self, fsource: str, fid: str, basin: bool = False) -> gpd.GeoDataFrame: """Get features of a single id. Parameters ---------- fsource : str The name of feature source. The valid sources are: comid, huc12pp, nwissite, wade, wqp fid : str The ID of the feature. basin : bool Whether to return the basin containing the feature. Returns ------- geopandas.GeoDataFrame NLDI indexed features in EPSG:4326. """ self._validate_fsource(fsource) url = "/".join([self.base_url, "linked-data", fsource, fid]) if basin: url += "/basin" return geoutils.json2geodf(self._geturl(url), ALT_CRS, DEF_CRS) def getcharacteristic_byid( self, comids: Union[List[str], str], char_type: str, char_ids: str = "all", values_only: bool = True, ) -> Union[pd.DataFrame, Tuple[pd.DataFrame, pd.DataFrame]]: """Get characteristics using a list ComIDs. Parameters ---------- comids : str or list The ID of the feature. char_type : str Type of the characteristic. Valid values are ``local`` for individual reach catchments, ``tot`` for network-accumulated values using total cumulative drainage area and ``div`` for network-accumulated values using divergence-routed. char_ids : str or list, optional Name(s) of the target characteristics, default to all. values_only : bool, optional Whether to return only ``characteristic_value`` as a series, default to True. If is set to False, ``percent_nodata`` is returned as well. Returns ------- pandas.DataFrame or tuple of pandas.DataFrame Either only ``characteristic_value`` as a dataframe or or if ``values_only`` is Fale return ``percent_nodata`` is well. """ if char_type not in self.valid_chartypes: valids = [ f'"{s}" for {d}' for s, d in self.valid_chartypes.items() ] raise InvalidInputValue("char", valids) comids = comids if isinstance(comids, list) else [comids] v_dict, nd_dict = {}, {} if char_ids == "all": payload = None else: _char_ids = char_ids if isinstance(char_ids, list) else [char_ids] valid_charids = self.get_validchars(char_type) idx = valid_charids.index if any(c not in idx for c in _char_ids): vids = valid_charids["characteristic_description"] raise InvalidInputValue( "char_id", [f'"{s}" for {d}' for s, d in vids.items()]) payload = {"characteristicId": ",".join(_char_ids)} for comid in comids: url = "/".join( [self.base_url, "linked-data", "comid", comid, char_type]) rjson = self._geturl(url, payload) char = pd.DataFrame.from_dict(rjson["characteristics"], orient="columns").T char.columns = char.iloc[0] char = char.drop(index="characteristic_id") v_dict[comid] = char.loc["characteristic_value"] if values_only: continue nd_dict[comid] = char.loc["percent_nodata"] def todf(df_dict: Dict[str, pd.DataFrame]) -> pd.DataFrame: df = pd.DataFrame.from_dict(df_dict, orient="index") df[df == ""] = np.nan df.index = df.index.astype("int64") return df.astype("f4") chars = todf(v_dict) if values_only: return chars return chars, todf(nd_dict) def get_validchars(self, char_type: str) -> pd.DataFrame: """Get all the avialable characteristics IDs for a give characteristics type.""" resp = self.session.get("/".join( [self.base_url, "lookups", char_type, "characteristics"])) c_list = ogc.utils.traverse_json( resp.json(), ["characteristicMetadata", "characteristic"]) return pd.DataFrame.from_dict( {c.pop("characteristic_id"): c for c in c_list}, orient="index") def navigate_byid( self, fsource: str, fid: str, navigation: str, source: str, distance: int = 500, ) -> gpd.GeoDataFrame: """Navigate the NHDPlus databse from a single feature id up to a distance. Parameters ---------- fsource : str The name of feature source. The valid sources are: comid, huc12pp, nwissite, wade, WQP. fid : str The ID of the feature. navigation : str The navigation method. source : str, optional Return the data from another source after navigating the features using fsource, defaults to None. distance : int, optional Limit the search for navigation up to a distance in km, defaults is 500 km. Note that this is an expensive request so you have be mindful of the value that you provide. Returns ------- geopandas.GeoDataFrame NLDI indexed features in EPSG:4326. """ self._validate_fsource(fsource) url = "/".join( [self.base_url, "linked-data", fsource, fid, "navigation"]) valid_navigations = self._geturl(url) if navigation not in valid_navigations.keys(): raise InvalidInputValue("navigation", list(valid_navigations.keys())) url = valid_navigations[navigation] r_json = self._geturl(url) valid_sources = {s["source"].lower(): s["features"] for s in r_json} if source not in valid_sources: raise InvalidInputValue("source", list(valid_sources.keys())) url = f"{valid_sources[source]}?distance={int(distance)}" return geoutils.json2geodf(self._geturl(url), ALT_CRS, DEF_CRS) def navigate_byloc( self, coords: Tuple[float, float], navigation: Optional[str] = None, source: Optional[str] = None, loc_crs: str = DEF_CRS, distance: int = 500, comid_only: bool = False, ) -> gpd.GeoDataFrame: """Navigate the NHDPlus databse from a coordinate. Parameters ---------- coordinate : tuple A tuple of length two (x, y). navigation : str, optional The navigation method, defaults to None which throws an exception if comid_only is False. source : str, optional Return the data from another source after navigating the features using fsource, defaults to None which throws an exception if comid_only is False. loc_crs : str, optional The spatial reference of the input coordinate, defaults to EPSG:4326. distance : int, optional Limit the search for navigation up to a distance in km, defaults to 500 km. Note that this is an expensive request so you have be mindful of the value that you provide. If you want to get all the available features you can pass a large distance like 9999999. comid_only : bool, optional Whether to return the nearest comid without navigation. Returns ------- geopandas.GeoDataFrame NLDI indexed features in EPSG:4326. """ _coords = MatchCRS().coords(((coords[0], ), (coords[1], )), loc_crs, DEF_CRS) lon, lat = _coords[0][0], _coords[1][0] url = "/".join([self.base_url, "linked-data", "comid", "position"]) payload = {"coords": f"POINT({lon} {lat})"} rjson = self._geturl(url, payload) comid = geoutils.json2geodf(rjson, ALT_CRS, DEF_CRS).comid.iloc[0] if comid_only: return comid if navigation is None or source is None: raise MissingItems(["navigation", "source"]) return self.navigate_byid("comid", comid, navigation, source, distance) def characteristics_dataframe( self, char_type: str, char_id: str, filename: Optional[str] = None, metadata: bool = False, ) -> Union[Dict[str, Any], pd.DataFrame]: """Get a NHDPlus-based characteristic from sciencebase.gov as dataframe. Parameters ---------- char_type : str Characteristic type. Valid values are ``local`` for individual reach catchments, ``tot`` for network-accumulated values using total cumulative drainage area and ``div`` for network-accumulated values using divergence-routed. char_id : str Characteristic ID. filename : str, optional File name, defaults to None that throws an error and shows a list of available files. metadata : bool Whether to only return the metadata for the selected characteristic, defaults to False. Useful for getting information about the dataset such as citation, units, column names, etc. Returns ------- pandas.DataFrame or dict The requested characteristic as a dataframe or if ``metadata`` is True the metadata as a dictionary. """ if char_type not in self.valid_chartypes: valids = [ f'"{s}" for {d}' for s, d in self.valid_chartypes.items() ] raise InvalidInputValue("char", valids) valid_charids = self.get_validchars(char_type) if char_id not in valid_charids.index: vids = valid_charids["characteristic_description"] raise InvalidInputValue( "char_id", [f'"{s}" for {d}' for s, d in vids.items()]) meta = self.session.get(valid_charids.loc[char_id, "dataset_url"], { "format": "json" }).json() if metadata: return meta flist = { f["name"]: f["downloadUri"] for f in meta["files"] if f["name"].split(".")[-1] == "zip" } if filename not in flist: raise InvalidInputValue("filename", list(flist.keys())) return pd.read_csv(flist[filename], compression="zip") def _validate_fsource(self, fsource: str) -> None: """Check if the given feature source is valid.""" if fsource not in self.valid_fsources: valids = [f'"{s}" for {d}' for s, d in self.valid_fsources.items()] raise InvalidInputValue("feature source", valids) def _geturl(self, url: str, payload: Optional[Dict[str, str]] = None): """Send a request to the service using GET method.""" if payload is None: payload = {"f": "json"} else: payload.update({"f": "json"}) try: return self.session.get(url, payload).json() except JSONDecodeError: raise ZeroMatched("No feature was found with the provided inputs.")
def get_connection_error(): url = "https://somefailedurl.com" s = RetrySession(retries=2) s.get(url)
class ScienceBase: """Access NHDPlus V2.1 Attributes from ScienceBase over CONUS. More info can be found `here <https://www.sciencebase.gov/catalog/item/5669a79ee4b08895842a1d47>`_. Parameters ---------- save_dir : str Directory to save the staged data frame containing metadata for the database, defaults to system's temp directory. The metadata dataframe is saved as a feather file, nhdplus_attrs.feather, in save_dir that can be loaded with Pandas. """ def __init__(self, save_dir: Optional[str] = None) -> None: self.save_dir = Path(save_dir) if save_dir else Path( tempfile.gettempdir()) if not self.save_dir.exists(): os.makedirs(self.save_dir) self.session = RetrySession() self.nhd_attr_item = "5669a79ee4b08895842a1d47" self.char_feather = Path(self.save_dir, "nhdplus_attrs.feather") def get_children(self, item: str) -> Dict[str, Any]: """Get childern items of an item.""" url = "https://www.sciencebase.gov/catalog/items" payload = { "filter": f"parentIdExcludingLinks={item}", "fields": "title,id", "format": "json", } return self.session.get(url, payload=payload).json() def get_files(self, item: str) -> Dict[str, Tuple[str, str]]: """Get all the available zip files in an item.""" url = "https://www.sciencebase.gov/catalog/item" payload = {"fields": "files,downloadUri", "format": "json"} r = self.session.get(f"{url}/{item}", payload=payload).json() files_url = zip(tlz.pluck("name", r["files"]), tlz.pluck("url", r["files"])) # TODO: Add units meta = "".join(tlz.pluck("metadataHtmlViewUri", r["files"], default="")) return { f.replace("_CONUS.zip", ""): (u, meta) for f, u in files_url if ".zip" in f } def stage_data(self) -> pd.DataFrame: """Stage the NHDPlus Attributes database and save to nhdplus_attrs.feather.""" r = self.get_children(self.nhd_attr_item) titles = tlz.pluck("title", r["items"]) titles = tlz.concat( tlz.map(tlz.partial(re.findall, "Select(.*?)Attributes"), titles)) titles = tlz.map(str.strip, titles) main_items = dict(zip(titles, tlz.pluck("id", r["items"]))) files = {} soil = main_items.pop("Soil") for i, item in main_items.items(): r = self.get_children(item) titles = tlz.pluck("title", r["items"]) titles = tlz.map( lambda s: s.split(":")[1].strip() if ":" in s else s, titles) child_items = dict(zip(titles, tlz.pluck("id", r["items"]))) files[i] = {t: self.get_files(c) for t, c in child_items.items()} r = self.get_children(soil) titles = tlz.pluck("title", r["items"]) titles = tlz.map(lambda s: s.split(":")[1].strip() if ":" in s else s, titles) child_items = dict(zip(titles, tlz.pluck("id", r["items"]))) stat = child_items.pop("STATSGO Soil Characteristics") ssur = child_items.pop("SSURGO Soil Characteristics") files["Soil"] = {t: self.get_files(c) for t, c in child_items.items()} r = self.get_children(stat) titles = tlz.pluck("title", r["items"]) titles = tlz.map(lambda s: s.split(":")[1].split(",")[1].strip(), titles) child_items = dict(zip(titles, tlz.pluck("id", r["items"]))) files["STATSGO"] = { t: self.get_files(c) for t, c in child_items.items() } r = self.get_children(ssur) titles = tlz.pluck("title", r["items"]) titles = tlz.map(lambda s: s.split(":")[1].strip(), titles) child_items = dict(zip(titles, tlz.pluck("id", r["items"]))) files["SSURGO"] = { t: self.get_files(c) for t, c in child_items.items() } chars = [] types = {"CAT": "local", "TOT": "upstream_acc", "ACC": "div_routing"} for t, dd in files.items(): for d, fd in dd.items(): for f, u in fd.items(): chars.append({ "name": f, "type": types.get(f[-3:], "other"), "theme": t, "description": d, "url": u[0], "meta": u[1], }) char_df = pd.DataFrame(chars, dtype="category") char_df.to_feather(self.char_feather) return char_df
class NLDI: """Access the Hydro Network-Linked Data Index (NLDI) service.""" def __init__(self) -> None: self.base_url = ServiceURL().restful.nldi self.session = RetrySession() resp = self.session.get("/".join([self.base_url, "linked-data"])).json() self.valid_fsources = {r["source"]: r["sourceName"] for r in resp} resp = self.session.get("/".join([self.base_url, "lookups"])).json() self.valid_chartypes = {r["type"]: r["typeName"] for r in resp} @staticmethod def _missing_warning(n_miss: int, n_tot: int) -> None: """Show a warning if there are misssing features.""" logger.warning(" ".join([ f"{n_miss} of {n_tot} inputs didn't return any features.", "They are returned as a list.", ])) def getfeature_byid( self, fsource: str, fid: Union[str, List[str]] ) -> Union[gpd.GeoDataFrame, Tuple[gpd.GeoDataFrame, List[str]]]: """Get feature(s) based ID(s). Parameters ---------- fsource : str The name of feature(s) source. The valid sources are: comid, huc12pp, nwissite, wade, wqp fid : str or list Feature ID(s). Returns ------- geopandas.GeoDataFrame or (geopandas.GeoDataFrame, list) NLDI indexed features in EPSG:4326. If some IDs don't return any features a list of missing ID(s) are returnd as well. """ self._validate_fsource(fsource) fid = fid if isinstance(fid, list) else [fid] urls = { f: "/".join([self.base_url, "linked-data", fsource, f]) for f in fid } features, not_found = self._get_urls(urls) if len(not_found) > 0: self._missing_warning(len(not_found), len(fid)) return features, not_found return features def comid_byloc( self, coords: Union[Tuple[float, float], List[Tuple[float, float]]], loc_crs: str = DEF_CRS, ) -> Union[gpd.GeoDataFrame, Tuple[gpd.GeoDataFrame, List[Tuple[float, float]]]]: """Get the closest ComID(s) based on coordinates. Parameters ---------- coords : tuple or list A tuple of length two (x, y) or a list of them. loc_crs : str, optional The spatial reference of the input coordinate, defaults to EPSG:4326. Returns ------- geopandas.GeoDataFrame or (geopandas.GeoDataFrame, list) NLDI indexed ComID(s) in EPSG:4326. If some coords don't return any ComID a list of missing coords are returnd as well. """ coords = coords if isinstance(coords, list) else [coords] coords_4326 = list( zip(*MatchCRS.coords(tuple(zip(*coords)), loc_crs, DEF_CRS))) base_url = "/".join( [self.base_url, "linked-data", "comid", "position"]) urls = {(coords[i][0], coords[i][1]): f"{base_url}?coords=POINT({lon} {lat})" for i, (lon, lat) in enumerate(coords_4326)} comids, not_found = self._get_urls(urls) comids = comids.reset_index(level=2, drop=True) if len(not_found) > 0: self._missing_warning(len(not_found), len(coords)) return comids, not_found return comids def get_basins( self, station_ids: Union[str, List[str]] ) -> Union[gpd.GeoDataFrame, Tuple[gpd.GeoDataFrame, List[str]]]: """Get basins for a list of station IDs. Parameters ---------- station_ids : str or list USGS station ID(s). Returns ------- geopandas.GeoDataFrame or (geopandas.GeoDataFrame, list) NLDI indexed basins in EPSG:4326. If some IDs don't return any features a list of missing ID(s) are returnd as well. """ station_ids = station_ids if isinstance(station_ids, list) else [station_ids] urls = { s: f"{self.base_url}/linked-data/nwissite/USGS-{s}/basin" for s in station_ids } basins, not_found = self._get_urls(urls) basins = basins.reset_index(level=1, drop=True) basins.index.rename("identifier", inplace=True) if len(not_found) > 0: self._missing_warning(len(not_found), len(station_ids)) return basins, not_found return basins def getcharacteristic_byid( self, comids: Union[List[str], str], char_type: str, char_ids: Union[str, List[str]] = "all", values_only: bool = True, ) -> Union[pd.DataFrame, Tuple[pd.DataFrame, pd.DataFrame]]: """Get characteristics using a list ComIDs. Parameters ---------- comids : str or list The ID of the feature. char_type : str Type of the characteristic. Valid values are ``local`` for individual reach catchments, ``tot`` for network-accumulated values using total cumulative drainage area and ``div`` for network-accumulated values using divergence-routed. char_ids : str or list, optional Name(s) of the target characteristics, default to all. values_only : bool, optional Whether to return only ``characteristic_value`` as a series, default to True. If is set to False, ``percent_nodata`` is returned as well. Returns ------- pandas.DataFrame or tuple of pandas.DataFrame Either only ``characteristic_value`` as a dataframe or or if ``values_only`` is Fale return ``percent_nodata`` as well. """ if char_type not in self.valid_chartypes: valids = [ f'"{s}" for {d}' for s, d in self.valid_chartypes.items() ] raise InvalidInputValue("char", valids) comids = comids if isinstance(comids, list) else [comids] v_dict, nd_dict = {}, {} if char_ids == "all": payload = None else: _char_ids = char_ids if isinstance(char_ids, list) else [char_ids] valid_charids = self.get_validchars(char_type) idx = valid_charids.index if any(c not in idx for c in _char_ids): vids = valid_charids["characteristic_description"] raise InvalidInputValue( "char_id", [f'"{s}" for {d}' for s, d in vids.items()]) payload = {"characteristicId": ",".join(_char_ids)} for comid in comids: url = "/".join( [self.base_url, "linked-data", "comid", comid, char_type]) rjson = self._get_url(url, payload) char = pd.DataFrame.from_dict(rjson["characteristics"], orient="columns").T char.columns = char.iloc[0] char = char.drop(index="characteristic_id") v_dict[comid] = char.loc["characteristic_value"] if values_only: continue nd_dict[comid] = char.loc["percent_nodata"] def todf(df_dict: Dict[str, pd.DataFrame]) -> pd.DataFrame: df = pd.DataFrame.from_dict(df_dict, orient="index") df[df == ""] = np.nan df.index = df.index.astype("int64") return df.astype("f4") chars = todf(v_dict) if values_only: return chars return chars, todf(nd_dict) def get_validchars(self, char_type: str) -> pd.DataFrame: """Get all the avialable characteristics IDs for a give characteristics type.""" resp = self.session.get("/".join( [self.base_url, "lookups", char_type, "characteristics"])) c_list = ogc.utils.traverse_json( resp.json(), ["characteristicMetadata", "characteristic"]) return pd.DataFrame.from_dict( {c.pop("characteristic_id"): c for c in c_list}, orient="index") def navigate_byid( self, fsource: str, fid: str, navigation: str, source: str, distance: int = 500, ) -> gpd.GeoDataFrame: """Navigate the NHDPlus databse from a single feature id up to a distance. Parameters ---------- fsource : str The name of feature source. The valid sources are: comid, huc12pp, nwissite, wade, WQP. fid : str The ID of the feature. navigation : str The navigation method. source : str, optional Return the data from another source after navigating the features using fsource, defaults to None. distance : int, optional Limit the search for navigation up to a distance in km, defaults is 500 km. Note that this is an expensive request so you have be mindful of the value that you provide. Returns ------- geopandas.GeoDataFrame NLDI indexed features in EPSG:4326. """ self._validate_fsource(fsource) url = "/".join( [self.base_url, "linked-data", fsource, fid, "navigation"]) valid_navigations = self._get_url(url) if navigation not in valid_navigations.keys(): raise InvalidInputValue("navigation", list(valid_navigations.keys())) url = valid_navigations[navigation] r_json = self._get_url(url) valid_sources = {s["source"].lower(): s["features"] for s in r_json} # type: ignore if source not in valid_sources: raise InvalidInputValue("source", list(valid_sources.keys())) url = f"{valid_sources[source]}?distance={int(distance)}" return geoutils.json2geodf(self._get_url(url), ALT_CRS, DEF_CRS) def navigate_byloc( self, coords: Tuple[float, float], navigation: Optional[str] = None, source: Optional[str] = None, loc_crs: str = DEF_CRS, distance: int = 500, ) -> gpd.GeoDataFrame: """Navigate the NHDPlus databse from a coordinate. Parameters ---------- coords : tuple A tuple of length two (x, y). navigation : str, optional The navigation method, defaults to None which throws an exception if comid_only is False. source : str, optional Return the data from another source after navigating the features using fsource, defaults to None which throws an exception if comid_only is False. loc_crs : str, optional The spatial reference of the input coordinate, defaults to EPSG:4326. distance : int, optional Limit the search for navigation up to a distance in km, defaults to 500 km. Note that this is an expensive request so you have be mindful of the value that you provide. If you want to get all the available features you can pass a large distance like 9999999. Returns ------- geopandas.GeoDataFrame NLDI indexed features in EPSG:4326. """ _coords = MatchCRS().coords(((coords[0], ), (coords[1], )), loc_crs, DEF_CRS) lon, lat = _coords[0][0], _coords[1][0] url = "/".join([self.base_url, "linked-data", "comid", "position"]) payload = {"coords": f"POINT({lon} {lat})"} rjson = self._get_url(url, payload) comid = geoutils.json2geodf(rjson, ALT_CRS, DEF_CRS).comid.iloc[0] if navigation is None or source is None: raise MissingItems(["navigation", "source"]) return self.navigate_byid("comid", comid, navigation, source, distance) def _validate_fsource(self, fsource: str) -> None: """Check if the given feature source is valid.""" if fsource not in self.valid_fsources: valids = [f'"{s}" for {d}' for s, d in self.valid_fsources.items()] raise InvalidInputValue("feature source", valids) def _get_urls(self, urls: Dict[Any, str]) -> Tuple[gpd.GeoDataFrame, List[str]]: """Get basins for a list of station IDs. Parameters ---------- urls_dict : dict A dict with keys as feature ids and values as corresponsing url. Returns ------- (geopandas.GeoDataFrame, list) NLDI indexed features in EPSG:4326 and list of ID(s) that no feature was found. """ not_found = [] resp = [] for f, u in urls.items(): try: rjson = self._get_url(u) resp.append((f, geoutils.json2geodf(rjson, ALT_CRS, DEF_CRS))) except (ZeroMatched, JSONDecodeError, ConnectionError): not_found.append(f) if len(resp) == 0: raise ZeroMatched("No feature was found with the provided inputs.") resp_df = gpd.GeoDataFrame(pd.concat(dict(resp))) return resp_df, not_found def _get_url(self, url: str, payload: Optional[Dict[str, str]] = None) -> Dict[str, Any]: """Send a request to the service using GET method.""" if payload: payload.update({"f": "json"}) else: payload = {"f": "json"} try: return self.session.get(url, payload).json() except JSONDecodeError: raise ZeroMatched("No feature was found with the provided inputs.") except ConnectionError: raise ConnectionError( "NLDI server cannot be reached at the moment.")