"""
hydrofunctions.validate
~~~~~~~~~~~~~~~~~~~~~~~
This module contains functions for testing that user input is valid.
Why 'pre-check' user imputs, instead of using standard
python duck typing? These functions are meant to enhance an interactive
session for the user, and will check a user's parameters
before requesting data from an online resource. Otherwise, the server will
return a 404 code and the user will have no idea why. Hydrofunctions tries to raise
an exception (usually a TypeError) before a request is made, so that the user
can fix their request. It also tries to provide a helpful error message to an
interactive session user.
Suggested format for these functions:
* first check that the input is a string,
* then do a regular expression to check that the input is more or less valid.
* raise exceptions when user input breaks format.
-----
"""
import re
[docs]def check_parameter_string(candidate, param):
"""Checks that a parameter is a string or a list of strings."""
parameters = {
"site": "NWIS station id(s) should be a string or list of strings,"
+ "often in the form of an eight digit number enclosed in quotes.",
"parameterCd": "NWIS parameter codes are five-digit strings that specify "
+ "the parameter that is being measured at the site. Common "
+ "codes are '00060' for stream stage in feet, '00065' for "
+ "stream discharge in cubic feet per second, and '72019' for "
+ "groundwater levels. Not all sites collect data for all "
+ "parameters. See a complete list of physical parameters here: "
+ "https://help.waterdata.usgs.gov/parameter_cd?group_cd=PHY "
+ "You may request multiple parameters by submitting a comma-"
+ "delimited string of codes with no spaces, or by submitting "
+ "a list of codes, like this: parameterCd = '00065,00060' or "
+ "parameterCd = ['00065', '00060'] ",
"county": "The NWIS county parameter accepts a five-digit string or "
+ "a list of five-digit strings to select all of the sites "
+ "within a county or list of counties. "
+ "Example: '51059' or ['51059', '51061'] are acceptable.",
"state": "This parameter uses US two-letter postal codes "
+ "such as 'MD' for Maryland or 'AZ' for Arizona.",
"default": "This parameter should be a string or a list of strings.",
}
if param in parameters:
msg = parameters[param] + " Actual value: {}".format(candidate)
else:
msg = (
"This parameter should be a string or a list of strings."
+ " Actual value: {}".format(candidate)
)
if candidate is None:
return None
elif isinstance(candidate, str) and candidate:
return candidate
elif isinstance(candidate, (list, tuple)) and candidate:
for s in candidate:
if not isinstance(s, str):
raise TypeError(msg + " bad element: {}".format(s))
return ",".join(str(s) for s in candidate)
else:
raise TypeError(msg)
[docs]def check_NWIS_bBox(input):
"""Checks that the USGS bBox is valid."""
msg = (
"NWIS bBox should be a string, list of strings, or tuple "
+ "containing the longitude and latitude of the lower left corner "
+ "of the bounding box, followed by the longitude and latitude "
+ "of the upper right corner of the bounding box. Most often in "
+ 'the form of "ll_long,ll_lat,ur_long,ur_lat" . '
+ "All latitude and longitude values should have less than 8 "
+ "places. "
+ "Actual value: {}".format(input)
)
if input is None:
return None
# assume that if it is a string it will be fine as is.
# don't accept a series of sites in a single string.
# Test for and reject empty strings: empty strings are false.
if isinstance(input, str) and input:
t = input.split(",")
if len(t) < 4:
raise TypeError(msg)
return input
# test for input is a list and it is not empty
elif (isinstance(input, list) or isinstance(input, tuple)) and input:
if len(input) < 4:
raise TypeError(msg)
# format: [-83.000000, 36.500000, -81.000000, 38.500000] ==> '-83.000000,36.500000,-81.000000,38.500000'
return ",".join([str(s) for s in input])
else:
raise TypeError(msg)
[docs]def check_NWIS_service(input):
"""Checks that the service is valid: either 'iv' or 'dv'"""
if input is None:
return None
if input in ["iv", "dv"]:
return input
else:
raise TypeError(
"The NWIS service type accepts 'dv' for daily values, "
"or 'iv' for instantaneous values. Actual value: "
"{}".format(input)
)
[docs]def check_datestr(input):
"""Checks that the start_date or end_date parameter is in yyyy-mm-dd format."""
# Use a regular expression to ensure in form of yyyy-mm-dd
if input is None:
return None
pattern = r"[1-2]\d\d\d-[0-1]\d-[0-3]\d\Z"
datestr = re.compile(pattern)
if isinstance(input, str) and datestr.match(input):
return input
else:
raise TypeError(
"Dates should be a string in the form of 'YYYY-MM-DD' "
"enclosed in quotes. Actual value: {}".format(input)
)
[docs]def check_period(input):
"""Checks that the period parameter in is the P##D format, where ## is
the number of days before now.
"""
if input is None:
return None
# TODO: check how many days maximum NWIS is willing to respond to.
# This pattern sets a maximum of 999 days (between 1 and 3 digits).
pattern = r"^P\d{1,3}D$"
periodstr = re.compile(pattern)
if isinstance(input, str) and periodstr.match(input):
return input
else:
raise TypeError(
"Period should be a string in the form of 'PxD', "
"where x represents the number of days before today, "
"with a maximum of 999 days. "
"Example: to request the previous 10 days, "
"enter 'period=P10D'. Actual value entered: {}".format(input)
)