Source code for hydrofunctions.validate

"""
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) )