Repository: dClimate/dWeather-Python-Client (language: python)

Issue:
Big refactor evan



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 69f851b..3b13733 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.coverage
 share
 dist
 *.egg-info
diff --git a/README.md b/README.md
index 9fe804d..d6eb67a 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,11 @@ See further examples in `tests`
 
 ## Development
 
+    cd dweather-python-client
+    python3 -m venv .
+    bin/pip3 install -r requirements.txt
+    bin/python3 -m pytest dweather_clieht/tests
+
 Some dweather_client features require an ipfs daemon to work.
 
 ### Download Go-IPFS version 0.6.0
diff --git a/dweather_client/aliases_and_units.py b/dweather_client/aliases_and_units.py
new file mode 100644
index 0000000..438bba2
--- /dev/null
+++ b/dweather_client/aliases_and_units.py
@@ -0,0 +1,203 @@
+"""
+Functions associated with aliases, units, converting between 
+different conventions, finding grids and stations that are the closest to a 
+given point.
+
+In general, all the idiosyncratic reality-based things that one has to 
+deal with.
+"""
+from dweather_client.ipfs_errors import AliasNotFound
+import zeep
+import os
+from astropy import units as u
+from astropy.units import imperial
+import pandas as pd
+
+FLASK_DATASETS = [
+    "rtma_pcp-hourly",
+    "chirpsc_final_05-daily",
+    "chirpsc_final_25-daily",
+    "chirpsc_prelim_05-daily",
+    "cpcc_precip_global-daily",
+    "cpcc_precip_us-daily",
+    "cpcc_temp_max-daily",
+    "cpcc_temp_min-daily",
+    "prismc-tmax-daily",
+    "prismc-tmin-daily",
+    "prismc-precip-daily",
+    "era5_land_wind_u-hourly",
+    "era5_land_wind_v-hourly"
+]
+
+UNIT_ALIASES = {
+    "kg/m**2": u.kg / u.m**2,
+    "mm": u.mm,
+    "degC": u.deg_C,
+    "m s**-1": u.m / u.s,
+    "degF": imperial.deg_F
+}
+
+METRIC_TO_IMPERIAL = {
+    u.mm: lambda q: q.to(imperial.inch),
+    u.deg_C: lambda q: q.to(imperial.deg_F, equivalencies=u.temperature()),
+    u.kg / u.m**2: lambda q: q.to(imperial.pound / imperial.ft ** 2),
+    u.m / u.s: lambda q: q.to(imperial.mile / u.hour)
+}
+
+IMPERIAL_TO_METRIC = {
+    imperial.inch: lambda q: q.to(u.mm),
+    imperial.deg_F: lambda q: q.to(u.deg_C, equivalencies=u.temperature()),
+    imperial.pound / imperial.ft ** 2: lambda q: q.to(u.kg / u.m**2),
+    imperial.mile / u.hour: lambda q: q.to(u.m / u.s)
+}
+
+STATION_ALIASES_TO_COLUMNS = {
+    ('SNWD',
+     'snow depth',
+     'snowdepth'): 'SNWD',
+    ('SNOW',
+     'snow fall',
+     'snowfall',
+     'snow'): 'SNOW',
+    ('WESD',
+     'snow water equivalent',
+     'water equivalent snow depth'): 'WESD',
+    ('TMAX',
+     'highs',
+     'max temperature',
+     'temperature max',
+     'maximum temperature',
+     'temperature maximum',
+     'max temp',
+     'temp max',
+     'maximum temp',
+     'temp maximum'): 'TMAX',
+    ('TMIN',
+     'lows',
+     'min temperature',
+     'temperature min'
+     'minimum temperature',
+     'temperature minimum',
+     'min temp',
+     'temp min',
+     'minimum temp',
+     'temp minimum'): 'TMIN',
+    ('PRCP',
+     'precipitation',
+     'precip',
+     'rain',
+     'rainfall'): 'PRCP'
+}
+
+def lookup_station_alias(alias):
+    for aliases in STATION_ALIASES_TO_COLUMNS:
+        if alias in aliases:
+            return STATION_ALIASES_TO_COLUMNS[aliases]
+    raise AliasNotFound()
+
+# see ghcnd readme ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt
+STATION_UNITS_LOOKUP = {
+    'PRCP': {'vectorize': lambda m: (m/10.0) * u.mm, 
+             'imperialize': lambda m: m.to(imperial.inch)},
+    'SNWD': {'vectorize': lambda m: m * u.mm, 
+             'imperialize': lambda m: m.to(imperial.inch)},
+    'SNOW': {'vectorize': lambda m: m * u.mm, 
+             'imperialize': lambda m: m.to(imperial.inch)},
+    'WESD': {'vectorize': lambda m: (m/10.0) * u.mm, 
+             'imperialize': lambda m: m.to(imperial.inch)},
+    'TMAX': {'vectorize': lambda m: (m/10.0) * u.deg_C, 
+             'imperialize': lambda m: m.to(imperial.deg_F, equivalencies=u.temperature())},
+    'TMIN': {'vectorize': lambda m: (m/10.0) * u.deg_C, 
+             'imperialize': lambda m: m.to(imperial.deg_F, equivalencies=u.temperature())},
+}
+
+parent_dir = os.path.dirname(os.path.abspath(__file__))
+CPC_LOOKUP_PATH = os.path.join(parent_dir, '/etc/cpc-grid-ids.csv')
+ICAO_LOOKUP_PATH = os.path.join(parent_dir, '/etc/airport-codes.csv')
+
+
+def lookup_station_column_units(column_name):
+    """
+    Get the metric and imperial units for a given station column name
+    """
+    return STATION_UNITS_LOOKUP[lookup_station_column_alias(column_name)]
+
+
+def icao_to_ghcn(icao_code):
+    """ 
+    Convert an icao airport code to ghcn.
+    return:
+        latitude, longitude
+    args:
+        icao = "xxxx" for example try "KLGA"
+    """
+    icao_codes = pd.read_csv(os.path.join(ICAO_LOOKUP_PATH)).set_index('ICAO')  # get lookup table
+    return icao_codes.loc[icao_code]["GHCN"]
+
+
+def get_station_ids_with_icao():
+    """
+    Get a list of all the station id that are associated with stations that have an icao code.
+    """
+    icao_lookup = pd.read_csv(os.path.join(ICAO_LOOKUP_PATH)).set_index('ICAO')
+    ids = []
+    for index, row in icao_lookup.iterrows():
+        if (row["GHCN"] not in ids):
+            ids.append(row["GHCN"])
+    return ids
+
+
+def cpc_grid_to_lat_lon(grid_id):
+    """ 
+    Convert a cpc grid id to conventional lat lon via a lookup table.
+    return:
+        latitude, longitude
+
+    args:
+        grid_id = "1100" example
+
+    """
+    cpc_grids = pd.read_csv(os.path.join(CPC_LOOKUP_PATH)).set_index('Grid ID')  # dataframe of cpc grid to lat/lon lookup table
+    myGrid = cpc_grids.iloc[grid_id]
+    coords = [myGrid["Latitude"], myGrid["Longitude"]]
+    coords = [coord+360 if coord < 0 else coord for coord in coords]  # converts negative coordinates to positive values
+
+    return coords[0], coords[1]
+
+
+def lat_lon_to_rtma_grid(lat, lon, grid_history):
+    grid_dict = build_rtma_reverse_lookup(grid_history)
+    response = {}
+    for timestamp in grid_dict:
+        try:
+            response[timestamp] = (grid_dict[timestamp]['lat'][lat], grid_dict[timestamp]['lon'][lon])
+        except KeyError:
+            response[timestamp] = (None, None)
+            continue
+    return response
+
+
+def rtma_grid_to_lat_lon(x, y, grid_history):
+    """
+    x: the x coordinate of the rtma grid
+    y: the y coordinate of the rtma grid
+    grid_history: the raw string representation of the rtma grid history to be
+        read directly from a file
+    """
+    grid_dict = build_rtma_lookup(grid_history)
+
+    # get the lat/lon associated with x and y.
+    return [(grid_dict[timestamp][0][y][x], grid_dict[timestamp][1][y][x]) for timestamp in grid_dict]
+
+
+def snotel_to_ghcnd(snotel_id, state_fips):
+    """
+    Convert a SnoTEL ID to a station id that can be passed into GHCND or
+    GHCNDi. snotel_id is a 3 or 4 digit integer, and the state_fips is 
+    a two character abbrevation for the state -- for example CO.
+    """
+    client = zeep.Client('https://wcc.sc.egov.usda.gov/awdbWebService/services?WSDL')
+    result = client.service.getStationMetadata(
+        '%s:%s:SNTL' % (str(snotel_id), str(state_fips)))
+    ghcn_id = 'USS00%s' % result['actonId']
+    return ghcn_id
diff --git a/dweather_client/arithmetic.py b/dweather_client/arithmetic.py
deleted file mode 100644
index f352eb4..0000000
--- a/dweather_client/arithmetic.py
+++ /dev/null
@@ -1,106 +0,0 @@
-"""
-Some functions for organizing dWeather query results into formats relevant to Arbol's use cases.
-Mainly related to summing and averaging over time.
-This module should not import pandas or numpy
-"""
-from dweather_client.http_client import get_rainfall_dict, get_rev_rainfall_dict
-from dweather_client.utils import listify_period
-import datetime
-
-HISTORICAL_START_YEAR = 1981
-
-def sum_period_rainfall(lat, lon, dataset, start_date, end_date, daily_cap=None, use_prelim=False, final_rev=None):
-    ''' Download the rainfall data from ipfs and compute the rainfall for the given term
-    Args:
-        lat (float): latitude of grid cell
-        lon (float): longitude of grid cell
-        dataset (str): the rainfall dataset name in ipfs
-        start_date (datetime.date): first date of term to include
-        end_date (datetime.date): last date of term to include
-        daily_cap (float): max rainfall in mm to count for a day
-        use_prelim (bool): if true, include prelim data and get as much of the period as possible
-    returns:
-        (float, int, bool)
-            float: the total rainfall
-            int: the number of days in the period
-            bool: true if all rainfall is in the 'Final' dataset
-    Raises:
-        DatasetError: If no matching dataset found on server
-        InputOutOfRangeError: If the lat/lon is outside the dataset range in metadata
-        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
-        DataMalformedError: If the grid cell file can't be parsed as rainfall data
-    '''
-    is_final = True
-    if use_prelim:
-        rainfall_dict, is_final = get_rev_rainfall_dict(lat, lon, dataset, end_date, final_rev)
-        # The rainfall dict is sorted by date, so the last date will be the most recent (prelim) data
-        # Need to check if the end date is in the rainfall dict, and if not, truncate the term to the end of the data
-        if not is_final and end_date not in rainfall_dict:
-            end_date = list(rainfall_dict)[-1]
-    else:
-        rainfall_dict = get_rainfall_dict(lat, lon, dataset)
-    dates = listify_period(start_date, end_date)
-    if daily_cap:
-        rain = [min(rainfall_dict[date], daily_cap) for date in dates]
-    else:
-        rain = [rainfall_dict[date] for date in dates]
-    return (sum(rain), len(rain), is_final)
-
-def build_historical_rainfall_dict(lat, lon, dataset, start_date, end_date, daily_cap=None, start_year=HISTORICAL_START_YEAR, end_year=None, use_prelim=False, final_rev=None, ignore_missing=False):
-    ''' Builds a dict of rainfall values over the term period for each year for a grid cell
-    Args:
-        lat (float): latitude of grid cell
-        lon (float): longitude of grid cell
-        dataset (str): the rainfall dataset name in ipfs
-        start_date (datetime.date): first date of term to include
-        end_date (datetime.date): last date of term to include
-        daily_cap (float): max rainfall in mm to count for a day
-        start_year (int): the first year of historical data to include, using the start date
-        end_year (int): the last year of data to include with the start_date year
-        use_prelim (bool): if True, include prelim rainfall if there is not enough final rainfall
-                if there is not enough rainfall including prelim then truncate the period to where we have data
-        ignore_missing (bool): if true, substitute '0' for the missing rainfall value
-                if false, set the whole year as None
-    returns
-        dict of int: (float, int): keys are the start date year for each term, values are
-                a tuple of total rainfall and the number of days in the term for the term period starting that year
-        bool is_final: true if all data included is from the final dataset
-        If there is missing rainfall data for a period, the value for that year will be None, None
-    Raises:
-        DatasetError: If no matching dataset found on server
-        InputOutOfRangeError: If the lat/lon is outside the dataset range in metadata
-        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
-        DataMalformedError: If the grid cell file can't be parsed as rainfall data
-    '''
-    is_final = True
-    if use_prelim:
-        rainfall_dict, is_final = get_rev_rainfall_dict(lat, lon, dataset, end_date, final_rev)
-        # The rainfall dict is sorted by date, so the last date will be the most recent (prelim) data
-        # Need to check if the end date is in the rainfall dict, and if not, truncate the term to the end of the data
-        if not is_final and end_date not in rainfall_dict:
-            end_date = list(rainfall_dict)[-1]
-    else:
-        rainfall_dict = get_rainfall_dict(lat, lon, dataset)
-    yearly_rainfall = {}
-    if end_year is None:
-        # If the end year is not provided they probably want all data up to the current year
-        end_year = start_date.year
-    for year in range(start_year, end_year + 1):
-        historical_start = start_date.replace(year=year)
-        # end date is tricky because it could be in the next calendar year
-        historical_end = end_date.replace(year=year + (end_date.year - start_date.year))
-        year_term = listify_period(historical_start, historical_end)
-        yearly_rain = [rainfall_dict[date] for date in year_term]
-        if None in yearly_rain:
-            if ignore_missing:
-                # replace None values with 0
-                yearly_rain = [x if x is not None else 0 for x in yearly_rain] 
-            else:
-                # return None for the whole year
-                yearly_rainfall[year] = None, None
-                continue
-        if daily_cap:
-            yearly_rain = list(map(lambda x: min(x, daily_cap), yearly_rain))
-        yearly_rainfall[year] = (sum(yearly_rain), len(yearly_rain))
-            
-    return yearly_rainfall, is_final
diff --git a/dweather_client/client.py b/dweather_client/client.py
new file mode 100644
index 0000000..68c5908
--- /dev/null
+++ b/dweather_client/client.py
@@ -0,0 +1,207 @@
+"""
+Use these functions to get historical climate data.
+"""
+from dweather_client.http_queries import get_station_csv, get_dataset_cell, get_metadata, get_heads
+from dweather_client.aliases_and_units import \
+    lookup_station_alias, STATION_UNITS_LOOKUP as SUL, METRIC_TO_IMPERIAL as M2I, IMPERIAL_TO_METRIC as I2M, FLASK_DATASETS, UNIT_ALIASES
+from dweather_client.ipfs_errors import AliasNotFound, DataMalformedError
+from dweather_client.grid_utils import snap_to_grid, conventional_lat_lon_to_cpc, cpc_lat_lon_to_conventional
+from dweather_client.http_queries import flask_query
+from dweather_client.struct_utils import tupleify
+from dweather_client.df_loader import get_atcf_hurricane_df, get_historical_hurricane_df, get_simulated_hurricane_df
+import datetime, pytz, csv
+from astropy import units as u
+import pandas as pd
+import numpy as np
+from timezonefinder import TimezoneFinder
+
+
+def get_gridcell_history(
+        lat,
+        lon,
+        dataset,
+        snap_lat_lon_to_closest_valid_point=True,
+        also_return_snapped_coordinates=False,
+        protocol='https',
+        also_return_metadata=False,
+        use_imperial_units=True,
+        return_result_as_counter=False):
+    """
+    Get the historical timeseries data for a gridded dataset in a dictionary,
+    or, if return_result_as_counter is set to True, as a collections.Counter
+
+    This is a dictionary of dates: climate values for a given dataset and 
+    lat, lon.
+
+    If snap_lat_lon_to_closest_valid_point is set to True (which it is
+    by default), returns the history for the closest valid lat lon as
+    determined by the dataset's metadata resolution.
+
+    protocol is set to 'https' by default, but can also be set to
+    'ipfs'. There are performance tradeoffs depending on which protocol is
+    selected.
+
+    return_result_as_counter is set to False by default, but if it is set
+    to true, the historical timeseries will be returned as a collections.Counter
+    instead of as a dict. collections.Counter is useful for performance sensitive
+    aggregations, for example as in grid_utils.get_polygon_df
+
+    also_return_metadata is set to False by default, but if set to True,
+    returns the metadata next to the dict/counter within a tuple.
+
+    use_imperial_units is set to True by default, but if set to False,
+    will get the appropriate metric unit from aliases_and_units
+    """
+    metadata = get_metadata(get_heads()[dataset])
+
+    # set up units
+    str_u = metadata['unit of measurement']
+    with u.imperial.enable():
+        dweather_unit = UNIT_ALIASES[str_u] if str_u in UNIT_ALIASES else u.Unit(str_u)
+    converter = None
+    # if imperial is desired and dweather_unit is metric
+    if use_imperial_units and (dweather_unit in M2I):
+        converter = M2I[dweather_unit]
+    # if metric is desired and dweather_unit is imperial
+    elif (not use_imperial_units) and (dweather_unit in I2M):
+        converter = I2M[dweather_unit]
+
+    # get dataset-specific "no observation" value
+    missing_value = metadata["missing value"]
+
+    history_dict = {}
+    (lat, lon), resp_dict = flask_query(dataset, lat, lon)
+    for k in resp_dict:
+        if type(missing_value) == str:
+            val = np.nan if resp_dict[k] == missing_value else float(resp_dict[k])
+        else:
+            val = np.nan if float(resp_dict[k]) == missing_value else float(resp_dict[k])
+        datapoint = val * dweather_unit
+        if converter is not None:
+            datapoint = converter(datapoint)
+        history_dict[k] = datapoint
+
+    # try a timezone-based transformation on the times in case we're using an hourly set.
+    try:
+        tf = TimezoneFinder()
+        local_tz = pytz.timezone(tf.timezone_at(lng=lon, lat=lat))
+        tz_history_dict = {}
+        for time in history_dict:
+            tz_history_dict[pytz.utc.localize(time).astimezone(local_tz)] = history_dict[time]
+        history_dict = tz_history_dict
+    except AttributeError:  # datetime.date (daily sets) doesn't work with this, only datetime.datetime (hourly sets)
+        pass
+
+    result = history_dict
+    if also_return_metadata:
+        result = tupleify(result) + ({"metadata": metadata},)
+    if also_return_snapped_coordinates:
+        result = tupleify(result) + ({"snapped to": (lat, lon)},)
+    return result
+
+def get_tropical_storms(
+        source,
+        basin,
+        radius=None,
+        lat=None,
+        lon=None,
+        min_lat=None,
+        min_lon=None,
+        max_lat=None,
+        max_lon=None):
+    """
+    return:
+        pd.DataFrame containing time series information on tropical storms
+    args:
+        source (str), one of: 'atcf', 'historical', 'simulated'
+        basin (str),
+            if source is 'atcf', one of: 'AL', 'CP', 'EP', 'SL'
+            if source is 'simulated', one of: 'EP', 'NA', 'NI', 'SI', 'SP' or 'WP'
+            if source is 'historical', one of: 'NI', 'SI', 'NA', 'EP', 'WP', 'SP', 'SA'
+        radius (float), lat (float), lon (float),
+            if given radius, lat, lon, will subset df to only include points within radius in km of the point (lat, lon)
+        min_lat (float), min_lon (float), max_lat (float), max_lon (float)
+            if given kwargs min_lat, min_lon, max_lat, max_lon, selects points within a bounding box.
+    Note:
+        (radius, lat, lon) and (min_lat, min_lon, max_lat, max_lon) are incompatible kwargs.
+        i.e., if function is given args containing members from both tuples, raise ValueError
+        in addition, the function's args must contain either all members of one of the above tuples, or none
+        i.e., if function is given args containing some but not all of the members of one of the above tuples, raise ValueError
+    """
+    if ((radius is not None) or (lat is not None) or (lon is not None)) \
+            and ((radius is None) or (lat is None) or (lon is None)):
+        raise ValueError
+    if ((min_lat is not None) or (min_lon is not None) or (max_lat is not None) or (max_lon is not None)) \
+            and ((min_lat is None) or (min_lon is None) or (max_lat is None) or (max_lon is None)):
+        raise ValueError
+    if radius and min_lat:
+        raise ValueError
+
+    if source == "atcf":
+        storm_getter = get_atcf_hurricane_df
+    elif source == "historical":
+        storm_getter = get_historical_hurricane_df
+    elif source == "simulated":
+        storm_getter = get_simulated_hurricane_df
+    else:
+        raise ValueError
+
+    if radius:
+        return storm_getter(basin, radius=radius, lat=lat, lon=lon)
+    elif min_lat:
+        return storm_getter(basin, min_lat=min_lat, min_lon=min_lon, max_lat=max_lat, max_lon=max_lon)
+    else:
+        return storm_getter(basin)
+
+def get_station_history(
+        station_id,
+        weather_variable,
+        dataset='ghcnd',
+        protocol='https',
+        use_imperial_units=True):
+    """
+    Takes in a station id and a weather variable.
+
+    Gets the csv body associated with the station_id, defaulting to the
+    ghcnd dataset. Pass in dataset='ghcnd-imputed-daily' for imputed,
+    though note that ghcndi is only temperature as of this writing.
+
+    Passing in use_imperial_units=False will return results in metric. 
+    Imperial is the default as Arbol is based in the USA and the bulk of our 
+    deals are done in imperial.
+
+        'SNWD' or alias 'snow depth' -- the depth of snow at the time of the
+        observation
+        'SNOW' or alias 'snowfall -- the total snowfall observed since the
+        last observation
+        'WESD' or alias 'snow water equivalent', 'water equivalent snow depth' 
+        -- the water level in inchesequivalent to the amount of snow currently 
+        on the ground at the time of the observation.
+        'TMAX' -- daily high temperature
+        'TMIN' -- daily low temperature
+        'PRCP' -- depth of rainfall
+
+    The GHCN column names are fairly esoteric so a column_lookup
+    dictionary will try to find a valid GHCN column name for common 
+    aliases.
+
+    """
+    csv_text = get_station_csv(station_id, station_dataset=dataset)
+    column = lookup_station_alias(weather_variable)
+    history = {}
+    reader = csv.reader(csv_text.split('\n'))
+    headers = next(reader)
+    date_col = headers.index('DATE')
+    data_col = headers.index(column)
+    for row in reader:
+        try:
+            if row[data_col] == '':
+                continue
+        except IndexError:
+            continue
+        datapoint = SUL[column]['vectorize'](float(row[data_col]))
+        if use_imperial_units:
+            datapoint = SUL[column]['imperialize'](datapoint)
+        history[datetime.datetime.strptime(row[date_col], "%Y-%m-%d").date()] = datapoint
+
+    return history
\ No newline at end of file
diff --git a/dweather_client/data_loader.py b/dweather_client/data_loader.py
deleted file mode 100644
index b9441c3..0000000
--- a/dweather_client/data_loader.py
+++ /dev/null
@@ -1,162 +0,0 @@
-from dweather_client.http_client import get_rainfall_dict, get_temperature_dict, get_station_csv, parse_station_snowfall_as_dict, parse_station_snow_depth_as_dict, parse_station_temps_as_dict
-import dweather_client.ipfs_datasets
-
-class GridCellDataLoader:
-
-    _instances = {}
-
-    def __init__(self, dataset, lat, lon, weather_type):
-        """
-        Args:
-            lat (float): the grid cell latitude
-            lon (float): the grid cell longitude
-            weather_type (str): either rainfall or temperature
-        """
-        self.revisions = {}
-        self.dataset = dataset
-        self.lat = lat
-        self.lon = lon
-        assert weather_type == "rainfall" or weather_type == "temperature", \
-            "weather_type must be 'rainfall' or 'temperature'"
-        self.weather_type = weather_type
-
-    def __new__(cls, dataset, lat, lon, weather_type):
-        """ Create new class only if one with args doesn't exist
-        Similar to singleton class, we only want one instance of DataLoader for each grid cell
-        """
-        if (dataset, lat, lon, weather_type) not in cls._instances:
-            cls._instances[(dataset, lat, lon, weather_type)] = super(GridCellDataLoader, cls).__new__(cls)
-        return cls._instances[(dataset, lat, lon, weather_type)]
-
-    
-    def load_revision(self, revision):
-        """ Get the revision from ipfs and save it
-        Args:
-            revision (str): the named dataset revision on the ipfs gateway
-        """
-        if self.weather_type == "rainfall":
-            metadata, data = get_rainfall_dict(self.lat, self.lon, revision, return_metadata=True)
-        elif self.weather_type == "temperature":
-            metadata, highs, lows = get_temperature_dict(self.lat, self.lon, revision, return_metadata=True)
-            data = {"highs": highs, "lows": lows}
-        self.revisions[revision] = {"metadata": metadata, "data": data}
-
-    def populate_revision(self, revision):
-        """ Fetch the revision only if we don't already have it
-        Args:
-            revision (str): the revision name on the ipfs gateway
-        """
-        if revision not in self.revisions:
-            self.load_revision(revision)
-
-    def get_revision(self, revision):
-        """ Return the saved data revision if it exists, otherwise load it and return it
-        Args:
-            revisiont (str): the dataset revision name as on the ipfs gateway
-        Returns:
-            dictionary (str):
-                {
-                    "metadata": the revision metadata
-                    "data": the revision data
-                }
-            for rainfall, "data" is a dict of datetime.date: rainfall values
-            for temperature, "data" is a dict { "highs": dict, "lows": dict} with highs and lows associating datetime.dates with temps
-        """
-        self.populate_revision(revision)
-        return self.revisions[revision]
-
-    def reload(self):
-        """ Get the lastest data from all saved revisions. Writes over saved revisions """
-        for revision in self.revisions:
-            self.load_revision(revision)
-
-    def load_all_revisions(self):
-        """ Get all revisions of the dataset. Writes over saved revisions"""
-        for revision in dweather_client.ipfs_datasets.datasets[self.dataset]:
-            self.load_revision(revision)
-
-    def build_multi_revision_dict(self):
-        """ Build out a dictionary using the most accurate data possible 
-        Returns:
-            dictionary
-        """
-        if self.weather_type == "rainfall":
-            all_data_dict = {}
-        elif self.weather_type == "temperature":
-            all_data_dict = {
-                "highs": {},
-                "lows": {}
-            }
-        for revision in dweather_client.ipfs_datasets.datasets[self.dataset]:
-            self.populate_revision(revision)
-            if self.weather_type == "rainfall":
-                new_data = {date: (self.revisions[revision]['data'][date], revision) for date in self.revisions[revision]["data"] if date not in all_data_dict}
-                all_data_dict.update(new_data)
-            elif self.weather_type == "temperature":
-                new_highs = {date: (self.revisions[revision]['data']['highs'][date], revision) for date in self.revisions[revision]["data"]["highs"] if date not in all_data_dict["highs"]}
-                new_lows = {date: (self.revisions[revision]['data']['lows'][date], revision) for date in self.revisions[revision]["data"]["lows"] if date not in all_data_dict["lows"]}
-                all_data_dict["highs"].update(new_highs)
-                all_data_dict["lows"].update(new_lows)
-
-        return all_data_dict
-
-class StationDataLoader:
-    _instances = {}
-
-    def __init__(self, station_id, station_dataset="ghcnd-imputed-daily"):
-        """
-        Args:
-            station_id (str): the station id
-        """
-        self.station_id = station_id
-        self.station_dataset = station_dataset
-        self.csv_text = ""
-
-    def __new__(cls, station_id, station_dataset="ghcnd-imputed-daily"):
-        """ Create new class only if one with args doesn't exist
-        Similar to singleton class, we only want one instance of DataLoader for each station
-        """
-        if (station_id, station_dataset) not in cls._instances:
-            cls._instances[(station_id, station_dataset)] = super(StationDataLoader, cls).__new__(cls)
-        return cls._instances[(station_id, station_dataset)]
-
-    def populate_csv(self):
-        """ Get the revision from ipfs and save it
-        Args:
-            revision (str): the named dataset revision on the ipfs gateway
-        """
-        if self.csv_text == '':
-            self.csv_text = get_station_csv(self.station_id, station_dataset=self.station_dataset)
-
-    def get_temperatures(self, use_fahrenheit=True):
-        """ Return the station data Tmins and Tmaxs 
-        Args:
-            use_fahrenheit: use degrees F if true, degrees C if false
-        returns:
-            tuple of dicts:
-                datetime.date: float the daily highs
-                datetime.date: float the daily lows
-        """
-        self.populate_csv()
-        tmaxs, tmins = parse_station_temps_as_dict(self.csv_text, use_fahrenheit)
-        return {"highs": tmaxs, "lows": tmins}
-
-    def get_snow_depth(self, use_inches=True):
-        """
-        Snow depth is the total amount of snow on the ground at the time
-        of observation. Compare to snowfall, which is the amount of new
-        snow that fell from the sky since the last observation,
-        regardless of the amount of snow on the ground.
-        """
-        self.populate_csv()
-        return parse_station_snow_depth_as_dict(self.csv_text, use_inches)
-        
-    def get_snowfall(self, use_inches=True):
-        """
-        Snow depth is the total amount of snow on the ground at the time
-        of observation. Compare to snowfall, which is the amount of new
-        snow that fell from the sky since the last observation,
-        regardless of the amount of snow on the ground.
-        """
-        self.populate_csv()     
-        return parse_station_snowfall_as_dict(self.csv_text, use_inches)
diff --git a/dweather_client/df_arithmetic.py b/dweather_client/df_arithmetic.py
deleted file mode 100644
index 1c084ce..0000000
--- a/dweather_client/df_arithmetic.py
+++ /dev/null
@@ -1,194 +0,0 @@
-"""
-Some functions for organizing dWeather query results into dataframe formats relevant to Arbol's use cases.
-Mainly related to summing and averaging over time.
-This module imports pandas and numpy, the arithmetic.py module should not
-"""
-import numpy as np
-import pandas as pd
-from dweather_client.http_client import get_rainfall_dict, get_rev_rainfall_dict, get_heads, get_metadata
-from dweather_client.utils import listify_period, get_n_closest_station_ids
-from dweather_client.ipfs_client import cat_rainfall_dict, cat_station_df, cat_station_csv, cat_metadata
-from dweather_client.df_utils import get_station_ids_with_icao
-import ipfshttpclient
-import datetime
-import logging
-import os
-from dweather_client.utils import snap_to_grid
-from dweather_client.http_client import get_metadata, get_rainfall_dict
-from shapely.geometry import Point, Polygon
-import ipfshttpclient
-from collections import Counter
-
-HISTORICAL_START_YEAR = 1981
-
-
-def get_polygon_df(shapefile_path, dataset, polygon_names, bounding_box, encoding='UTF-8'):
-    """
-    Get a dataframe of climate data for a given set of polygons.
-    """
-    import geopandas as gpd
-    polygons = gpd.read_file(shapefile_path)[['NAME_0', 'NAME_1', 'geometry']]
-    polygons.columns = ["country", "state", "geometry"]
-
-    metadata = get_metadata(get_heads()[dataset])
-
-    start = datetime.datetime.strptime(metadata['date range'][0], '%Y/%d/%m')
-    end = datetime.datetime.today()
-    date_range = pd.date_range(start=start, end=end)
-    df = pd.DataFrame([[0 for i in polygon_names] for i in date_range], columns=polygon_names)
-    df.insert(0, "Date", date_range, True)
-    df.index = df['Date']
-    df.drop(['Date'], axis=1, inplace=True)
-    df.index.name = None
-
-    for index, row in polygons.iterrows():
-        bbox_size = int( \
-            abs(bounding_box[1].y - bounding_box[0].y) *
-            abs(bounding_box[1].x - bounding_box[0].x) /
-            metadata['resolution'] /
-            metadata['resolution']
-        )
-        logging.info("Building %s (%i of %i polygons)" % (row['state'], index + 1, len(polygons.index)))
-        logging.info("Matching points within a bounding box of %i points." % bbox_size)
-        exec_counter = 0
-        poly_counter = Counter({})
-        lat_bounds = [lat for lat in sorted((bounding_box[0].y, bounding_box[1].y))] # gotta sort for np.arange
-        lon_bounds = [lon for lon in sorted((bounding_box[0].x, bounding_box[1].x))]
-        for latitr in np.arange(lat_bounds[0], lat_bounds[1], metadata['resolution']):
-            for lonitr in np.arange(lon_bounds[0], lon_bounds[1], metadata['resolution']):
-                if ((exec_counter % 10000) == 0):
-                    logging.info("Scanning point %i of %i" % (exec_counter, bbox_size))
-                if Point(lonitr, latitr).within(row['geometry']):
-                    slat, slon = snap_to_grid(latitr, lonitr, metadata)
-                    logging.info("Found match at (%s, %s), point %i of %i" % ( \
-                        "{:.3f}".format(slat),
-                        "{:.3f}".format(slon),
-                        exec_counter,
-                        bbox_size
-                    ))
-                    try:
-                        if 'cpc' in dataset:
-                            slat, slon = conventional_lat_lon_to_cpc(slat, slon)
-                        rain_counter = get_rainfall_dict(slat, slon, dataset, get_counter=True)
-                        poly_counter = poly_counter + rain_counter
-                    except:
-                        logging.warning("Could not retrieve data for (%s, %s)" % ("{:.3f}".format(slat), "{:.3f}".format(slon)))
-                        exec_counter = exec_counter + 1
-                        continue
-                exec_counter = exec_counter + 1
-        for day in poly_counter:
-            df.at[day.strftime('%Y-%m-%d'), row['state']] += poly_counter[day]
-    return df 
-
-def cat_station_df_list(station_ids, station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
-    batch_hash = force_hash
-    if (force_hash is None):
-        batch_hash = get_heads()[station_dataset]
-    metadata = cat_metadata(batch_hash, pin=pin)
-    station_content = []
-    with ipfshttpclient.connect() as client:
-        for station_id in station_ids:
-            logging.info("(%i of %i): Loading station %s from %s into DataFrame%s" % ( \
-                station_ids.index(station_id) + 1,
-                len(station_ids),
-                station_id, 
-                "dWeather head" if force_hash is None else "forced hash",
-                " and pinning to ipfs datastore" if pin else ""
-            ))
-            try:
-                station_content.append(cat_station_df( \
-                    station_id,
-                    station_dataset=station_dataset,
-                    client=client,
-                    pin=pin,
-                    force_hash=batch_hash
-            ))
-            except ipfshttpclient.exceptions.ErrorResponse:
-                logging.warning("Station %s not found" % station_id)
-                
-    return station_content 
-
-def cat_icao_stations(station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
-    """
-    For every station that has an icao code, load it into a dataframe and
-    return them all as a list.
-    """
-    station_ids = get_station_ids_with_icao()
-    return cat_station_df_list(station_ids, station_dataset=station_dataset, pin=pin, force_hash=force_hash)
-
-def cat_n_closest_station_dfs(lat, lon, n, station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
-    """
-    Load the closest n stations to a given point into a list of dataframes.
-    """
-    if (force_hash is None):
-        metadata = cat_metadata(get_heads()[station_dataset])
-    else:
-        metadata = cat_metadata(force_hash)
-    station_ids = get_n_closest_station_ids(lat, lon, metadata, n)
-    return cat_station_df_list(station_ids, station_dataset=station_dataset, pin=pin, force_hash=force_hash)
-
-def sum_period_df(df, ps, pe, yrs, peril):
-    """ 
-    Get the sums of a peril by a defined period.
-    
-    return:
-        pd.DataFrame indexed by year
-        
-    args:
-        df = weather data with a daily index
-        ps = contract start period, datetime.date object
-        pe = contract end period, datetime.date object
-        yrs = years lookback
-        peril = the name of column you wish to sum
-        
-    """
-    newdf = df
-    years = np.array([])
-    sums = np.array([])
-
-    for year in range(ps.year-yrs, ps.year):
-        if ps.year < pe.year: # if period crosses jan 1 into a new year
-            left = str(year)+'-'+str(ps.month)+'-'+str(ps.day)
-            right = str(year+1)+'-'+str(pe.month)+'-'+str(pe.day)
-        else:
-            left = str(year)+'-'+str(ps.month)+'-'+str(ps.day)
-            right = str(year)+'-'+str(pe.month)+'-'+str(pe.day)
-
-        sumChunk = newdf[left:right]
-        years = np.append(years, int(year))
-        sums = np.append(sums, sumChunk[peril].sum())
-    return pd.DataFrame({'year': years, 'value': sums}).set_index("year")
-
-
-def avg_period_df(df, ps, pe, yrs, peril):
-    """ 
-    Gets the avg of a peril by a defined period
-    
-    return:
-        pd.DataFrame indexed by year
-        
-    args:
-        df = weather data with a daily index
-        ps = contract start period, datetime.date object
-        pe = contract end period, datetime.date object
-        yrs = years lookback
-        peril = the name of column you wish to avg
-        
-    """
-
-    newdf = df
-    years = np.array([])
-    avgs = np.array([])
-
-    for year in range(ps.year-yrs, ps.year):
-        if ps.year < pe.year: # if period crosses jan 1 into a new year
-            left = str(year)+'-'+str(ps.month)+'-'+str(ps.day)
-            right = str(year+1)+'-'+str(pe.month)+'-'+str(pe.day)
-        else:
-            left = str(year)+'-'+str(ps.month)+'-'+str(ps.day)
-            right = str(year)+'-'+str(pe.month)+'-'+str(pe.day)
-
-        sumChunk = newdf[left:right]
-        years = np.append(years,int(year))
-        avgs = np.append(avgs, sumChunk[peril].mean())
-    return pd.DataFrame({'year': years, 'value': avgs}).set_index('year')
diff --git a/dweather_client/df_loader.py b/dweather_client/df_loader.py
index 82def67..3bbaf5f 100644
--- a/dweather_client/df_loader.py
+++ b/dweather_client/df_loader.py
@@ -1,97 +1,7 @@
-"""
-Basic functions for getting data from a dWeather gateway via https if you prefer to work
-in pandas dataframes rather than Python's built in types. A wrapper for http_client.
-"""
-from dweather_client.http_client import get_rainfall_dict, get_temperature_dict,\
-    RTMAClient, get_station_csv, get_simulated_hurricane_files, get_hurricane_dict, get_ibracs_hurricane_file, get_era5_dict
-from dweather_client.ipfs_client import cat_station_csv
-from dweather_client.df_utils import get_station_ids_with_icao, nearby_storms, boxed_storms
 import pandas as pd
-import numpy as np
-import io
-import ipfshttpclient
+from dweather_client.http_queries import get_simulated_hurricane_files, get_hurricane_dict, get_ibracs_hurricane_file
+from dweather_client.df_utils import nearby_storms, boxed_storms
 
-class RTMADFClient(RTMAClient):
-    def get_best_rtma_df(self, lat, lon):
-        """
-        RTMA precipitation.
-
-        Get a dataframe of for the closest valid rtma grid pair for a
-        given lat lon.
-
-        Returns a dataframe indexed on hourly datetime objects.
-        """
-        snapped_lat_lon, rtma_dict = self.get_best_rtma_dict(lat, lon)
-        rtma_dict = {"DATE": [k for k in rtma_dict.keys()], "PRCP": [k for k in rtma_dict.values()]}
-        rtma_df = pd.DataFrame.from_dict(rtma_dict)
-        rtma_df.DATE = pd.to_datetime(rtma_df.DATE)
-        return snapped_lat_lon, rtma_df.set_index(['DATE'])
-
-def get_rainfall_df(lat, lon, dataset):
-    """
-    Get full daily rainfall time series from cpc, prism, or chirps in mm.
-    
-    return:
-        pd.DataFrame loaded with all daily rainfall
-    
-    args:
-        lat: integer rounded to 3 decimals
-        lon: integer rounded to 3 decimals
-        dataset = SUPPORTED_DATASETS[n] where n is the index of the dataset you want to use
-    """
-    
-    rainfall_dict = get_rainfall_dict(lat, lon, dataset)
-    rainfall_dict = {"DATE": [k for k in rainfall_dict.keys()], "PRCP": [k for k in rainfall_dict.values()]}
-    
-    rainfall_df = pd.DataFrame.from_dict(rainfall_dict)
-    rainfall_df.DATE = pd.to_datetime(rainfall_df.DATE)
-    
-    return rainfall_df.set_index(['DATE'])
-
-def get_era5_wind_speed_df(lat, lon):
-    """
-    Get era5 time series df containing wind-u, wind-v, and total windspeed calculated
-    with the pythagorean theorem. 
-
-    return:
-        pd.DataFrame with pd.datetime index in hours
-    
-    args:
-        lat: float latitude that is over land (throws exception if invalid for era5)
-        lon: float longitude that is over land (throws exception if invalid for era5)
-    """
-    snapped_lat_lon, df_u = get_era5_df(lat, lon, 'era5_land_wind_u-hourly')
-    df_v = get_era5_df(lat, lon, 'era5_land_wind_v-hourly')[1]
-
-    res = pd.DataFrame()
-    res['wind_u'] = df_u.VALUE
-    res['wind_v'] = df_v.VALUE
-
-    # index the data to the smaller of the two datasets in the event one has updated and the other has not
-    res_index = df_v.index if len(df_v) < len(df_u) else df_u.index
-    res.set_index(res_index)
-
-    res['wind_speed'] = np.hypot(res.wind_u, res.wind_v)
-
-    return snapped_lat_lon, res
-
-def get_era5_df(lat, lon, dataset):
-    """
-    Get era5 time series df
-
-    return:
-        pd.DataFrame with pd.datetime index in hours
-    
-    args:
-        lat: float latitude that is over land (throws exception if invalid for era5)
-        lon: float longitude that is over land (throws exception if invalid for era5)
-        dataset: str currently only 'era5_land_wind_u-hourly'. More to come
-    """
-    snapped_lat_lon, era5_dict = get_era5_dict(lat, lon, dataset)
-    era5_dict = {"DATE": [k for k in era5_dict.keys()], "VALUE": [v for v in era5_dict.values()]}
-    era5_df = pd.DataFrame.from_dict(era5_dict)
-    era5_df.DATE = pd.to_datetime(era5_df.DATE)
-    return snapped_lat_lon, era5_df.set_index(['DATE'])
 
 def get_simulated_hurricane_df(basin, **kwargs):
     """
@@ -113,7 +23,7 @@ def get_simulated_hurricane_df(basin, **kwargs):
         df = nearby_storms(df, kwargs['lat'], kwargs['lon'], kwargs['radius'])
     elif {'min_lat', 'min_lon', 'max_lat', 'max_lon'}.issubset(kwargs.keys()):
         df = boxed_storms(df, kwargs['min_lat'], kwargs['min_lon'], kwargs['max_lat'], kwargs["max_lon"])
-    
+
     return df
 
 def get_atcf_hurricane_df(basin, **kwargs):
@@ -175,52 +85,8 @@ def get_historical_hurricane_df(basin, **kwargs):
         df = nearby_storms(df, kwargs['lat'], kwargs['lon'], kwargs['radius'])
     elif {'min_lat', 'min_lon', 'max_lat', 'max_lon'}.issubset(kwargs.keys()):
         df = boxed_storms(df, kwargs['min_lat'], kwargs['min_lon'], kwargs['max_lat'], kwargs["max_lon"])
-    
+
     df["HOUR"] = pd.to_datetime(df["ISO_TIME"])
     del df["ISO_TIME"]
 
     return df
-
-def get_temperature_df(lat, lon, dataset_revision):
-    """
-    Get full temperature data from one of the temperature datasets
-    Args:
-        lat: float to 3 decimals
-        lon: float to 3 decimals
-        dataset_revision: the name of the dataset as listed on the ipfs gateway
-    Returns:
-        a pandas DataFrame with cols DATE, HIGH and LOW
-    """
-    highs, lows = get_temperature_dict(lat, lon, dataset_revision)
-    intermediate_dict = {
-        "DATE": [date for date in highs],
-        "HIGH": [highs[date] for date in highs],
-        "LOW": [lows[date] for date in lows]
-    }
-    temperature_df = pd.DataFrame.from_dict(intermediate_dict)
-    temperature_df.DATE = pd.to_datetime(temperature_df.DATE)
-
-    return  temperature_df.set_index(["DATE"])
-
-
-def get_station_df(station_id, station_dataset="ghcnd-imputed-daily"):
-    """ Get a given station's raw data as a pandas dataframe. """
-    df = pd.read_csv(io.StringIO(get_station_csv(station_id, station_dataset=station_dataset)))
-    return df.set_index(pd.DatetimeIndex(df['DATE']))
-
-
-def get_station_rainfall_df(station_id, station_dataset="ghcnd"):
-    """ Get full daily rainfall time series from GHCN Station Data. """
-    # GHCND imputed does not have rainfall, only temperatures, so change the default
-    return get_station_df(station_id, station_dataset=station_dataset)[['PRCP', 'NAME']]
-    
-    
-def get_station_temperature_df(station_id, station_dataset="ghcnd-imputed-daily"):
-    """ Get full daily min, max temp time series from GHCN Station Data. """
-    return get_station_df(station_id, station_dataset=station_dataset)[['TMIN', 'TMAX']]#, 'NAME']]
-
-
-def get_station_snow_df(station_id, station_dataset="ghcnd"):
-    """ Get full daily snowfall time series from GHCN Station Data in mm. """
-    return get_station_df(station_id, station_dataset=station_dataset)[['SNOW', 'NAME']]
-
diff --git a/dweather_client/df_utils.py b/dweather_client/df_utils.py
index b5c55c1..3147be9 100644
--- a/dweather_client/df_utils.py
+++ b/dweather_client/df_utils.py
@@ -1,111 +1,4 @@
-"""
-Helpful auxilliary functions that don't directly interact with IPFS.
-Basically, it should go here if it doesn't need to import from df_loader or http_client,
-this module uses pandas
-"""
-import math
-import pandas as pd
 import numpy as np
-import datetime
-import os
-
-PARENT_DIR = os.path.dirname(os.path.abspath(__file__))
-CPC_LOOKUP_PATH = PARENT_DIR + '/etc/cpc-grid-ids.csv'
-ICAO_LOOKUP_PATH = PARENT_DIR + '/etc/airport-codes.csv'
-
-def dataframeify(dict_or_df):
-    """
-    Convert a dict to a dataframe.
-    If "dict" is already a dataframe, just return it.
-    """
-    if isinstance(dict_or_df, pd.DataFrame):
-        return dict_or_df
-    else:
-        return pd.DataFrame.from_dict(dict_or_df, orient="index")
-
-
-def cpc_grid_to_lat_lon(grid):
-    """ 
-    Convert a cpc grid id to conventional lat lon via a lookup table.
-    return:
-        latitude, longitude
-        
-    args:
-        grid = "1100" example
-    
-    """
-    cpc_grids =  pd.read_csv(os.path.join(CPC_LOOKUP_PATH)).set_index('Grid ID') #dataframe of cpc grid to lat/lon lookup table
-    myGrid = cpc_grids.iloc[grid]
-    coords = [myGrid["Latitude"], myGrid["Longitude"]]
-    coords = [coord+360 if coord < 0 else coord for coord in coords] #converts negative coordinates to positive values
-    
-    return coords[0], coords[1]
-
-
-def get_station_ids_with_icao():
-    """
-    Get a list of station id that are associated with stations that have an icao code.
-    """
-    icao_lookup = pd.read_csv(os.path.join(ICAO_LOOKUP_PATH)).set_index('ICAO')
-    ids = []
-    for index, row in icao_lookup.iterrows():
-        if (row["GHCN"] not in ids):
-            ids.append(row["GHCN"])
-    return ids
-
-def icao_to_ghcn(icao):
-    """ 
-    Convert an icao airport code to ghcn.
-    return:
-        latitude, longitude
-    args:
-        icao = "xxxx" for example try "KLGA"
-    """
-    icao_codes = pd.read_csv(os.path.join(ICAO_LOOKUP_PATH)).set_index('ICAO') #get lookup table
-    return icao_codes.loc[icao]["GHCN"]
-
-
-def period_slice_df(df, ps, pe, yrs):
-    """ 
-    Slice a dataframe by period where n is based on years lookback 
-    
-    return:
-        pd.DataFrame()
-    
-    args:
-        df = any pd.DataFrame() with a datetime.date() index
-        ps = datetime.date object (contract period start)
-        ps = datetime.date object (contract period end)
-        yrs = 30 (years lookback)
-        
-    limitations:
-        max period is 364 days
-        
-    """
-
-    mydf = pd.DataFrame()
-    
-    leftDay = ~((df.index.day < ps.day) & (df.index.month == ps.month))
-    rightDay = ~((df.index.day > pe.day) & (df.index.month == pe.month))
-    #first year just get end
-    if ps.year < pe.year:
-        firstYear = ps.year - yrs
-        yr = df.index.year == firstYear
-        mon = df.index.month >= ps.month
-        #a boolean mask to cut the left and right half month days if need be
-        mydf = mydf.append(df[(yr)&(mon)&(leftDay)])
-        #everything in betweeng
-        for yrss in range(ps.year - (yrs - 1), pe.year - 1):
-            ##boolean mask for now, better method needed##
-            yearChunk = (df.index.year == yrss)&((df.index.month<=pe.month)|(df.index.month>=ps.month))
-            yearChunk = (yearChunk)&leftDay&rightDay
-            mydf = mydf.append(df[yearChunk])
-        #build last year
-        mydf = mydf.append(df[rightDay&(df.index.month<=pe.month)&(df.index.year==pe.year-1)])
-    else:
-        mydf = df[leftDay&rightDay&(df.index.month>=ps.month)&(df.index.month<=pe.month)&(df.index.year>=ps.year-yrs)]
-            
-    return mydf
 
 def boxed_storms(df, min_lat, min_lon, max_lat, max_lon):
     return df[(df['lat'] >= min_lat) & (df['lon'] >= min_lon) & (df['lat'] <= max_lat) & (df['lon'] <= max_lon)]
@@ -114,7 +7,6 @@ def nearby_storms(df, c_lat, c_lon, radius):
     """
     return:
         DataFrame with all time series points in `df` within `radius` in km of the point `(c_lat, c_lon)`
-
     args:
         c_lat (float): latitude coordinate of bounding circle
         c_lon (float): longitude coordinate of bounding circle
@@ -127,7 +19,6 @@ def haversine_vectorize(lon1, lat1, lon2, lat2):
     Vectorized version of haversine great circle calculation. 
     return: 
         distance in km between `(lat1, lon1)` and `(lat2, lon2)`
-
     args:
         lon1 (float) first longitude coord
         lat1 (float) first latitude coord
diff --git a/dweather_client/grid_utils.py b/dweather_client/grid_utils.py
new file mode 100644
index 0000000..5287142
--- /dev/null
+++ b/dweather_client/grid_utils.py
@@ -0,0 +1,219 @@
+"""
+Various utilities associated with the coordinate grid system.
+"""
+from dweather_client.http_queries import get_heads, get_metadata
+try:
+    import geopandas as gpd
+except:
+    gpd = None
+from geopy.distance import geodesic
+from heapq import heappush, heappushpop
+import pandas as pd
+import numpy as np
+import datetime, math
+
+
+def get_polygon_df(shapefile_path, dataset, polygon_names, bounding_box, encoding='UTF-8'):
+    """
+    Get a dataframe of climate data for a given set of polygons.
+    """
+    polygons = gpd.read_file(shapefile_path)[['NAME_0', 'NAME_1', 'geometry']]
+    polygons.columns = ["country", "state", "geometry"]
+
+    metadata = get_metadata(get_heads()[dataset])
+
+    start = datetime.datetime.strptime(metadata['date range'][0], '%Y/%d/%m')
+    end = datetime.datetime.today()
+    date_range = pd.date_range(start=start, end=end)
+    df = pd.DataFrame([[0 for i in polygon_names] for i in date_range], columns=polygon_names)
+    df.insert(0, "Date", date_range, True)
+    df.index = df['Date']
+    df.drop(['Date'], axis=1, inplace=True)
+    df.index.name = None
+
+    for index, row in polygons.iterrows():
+        bbox_size = int( \
+            abs(bounding_box[1].y - bounding_box[0].y) *
+            abs(bounding_box[1].x - bounding_box[0].x) /
+            metadata['resolution'] /
+            metadata['resolution']
+        )
+        logging.info("Building %s (%i of %i polygons)" % (row['state'], index + 1, len(polygons.index)))
+        logging.info("Matching points within a bounding box of %i points." % bbox_size)
+        exec_counter = 0
+        poly_counter = Counter({})
+        lat_bounds = [lat for lat in sorted((bounding_box[0].y, bounding_box[1].y))] # gotta sort for np.arange
+        lon_bounds = [lon for lon in sorted((bounding_box[0].x, bounding_box[1].x))]
+        for latitr in np.arange(lat_bounds[0], lat_bounds[1], metadata['resolution']):
+            for lonitr in np.arange(lon_bounds[0], lon_bounds[1], metadata['resolution']):
+                if ((exec_counter % 10000) == 0):
+                    logging.info("Scanning point %i of %i" % (exec_counter, bbox_size))
+                if Point(lonitr, latitr).within(row['geometry']):
+                    slat, slon = snap_to_grid(latitr, lonitr, metadata)
+                    logging.info("Found match at (%s, %s), point %i of %i" % ( \
+                        "{:.3f}".format(slat),
+                        "{:.3f}".format(slon),
+                        exec_counter,
+                        bbox_size
+                    ))
+                    try:
+                        if 'cpc' in dataset:
+                            slat, slon = conventional_lat_lon_to_cpc(slat, slon)
+                        rain_counter = get_rainfall_dict(slat, slon, dataset, get_counter=True)
+                        poly_counter = poly_counter + rain_counter
+                    except:
+                        logging.warning("Could not retrieve data for (%s, %s)" % ("{:.3f}".format(slat), "{:.3f}".format(slon)))
+                        exec_counter = exec_counter + 1
+                        continue
+                exec_counter = exec_counter + 1
+        for day in poly_counter:
+            df.at[day.strftime('%Y-%m-%d'), row['state']] += poly_counter[day]
+    return df 
+
+def haversine_vectorize(lon1, lat1, lon2, lat2):
+    """
+    Vectorized version of haversine great circle calculation. 
+    return: 
+        distance in km between `(lat1, lon1)` and `(lat2, lon2)`
+
+    args:
+        lon1 (float) first longitude coord
+        lat1 (float) first latitude coord
+        lon2 (float) second longitude coord
+        lat2 (float) second latitude coord
+    """
+    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
+    newlon = lon2 - lon1
+    newlat = lat2 - lat1
+    haver_formula = np.sin(newlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(newlon/2.0)**2
+    dist = 2 * np.arcsin(np.sqrt(haver_formula))
+    km = 6367 * dist
+    return km
+
+
+def nearby_storms(df, c_lat, c_lon, radius): 
+    """
+    return:
+        DataFrame with all time series points in `df` within `radius` in km of the point `(c_lat, c_lon)`
+
+    args:
+        c_lat (float): latitude coordinate of bounding circle
+        c_lon (float): longitude coordinate of bounding circle
+    """ 
+    dist = haversine_vectorize(df['lon'], df['lat'], c_lon, c_lat)
+    return df[dist < radius]
+
+def get_n_closest_station_ids(lat, lon, n, metadata=None):
+    """
+    Get the station ids for the <n> closest stations to a given lat lon. 
+    Requires metadata of ghcnd to get station coordinates.
+    """
+    if metadata == None:
+        metadata = get_metadata(get_heads()['ghcnd'])
+    pq = []
+    for feature in metadata["stations"]["features"]:
+        s_lat = float(feature["geometry"]["coordinates"][0])
+        s_lon = float(feature["geometry"]["coordinates"][1])
+        distance = geodesic([lat, lon], [s_lat, s_lon]).miles
+        station_id = feature["properties"]["station id"]
+        if (len(pq) >= n):
+            heappushpop(pq, (1 / distance, station_id))
+        else:
+            heappush(pq, (1 / distance, station_id))
+    return [pair[1] for pair in pq]
+
+def build_rtma_lookup(grid_history):
+    """
+    turn the string representation of the grid history into a data structure.
+    {timestamp: (lat_list, lon_list), timestamp: (lat_list, lon_list)}
+    """
+    grid_history = grid_history.split('\n\n')
+    grid_dict = {grid_history[0]: [grid_history[1], grid_history[2]], grid_history[3]: [grid_history[4], grid_history[5]]}
+    for timestamp in grid_dict:
+        for dimension in (0, 1):
+            grid_dict[timestamp][dimension] = [y.strip('][').split(', ') for y in grid_dict[timestamp][dimension].split('\n')]
+    return grid_dict
+
+def build_rtma_reverse_lookup(grid_history):
+    """
+    Reverse index the rtma lookup data structure to enable querying by lat lon.
+    {timestamp: {'lat': {latitude: (x, y)}}, {'lon': {longitude: (x, y)}}}
+    Example query: 
+        rev_grid_dict['2011-01-01T00:00:00']['lat']['20.191999000000006']
+        rev_grid_dict['2011-01-01T00:00:00']['lon']['238.445999']
+    """
+    grid_dict = build_rtma_lookup(grid_history)
+    rev_grid_dict = {}
+    for timestamp in grid_dict:
+        rev_grid_dict[timestamp] = {'lat': {}, 'lon': {}}
+        # reindex lat
+        for y in range(0, len(grid_dict[timestamp][0])):
+            for x in range(0, len(grid_dict[timestamp][0][y])):
+                if grid_dict[timestamp][0][y][x] in rev_grid_dict[timestamp]['lat']:
+                    logging.error("Conflict in parsing rtma grid history.")
+                rev_grid_dict[timestamp]['lat'][grid_dict[timestamp][0][y][x]] = (x, y)
+        # reindex lon
+        for y in range(0, len(grid_dict[timestamp][1])):
+            for x in range(0, len(grid_dict[timestamp][1][y])):
+                if grid_dict[timestamp][1][y][x] in rev_grid_dict[timestamp]['lon']:
+                    logging.error('Conflict in Parsing rtma grid history.')
+                rev_grid_dict[timestamp]['lon'][grid_dict[timestamp][1][y][x]] = (x, y)
+    return rev_grid_dict
+
+def zero_three_sixty_to_negative_one_eighty_one_eighty(lat, lon):
+    return cpc_lat_lon_to_conventional(lat, lon)
+
+def negative_one_eighty_one_eighty_to_zero_three_sixty(lat, lon):
+    return conventional_lat_lon_to_cpc(lat, lon)
+
+def cpc_lat_lon_to_conventional(lat, lon):
+    """
+    Convert a pair of coordinates from the idiosyncratic CPC lat lon
+    format to the conventional lat lon format.
+    """
+    lat, lon = float(lat), float(lon)
+    if (lon >= 180):
+        return lat, lon - 360
+    else:
+        return lat, lon
+
+
+def conventional_lat_lon_to_cpc(lat, lon):
+    """
+    Convert a pair of coordinates from conventional (lat,lon)
+    to the idiosyncratic CPC (lat,lon) format.
+    """
+    lat, lon = float(lat), float(lon)
+    if (lon < 0):
+        return lat, lon + 360
+    else:
+        return lat, lon
+
+
+def snap_to_grid(lat, lon, metadata):
+    """ 
+    Find the nearest valid (lat,lon) for a given metadata file and arbitrary
+    "unsnapped" lat lon.
+
+    Use this when you want to query a gridded dataset for some arbitrary
+    point.
+
+    return: lat, lon
+    args:
+        lat = -90 < lat < 90, float
+        lon = -180 < lon < 180, float
+        metadata: a dWeather metadata file
+
+    """
+    resolution = metadata['resolution']
+    min_lat = metadata['latitude range'][0]
+    min_lon = metadata['longitude range'][0]
+    precision = metadata['filename decimal precision']
+
+    if 'source data url' in metadata and 'cpc' in metadata['source data url']:
+        min_lat, min_lon = cpc_lat_lon_to_conventional(min_lat, min_lon)
+
+    snap_lat = round(round((lat - min_lat)/resolution) * resolution + min_lat, precision)
+    snap_lon = round(round((lon - min_lon)/resolution) * resolution + min_lon, precision)
+
+    return snap_lat, snap_lon
diff --git a/dweather_client/http_client.py b/dweather_client/http_client.py
deleted file mode 100644
index 4e0f015..0000000
--- a/dweather_client/http_client.py
+++ /dev/null
@@ -1,597 +0,0 @@
-"""
-Basic functions for getting data from a dWeather gateway via https.
-"""
-import os, pickle, math, requests, datetime, io, gzip, json, logging, csv, tarfile
-from dweather_client.ipfs_errors import *
-from dweather_client.utils import listify_period, lat_lon_to_rtma_grid, find_closest_lat_lon, build_rtma_reverse_lookup, build_rtma_lookup, conventional_lat_lon_to_cpc, cpc_lat_lon_to_conventional, mms_to_inches, celcius_to_fahrenheit
-import dweather_client.ipfs_datasets
-from collections import Counter, deque
-
-MM_TO_INCHES = 0.0393701
-RAINFALL_PRECISION = 5
-GATEWAY_URL = 'https://gateway.arbolmarket.com'
-
-def get_heads(url=GATEWAY_URL):
-    """
-    Get heads.json for a given IPFS gateway.
-    Args:
-        url (str): base url of the IPFS gateway url
-    Returns (example heads.json):
-        {
-            'chirps_05-daily': 'Qm...',
-            'chirps_05-monthly': 'Qm...',
-            'chirps_25-daily': 'Qm...',
-            'chirps_25-monthly': 'Qm...',
-            'cpc_us-daily': 'Qm...',
-            'cpc_us-monthly': 'Qm...'
-        }
-    """
-    hashes_url = url + "/climate/hashes/heads.json"
-    r = requests.get(hashes_url)
-    r.raise_for_status()
-    return r.json()
-
-def get_metadata(hash_str, url=GATEWAY_URL):
-    """
-    Get the metadata file for a given hash.
-    Args:
-        url (str): the url of the IPFS server
-        hash_str (str): the hash of the ipfs dataset
-    Returns (example metadata.json):
-    
-        {
-            'date range': [
-                '1981/01/01',
-                '2019/07/31'
-            ],
-            'entry delimiter': ',',
-            'latitude range': [
-                -49.975, 49.975
-            ],
-            'longitude range': [
-                -179.975, 179.975]
-            ,
-            'name': 'CHIRPS .05 Daily Full Set Uncompressed',
-            'period': 'daily',
-            'precision': 0.01,
-            'resolution': 0.05,
-            'unit of measurement': 'mm',
-            'year delimiter': '\n'
-        }
-    """
-    metadata_url = "%s/ipfs/%s/metadata.json" % (url, hash_str)
-    r = requests.get(metadata_url)
-    r.raise_for_status()
-    return r.json()
-
-def traverse_ll(head):
-    release_itr = head
-    release_ll = deque()
-    while True:
-        release_ll.appendleft(release_itr)
-        prev_release = get_metadata(release_itr)['previous hash']
-        if prev_release != None:
-            release_itr = prev_release
-        else:
-            return release_ll
-
-def get_hurricane_release_dict(release_hash, url=GATEWAY_URL):
-    url = "%s/ipfs/%s/history.json.gz" % (url, release_hash)
-    resp = requests.get(url)
-    resp.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(resp.content)) as zip_data:
-        return json.loads(zip_data.read().decode("utf-8"))
-
-def get_hurricane_dict(head=get_heads()['atcf_btk-seasonal']):
-    """
-    Get a hurricane dictionary for the atcf_btk-seasonal dataset. 
-
-    To get a unique value to query the dict by storm, use BASIN + CY + the year
-    part of the HOUR value. BASIN is the ocean, CY is the storm index, and
-    the year is needed as well because the storm index resets every year.
-
-    Note that there will be multiple readings with the same HOUR value,
-    as readings are taken more than once per hour and then rounded to the nearest
-    hour before posting. 
-    """
-    heads = get_heads()
-    hurr_head = heads['atcf_btk-seasonal']
-    release_ll = traverse_ll(hurr_head)
-    hurr_dict = {}
-    for release_hash in release_ll:
-        release_content = get_hurricane_release_dict(release_hash)
-        try:
-            hurr_dict['features'] += release_content['features']
-        except KeyError:
-            hurr_dict.update(release_content)
-    return hurr_dict
-
-def get_simulated_hurricane_files(basin):
-    """
-    Gets the names of files containing STORM simulated TC data. Takes a basin ID, one of:
-    EP, NA, NI, SI, SP or WP
-    """
-    if basin not in {'EP', 'NA', 'NI', 'SI', 'SP', 'WP'}:
-        raise ValueError("Invalid basin ID")
-    heads = get_heads()
-    hurr_hash = heads['storm-simulated-hurricane']
-    metadata  = get_metadata(hurr_hash)
-    base_url = f"{GATEWAY_URL}/ipfs/{hurr_hash}/"
-    files = [base_url + f for f in metadata['files'] if basin in f]
-    return files
-
-def get_ibracs_hurricane_file():
-    heads = get_heads()
-    hurr_hash = heads['ibtracs-tropical-storm']
-    metadata = get_metadata(hurr_hash)
-    base_url = f"{GATEWAY_URL}/ipfs/{hurr_hash}/"
-    return base_url + metadata["files"][0]
-
-def get_station_csv(station_id, station_dataset="ghcnd-imputed-daily", url=GATEWAY_URL):
-    """
-    Retrieve the contents of a station data csv file.
-    Args:
-        station_id (str): the id of the weather station
-        station_dataset (str): which dataset to use, on of ["ghcnd", "ghcnd-imputed-daily"]
-    returns:
-        the contents of the station csv file as a string
-    """
-    all_hashes = get_heads()
-    dataset_hash = all_hashes[station_dataset]
-    dataset_url = "%s/ipfs/%s/%s.csv.gz" % (url, dataset_hash, str(station_id))
-    r = requests.get(dataset_url)
-    r.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as zip_data:
-        return zip_data.read().decode("utf-8")
-
-
-def parse_station_snow_depth_as_dict(csv_text, use_inches=True):
-    return parse_station_snow_as_dict( \
-        csv_text, 
-        snow_representation="SNWD",
-        use_inches=True
-    )
-
-def parse_station_snowfall_as_dict(csv_text, use_inches=True):
-    return parse_station_snow_as_dict( \
-        csv_text,
-        snow_representation="SNOW",
-        use_inches=True
-    )
-
-
-def parse_station_snow_as_dict(csv_text, snow_representation, use_inches=True):
-    reader = csv.reader(csv_text.split())
-    column_names = next(reader)
-    date_col = column_names.index('DATE')
-    snow_col = column_names.index(snow_representation)
-    snowfall = {}
-    for row in reader:
-        if row[snow_col] == '':
-            continue
-        row_snow = float(row[snow_col])/10.0
-        if use_inches:
-            row_snow = mms_to_inches(row_snow)
-        snowfall[datetime.datetime.strptime(row[date_col], "%Y-%m-%d").date()] = row_snow
-    return snowfall
-
-def parse_station_temps_as_dict(csv_text, use_fahrenheit=True):
-    """
-    Parse a station CSV file and get column values
-    Will automatically index by date
-    Args:
-        csv_text (str): the GHCND station csv text
-        use_fahrenheight (bool): if true use deg F, otherwise degrees C
-    Returns:
-        tuple:
-            dict of datetime.date: float temperature highs
-            dict of datetime.date: float temperature lows
-    """
-    reader = csv.reader(csv_text.split())
-    column_names = next(reader)
-    date_col = column_names.index('DATE')
-    tmax_col = column_names.index('TMAX')
-    tmin_col = column_names.index('TMIN')
-    tmins = {}
-    tmaxs = {}
-    for row in reader:
-        # data is in tenths of a degree C
-        if row[tmin_col] == '' or row[tmax_col] == '':
-            continue
-        tmax = float(row[tmax_col])/10.0
-        tmin = float(row[tmin_col])/10.0
-        if use_fahrenheit:
-            tmax = celcius_to_fahrenheit(tmax)
-            tmin = celcius_to_fahrenheit(tmin)
-        tmaxs[datetime.datetime.strptime(row[date_col], "%Y-%m-%d").date()] = tmax
-        tmins[datetime.datetime.strptime(row[date_col], "%Y-%m-%d").date()] = tmin
-
-    return tmins, tmaxs
-
-def get_station_by_wmo_id(wmo_id):
-    pass
-
-def get_station_by_airport_code(code):
-    pass
-
-def get_hash_cell(hash_str, coord_str, url=GATEWAY_URL):
-    dataset_url = '%s/ipfs/%s/%s' % (url, hash_str, coord_str)
-    r = requests.get(dataset_url)
-    r.raise_for_status()
-    return r.text
-
-def get_zipped_hash_cell(hash_str, coord_str, url=GATEWAY_URL):
-    """
-    Read a text file on the ipfs server compressed with gzip.
-    Args:
-        url (str): the url of the ipfs server
-        hash_str (str): the hash of the dataset
-        coord_str (str): the text file coordinate name e.g. 45.000_-96.000
-    Returns:
-        the contents of the file as a string
-    """
-    dataset_url = '%s/ipfs/%s/%s.gz' % (url, hash_str, coord_str)
-    r = requests.get(dataset_url)
-    r.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as zip_data:
-        return zip_data.read().decode("utf-8")
-
-class RTMAClient:
-    def __init__(self):
-        logging.info("Loading rtma valid lat lons")
-        with gzip.GzipFile(os.path.join(os.path.dirname(__file__), 'etc/rtma_lat_lons.p.gz')) as lat_lons:
-            self.valid_lat_lons = pickle.load(lat_lons)
-        self.rtma_head = get_heads()['rtma_pcp-hourly']
-        self.metadata = get_metadata(self.rtma_head)
-        self.rtma_start_date = datetime.datetime.strptime(self.metadata['date range'][0], "%Y-%m-%dT%H:%M:%S")
-        self.rtma_end_date = datetime.datetime.strptime(self.metadata['date range'][1], "%Y-%m-%dT%H:%M:%S")
-        logging.info("Downloading rtma grid lookup")
-        r = requests.get('%s/ipfs/%s/grid_history.txt.gz' % (GATEWAY_URL, self.rtma_head))
-        r.raise_for_status()
-        logging.info("Unzipping rtma grid lookup")
-        with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
-            grid_history = grid_history_file.read().decode('utf-8')
-        logging.info("Loading rtma grid lookup")
-        self.r_lookup = build_rtma_reverse_lookup(grid_history)
-        self.new_grid = list(self.r_lookup.keys())[-1]
-
-    def get_best_rtma_dict(self, lat, lon):
-        lat, lon = float(lat), float(lon)
-        lat, lon = conventional_lat_lon_to_cpc(lat, lon)
-        if ((lat < 20) or (53 < lat)):
-            raise InputOutOfRangeError('RTMA only covers latitudes 20 thru 53')
-        if ((lon < 228) or (300 < lon)):
-            raise InputOutOfRangeError('RTMA only covers longitudes -132 thru -60')
-        lat, lon = str(lat), str(lon)
-        logging.info('Finding closest lat lon')
-        logging.info('Number of buckets in lookup: %i' % len(self.valid_lat_lons))
-        closest_lat_lon = find_closest_lat_lon(self.valid_lat_lons[(lat[:2], lon[:3])], (lat, lon))
-        logging.info('Finding rtma xy associated with closest lat lon')
-        lat_xy = self.r_lookup[self.new_grid]['lat'][closest_lat_lon[0]]
-        lon_xy = self.r_lookup[self.new_grid]['lon'][closest_lat_lon[1]]
-        assert lat_xy == lon_xy
-        return cpc_lat_lon_to_conventional(closest_lat_lon[0], closest_lat_lon[1]), self.get_rtma_dict(lat_xy[0], lat_xy[1])
-
-    def get_rtma_dict(self, x, y):
-        r = requests.get('%s/ipfs/%s/%s_%s.gz' % (GATEWAY_URL, self.rtma_head, str(x).zfill(4), str(y).zfill(4)))
-        r.raise_for_status()
-        with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as cell_text_file:
-            cell_text = cell_text_file.read().decode('utf-8')
-        hour_itr = self.rtma_start_date
-        rtma_dict = {}
-        for year_data in cell_text.split('\n'):
-            for hour_data in year_data.split(','):
-                rtma_dict[hour_itr] = hour_data
-                hour_itr = hour_itr + datetime.timedelta(hours=1)
-        assert hour_itr == self.rtma_end_date
-        return rtma_dict
-
-def get_full_rtma_history(lat, lon):
-    """
-    Calls endpoint that iterates through all updates to the RTMA dataset and returns a dictionary
-    containing the full time series of data.
-    Args:
-        lat (float): latitude coordinate of RTMA data
-        lon (float): longitude coordinate of RTMA data
-    Returns:
-        tuple containing (<ret_lat>, <ret_lon>, <data>)
-        where ret_lat and ret_lon are floats representing the coordinates of the data after the
-        argument coordinates are snapped to the RTMA grid, and <data> is a time series dict with 
-        datetime keys
-    """
-    if ((lat < 20) or (53 < lat)):
-        raise InputOutOfRangeError('RTMA only covers latitudes 20 thru 53')
-    if ((lon < -132) or (-60 < lon)):
-        raise InputOutOfRangeError('RTMA only covers longitudes -132 thru -60')
-    base_url = "https://parser.arbolmarket.com/linked-list/rtma"
-    r = requests.get(f"{base_url}/{lat}_{lon}")
-    resp = r.json()
-    data_dict = {}
-    for k, v in resp["data"].items():
-        data_dict[datetime.datetime.fromisoformat(k)] = v
-    return ((resp["lat"], resp["lon"]), data_dict)
-
-def get_dataset_cell(lat, lon, dataset_revision):
-    """ 
-    Retrieve the text of a grid cell data file for a given lat lon and dataset.
-    Args:
-        lat (float): the latitude of the grid cell, to 3 decimals
-        lon (float): the longitude of the grid cell, to 3 decimals
-    Returns:
-        A tuple (json, str) of the dataset metadata file and the grid cell data text
-    Raises: 
-        DatasetError: If no matching dataset found on server
-        InputOutOfRangeError: If the lat/lon is outside the dataset range in metadata
-        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
-    """
-    all_hashes = get_heads()
-    if dataset_revision in all_hashes:
-        dataset_hash = all_hashes[dataset_revision]
-    else:
-        raise DatasetError('{} not found on server'.format(dataset_revision))
-
-    metadata = get_metadata(dataset_hash)
-    min_lat, max_lat = sorted(metadata["latitude range"])
-    min_lon, max_lon = sorted(metadata["longitude range"])
-    if lat < min_lat or lat > max_lat:
-        raise InputOutOfRangeError("Latitude {} out of dataset revision range [{:.3f}, {:.3f}] for {}".format(lat, min_lat, max_lat, dataset_revision))
-    if  lon < min_lon or lon > max_lon:
-        raise InputOutOfRangeError("Longitude {} out of dataset revision range [{:.3f}, {:.3f}] for {}".format(lon, min_lon, max_lon, dataset_revision))
-    coord_str = "{:.3f}_{:.3f}".format(lat,lon)
-    try:
-        if "compression" in metadata and metadata["compression"] == "gzip":
-            text_data = get_zipped_hash_cell(dataset_hash, coord_str)
-        else:
-            text_data = get_hash_cell(dataset_hash, coord_str)
-        return metadata, text_data
-    except requests.exceptions.RequestException as e:
-        raise CoordinateNotFoundError('Coordinate ({}, {}) not found  on ipfs in dataset revision {}'.format(lat, lon, dataset_revision))
-
-def get_rainfall_dict(lat, lon, dataset_revision, return_metadata=False, get_counter=False):
-    """ 
-    Build a dict of rainfall data for a given grid cell.
-    Args:
-        lat (float): the latitude of the grid cell, to 3 decimals
-        lon (float): the longitude of the grid cell, to 3 decimals
-    Returns:
-        a dict ({datetime.date: float}) of datetime dates and the corresponding rainfall in mm for that date
-    Raises:
-        DatasetError: If no matching dataset found on server
-        InputOutOfRangeError: If the lat/lon is outside the dataset range in metadata
-        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
-        DataMalformedError: If the grid cell file can't be parsed as rainfall data
-    """
-    metadata, rainfall_text = get_dataset_cell(lat, lon, dataset_revision)
-    dataset_start_date = datetime.datetime.strptime(metadata['date range'][0], "%Y/%m/%d").date()
-    dataset_end_date = datetime.datetime.strptime(metadata['date range'][1], "%Y/%m/%d").date()
-    timedelta = dataset_end_date - dataset_start_date
-    days_in_record = timedelta.days + 1 # we have both the start and end date in the dataset so its the difference + 1
-    day_strs = rainfall_text.replace(',', ' ').split()
-    if (len(day_strs) != days_in_record):
-        raise DataMalformedError ("Number of days in data file does not match the provided metadata")
-    rainfall_dict = Counter({}) if get_counter else {}
-    for i in range(days_in_record):
-        if day_strs[i] == metadata["missing value"]:
-            rainfall_dict[dataset_start_date + datetime.timedelta(days=i)] = 0 if get_counter else None
-        else:
-            rainfall_dict[dataset_start_date + datetime.timedelta(days=i)] = float(day_strs[i])
-    if return_metadata:
-        return metadata, rainfall_dict
-    else:
-        return rainfall_dict
-
-
-def get_prismc_dict(lat, lon, dataset):
-    """
-    Builds a dict of latest PRISM data by using datasets combining all PRISM revisions
-    Notes:
-        Preferentially uses newer data. For example, if PRISM releases provisional data 
-        for the date 2020-04-05 on 2020-05-01, when the ingestion script is run later that day, 
-        that data will be placed in the last node in the linked list, and when `get_prismc_dict` is run,
-        that data will appear in the return value.
-
-        Then, when stable data for 2020-04-05 is released on 2020-10-01, when the ingestion script is run again,
-        that data will be placed in the last node. Finally, because that data is later in the chain, when `get_prismc_dict` is run,
-        the 2020-04-05 key will point to the data released on 2020-10-01, and the outdated data released on 2020-05-01, though still in
-        the chain, will be ignored.
-    Args:
-        lat (float): the latitude of the grid cell, to 3 decimals
-        lon (float): the longitude of the grid cell, to 3 decimals
-        dataset (str): one of 'precip', 'tmax' or 'tmin'
-    Returns:
-        a dict ({datetime.date: float}) of datetime dates and the corresponding weather values.
-        Units are mm for precip or degrees F for tmax and tmin
-    """
-    if dataset not in {"precip", "tmax", "tmin"}:
-        raise ValueError("Dataset must be 'precip', 'tmax' or 'tmin'")
-    str_lat, str_lon = "{:.3f}".format(lat), "{:.3f}".format(lon)
-    prismc_head = get_heads()[f"prismc-{dataset}-daily"]
-    date_dict = {}
-    hashes = traverse_ll(prismc_head)
-    for h in list(hashes)[::-1]:
-        tar_url = f"{GATEWAY_URL}/ipfs/{h}/{str_lat}.tar"
-        resp = requests.get(tar_url)
-        resp.raise_for_status()
-        with tarfile.open(fileobj=io.BytesIO(resp.content)) as tar:
-            with tar.extractfile(f"{str_lat}_{str_lon}.gz") as f:
-                with gzip.open(f) as gz:
-                    for i, line in enumerate(gz):
-                        day_of_year = datetime.date(1981 + i, 1, 1)
-                        data_list = line.decode('utf-8').strip().split(',')
-                        for point in data_list:
-                            if (day_of_year not in date_dict) and point:
-                                date_dict[day_of_year] = float(point)
-                                day_of_year += datetime.timedelta(days=1)
-    return date_dict
-
-def get_rev_rainfall_dict(lat, lon, dataset, desired_end_date, latest_rev):
-    """
-    Build a dictionary of rainfall data. Include as much of the most accurate, final data as possible. Start by buidling from the most accurate data,
-    then keep appending data from more recent/less accurate versions of the dataset until we run out or reach the end date.
-
-    This will not throw an error if there are no revisions with data available, it will simply return what is available.
-    Args:
-        lat (float): the grid cell latitude
-        lon (float): the grid cell longitude
-        dataset (str): the name of the dataset, e.g., "chirps_05-daily" on hashes.json
-        desired_end_date (datetime.date): the last day of data needed.
-        latest_rev (str): the least accurate revision of the dataset that is considered final
-    Returns:
-        tuple:
-            a dict ({datetime.date: float}) of datetime dates and the corresponding rainfall in mm for that date
-            bool is_final: if all data up to desired end date is final, this will be true
-    """
-    all_rainfall = {}
-    is_final = True
-
-    # Build the rainfall from the most accurate revision of the dataset to the least
-    for dataset_revision in dweather_client.ipfs_datasets.datasets[dataset]:
-        additional_rainfall = get_rainfall_dict(lat, lon, dataset_revision)
-        all_dates = list(all_rainfall) + list(additional_rainfall)
-        # This method of dict comprehension preserves the order of the dict
-        all_rainfall = {date: all_rainfall[date] if date in all_rainfall else additional_rainfall[date] for date in all_dates}
-        # stop when we have the desired end date in the dataset
-        if desired_end_date in all_rainfall:
-            return all_rainfall, is_final
-        # data is no longer final after we pass the specified version
-        if dataset_revision == latest_rev:
-            is_final = False
-
-    # If we don't reach the desired dataset, return all data.
-    return all_rainfall, is_final
-
-def get_temperature_dict(lat, lon, dataset_revision, return_metadata=False):
-    """
-    Build a dict of temperature data for a given grid cell.
-    Args:
-        lat (float): the latitude of the grid cell, to 3 decimals
-        lon (float): the longitude of the grid cell, to 3 decimals
-    Returns:
-        tuple (highs, lows) of dicts
-        highs: dict ({datetime.date: float}) of datetime dates and the corresponding high temperature in degress F
-        lows: dict ({datetime.date: float}) of datetime dates and the corresponding low temperature in degress F
-    Raises:
-        DatasetError: If no matching dataset_revision found on server
-        InputOutOfRangeError: If the lat/lon is outside the dataset_revision range in metadata
-        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
-        DataMalformedError: If the grid cell file can't be parsed as temperature data
-    """
-    metadata, temp_text = get_dataset_cell(lat, lon, dataset_revision)
-    dataset_start_date = datetime.datetime.strptime(metadata['date range'][0], "%Y/%m/%d").date()
-    dataset_end_date = datetime.datetime.strptime(metadata['date range'][1], "%Y/%m/%d").date()
-    timedelta = dataset_end_date - dataset_start_date
-    days_in_record = timedelta.days + 1 # we have both the start and end date in the dataset_revision so its the difference + 1
-    day_strs = temp_text.replace(',', ' ').split()
-    if (len(day_strs) != days_in_record):
-        raise DataMalformedError ("Number of days in data file does not match the provided metadata")
-    highs = {}
-    lows = {}
-    for i in range(days_in_record):
-        low, high = map(float, day_strs[i].split('/'))
-        date_iter = dataset_start_date + datetime.timedelta(days=i)
-        highs[date_iter] = high
-        lows[date_iter] = low
-    if return_metadata:
-        return metadata, highs, lows
-    else:
-        return highs, lows
-
-def get_rev_temperature_dict(lat, lon, dataset, desired_end_date, latest_rev):
-    """
-    Build a dictionary of rainfall data. Include as much final data as possible. If the desired end date
-    is not in the final dataset, append as much prelim as possible.
-    Args:
-        lat (float): the latitude of the grid cell, to 3 decimals
-        lon (float): the longitude of the grid cell, to 3 decimals
-        dataset (str): the dataset name as on hashes.json
-        desired_end_date (datetime.date): don't include prelim data after this point if not needed
-        latest_rev (str): The least accurate revision that is still considered 'final'
-    returns:
-        tuple (highs, lows) of dicts and a bool
-        highs: dict ({datetime.date: float}) of datetime dates and the corresponding high temperature in degress F
-        lows: dict ({datetime.date: float}) of datetime dates and the corresponding low temperature in degress F
-        is_final: True if all data is from final dataset, false if prelim included
-    """
-    highs = {}
-    lows = {}
-    is_final = True
-
-    # Build the data from the most accurate version of the dataset to the least
-    for dataset_revision in dweather_client.ipfs_datasets.datasets[dataset]:
-        additional_highs, additional_lows = get_temperature_dict(lat, lon, dataset_revision)
-        all_dates = list(highs) + list(additional_highs)    
-        highs = {date: highs[date] if date in highs else additional_highs[date] for date in all_dates}
-        lows = {date: lows[date] if date in lows else additional_lows[date] for date in all_dates}
-        # Stop early if we have the end date
-        if desired_end_date in highs:
-            return highs, lows, is_final
-
-        # data is no longer final after we pass the specified version
-        if dataset_revision == latest_rev:
-            is_final = False
-
-    # If we don't reach the desired dataset, return all data.
-    return highs, lows, is_final
-
-def get_rev_tagged_temperature_dict(lat, lon, dataset, desired_end_date=None):
-    ''' Build temps with a revision tag by each date
-    Args:
-        lat (float): the grid cell latitude
-        lon (float): the grid cell longitude
-        dataset (str): the dataset name in ipfs
-        desired_end_date (datetime.date): stop early if we get this end date
-    returns
-        highs: dict with keys of dates, values are tuple (temperature, revision tag) for that date
-        lows: dict with keys of dates, values are tuple (temperature, revision tag) for that date
-    '''
-    highs = {}
-    lows = {}
-
-    # Build the data from the most accurate version of the dataset to the least
-    for dataset_version in dweather_client.ipfs_datasets.datasets[dataset]:
-        additional_highs, additional_lows = get_temperature_dict(lat, lon, dataset_version)
-        all_dates = list(highs) + list(additional_highs)    
-        highs = {date: highs[date] if date in highs else (additional_highs[date], dataset_version) for date in all_dates}
-        lows = {date: lows[date] if date in lows else (additional_lows[date], dataset_version) for date in all_dates}
-        # Stop early if we have the end date
-        if desired_end_date in highs:
-            return highs, lows
-
-    # If we don't reach the desired dataset, return all data.
-    return highs, lows
-
-def get_era5_dict(lat, lon, dataset):
-    """
-    Builds a dict of era5 data
-    Args:
-        lat (float): the latitude of the grid cell. Will be rounded to one decimal
-        lon (float): the longitude of the grid cell. Will be rounded to one decimal
-        dataset (str): valid era5 dataset. Currently only 'era5_land_wind_u-hourly', but
-        more will be added to ipfs soon
-    Returns:
-        a dict ({datetime.datetime: float}) of datetimes and the corresponding weather values.
-        Units are m/s for the wind datasets
-    """
-    heads = get_heads()
-    era5_hash = heads[dataset]
-
-    snapped_lat, snapped_lon = round(lat, 1), round(lon, 1)
-    cpc_lat, cpc_lon = conventional_lat_lon_to_cpc(snapped_lat, snapped_lon)
-    formatted_lat, formatted_lon = f"{cpc_lat:08.3f}", f"{cpc_lon:08.3f}"
-    url = f"{GATEWAY_URL}/ipfs/{era5_hash}/{formatted_lat}_{formatted_lon}.gz"
-    resp = requests.get(url)
-    resp.raise_for_status()
-    datetime_dict = {}
-    with gzip.GzipFile(fileobj=io.BytesIO(resp.content)) as gz:
-        for i, line in enumerate(gz):
-            time_of_year = datetime.datetime(1990 + i, 1, 1)
-            data_list = line.decode('utf-8').strip().split(',')
-            for point in data_list:
-                datetime_dict[time_of_year] = float(point)
-                time_of_year += datetime.timedelta(hours=1)
-    return (snapped_lat, snapped_lon), datetime_dict
-
-
-
-
-
diff --git a/dweather_client/http_queries.py b/dweather_client/http_queries.py
new file mode 100644
index 0000000..df4b0bb
--- /dev/null
+++ b/dweather_client/http_queries.py
@@ -0,0 +1,227 @@
+"""
+Queries associated with the https protocol option.
+"""
+import os, pickle, math, requests, datetime, io, gzip, json, logging, csv, tarfile
+from collections import Counter, deque
+from dweather_client.ipfs_errors import *
+from dweather_client.aliases_and_units import FLASK_DATASETS
+
+GATEWAY_URL = 'https://gateway.arbolmarket.com'
+
+def get_heads(url=GATEWAY_URL):
+    """
+    Get heads.json for a given IPFS gateway.
+    Args:
+        url (str): base url of the IPFS gateway url
+    Returns (example heads.json):
+        {
+            'chirps_05-daily': 'Qm...',
+            'chirps_05-monthly': 'Qm...',
+            'chirps_25-daily': 'Qm...',
+            'chirps_25-monthly': 'Qm...',
+            'cpc_us-daily': 'Qm...',
+            'cpc_us-monthly': 'Qm...'
+        }
+    """
+    hashes_url = url + "/climate/hashes/heads.json"
+    r = requests.get(hashes_url)
+    r.raise_for_status()
+    return r.json()
+
+def get_metadata(hash_str, url=GATEWAY_URL):
+    """
+    Get the metadata file for a given hash.
+    Args:
+        url (str): the url of the IPFS server
+        hash_str (str): the hash of the ipfs dataset
+    Returns (example metadata.json):
+    
+        {
+            'date range': [
+                '1981/01/01',
+                '2019/07/31'
+            ],
+            'entry delimiter': ',',
+            'latitude range': [
+                -49.975, 49.975
+            ],
+            'longitude range': [
+                -179.975, 179.975]
+            ,
+            'name': 'CHIRPS .05 Daily Full Set Uncompressed',
+            'period': 'daily',
+            'precision': 0.01,
+            'resolution': 0.05,
+            'unit of measurement': 'mm',
+            'year delimiter': '\n'
+        }
+    """
+    metadata_url = "%s/ipfs/%s/metadata.json" % (url, hash_str)
+    r = requests.get(metadata_url)
+    r.raise_for_status()
+    return r.json()
+
+def get_hash_cell(hash_str, coord_str, url=GATEWAY_URL):
+    """
+    Read a text file from the gateway.
+    Args:
+        hash_str (str): the hash associated with the desired dataset
+        coord_str (str): the string representation of the file 
+        e.g. '45.000_-96.000'
+    Returns:
+        The contents of the file as a string
+    """
+    dataset_url = '%s/ipfs/%s/%s' % (url, hash_str, coord_str)
+    r = requests.get(dataset_url)
+    r.raise_for_status()
+    return r.text
+
+def get_zipped_hash_cell(hash_str, coord_str, url=GATEWAY_URL):
+    """
+    Read and decompress a text file from the gateway.
+    Args:
+        url (str): the url of the ipfs server
+        hash_str (str): the hash of the dataset
+        coord_str (str): the text file coordinate name e.g. 
+        45.000_-96.000
+    Returns:
+        the contents of the file as a string
+    """
+    dataset_url = '%s/ipfs/%s/%s.gz' % (url, hash_str, coord_str)
+    r = requests.get(dataset_url)
+    r.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as zip_data:
+        return zip_data.read().decode("utf-8")
+
+def get_dataset_cell(lat, lon, dataset_revision, metadata=None):
+    """ 
+    Retrieve the text of a grid cell data file for a given lat lon and dataset.
+    Args:
+        lat (float): the latitude of the grid cell, to 3 decimals
+        lon (float): the longitude of the grid cell, to 3 decimals
+    Returns:
+        A tuple (json, str) of the dataset metadata file and the grid cell data text
+    Raises: 
+        DatasetError: If no matching dataset found on server
+        InputOutOfRangeError: If the lat/lon is outside the dataset range in metadata
+        CoordinateNotFoundError: If the lat/lon coordinate is not found on server
+    """
+    all_hashes = get_heads()
+    if dataset_revision in all_hashes:
+        dataset_hash = all_hashes[dataset_revision]
+    else:
+        raise DatasetError('{} not found on server'.format(dataset_revision))
+    if metadata is None:
+        metadata = get_metadata(dataset_hash)
+    min_lat, max_lat = sorted(metadata["latitude range"])
+    min_lon, max_lon = sorted(metadata["longitude range"])
+    if lat < min_lat or lat > max_lat:
+        raise InputOutOfRangeError("Latitude {} out of dataset revision range [{:.3f}, {:.3f}] for {}".format(lat, min_lat, max_lat, dataset_revision))
+    if  lon < min_lon or lon > max_lon:
+        raise InputOutOfRangeError("Longitude {} out of dataset revision range [{:.3f}, {:.3f}] for {}".format(lon, min_lon, max_lon, dataset_revision))
+    coord_str = "{:.3f}_{:.3f}".format(lat,lon)
+    try:
+        if "compression" in metadata and metadata["compression"] == "gzip":
+            text_data = get_zipped_hash_cell(dataset_hash, coord_str)
+        else:
+            text_data = get_hash_cell(dataset_hash, coord_str)
+        return text_data
+    except requests.exceptions.RequestException as e:
+        raise CoordinateNotFoundError('Coordinate ({}, {}) not found  on ipfs in dataset revision {}'.format(lat, lon, dataset_revision))
+
+def get_station_csv(station_id, station_dataset="ghcnd-imputed-daily", url=GATEWAY_URL):
+    """
+    Retrieve the contents of a station data csv file.
+    Args:
+        station_id (str): the id of the weather station
+        station_dataset (str): which dataset to use, on of ["ghcnd", "ghcnd-imputed-daily"]
+    returns:
+        the contents of the station csv file as a string
+    """
+    all_hashes = get_heads()
+    dataset_hash = all_hashes[station_dataset]
+    dataset_url = "%s/ipfs/%s/%s.csv.gz" % (url, dataset_hash, str(station_id))
+    r = requests.get(dataset_url)
+    r.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as zip_data:
+        return zip_data.read().decode("utf-8")
+
+def get_hurricane_release_dict(release_hash, url=GATEWAY_URL):
+    url = "%s/ipfs/%s/history.json.gz" % (url, release_hash)
+    resp = requests.get(url)
+    resp.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(resp.content)) as zip_data:
+        return json.loads(zip_data.read().decode("utf-8"))
+
+def get_hurricane_dict(head=None):
+    """
+    Get a hurricane dictionary for the atcf_btk-seasonal dataset. 
+    To get a unique value to query the dict by storm, use BASIN + CY + the year
+    part of the HOUR value. BASIN is the ocean, CY is the storm index, and
+    the year is needed as well because the storm index resets every year.
+    Note that there will be multiple readings with the same HOUR value,
+    as readings are taken more than once per hour and then rounded to the nearest
+    hour before posting. 
+    """
+    heads = get_heads()
+    hurr_head = heads['atcf_btk-seasonal']
+    release_ll = traverse_ll(hurr_head)
+    hurr_dict = {}
+    for release_hash in release_ll:
+        release_content = get_hurricane_release_dict(release_hash)
+        try:
+            hurr_dict['features'] += release_content['features']
+        except KeyError:
+            hurr_dict.update(release_content)
+    return hurr_dict
+
+def get_simulated_hurricane_files(basin):
+    """
+    Gets the names of files containing STORM simulated TC data. Takes a basin ID, one of:
+    EP, NA, NI, SI, SP or WP
+    """
+    if basin not in {'EP', 'NA', 'NI', 'SI', 'SP', 'WP'}:
+        raise ValueError("Invalid basin ID")
+    heads = get_heads()
+    hurr_hash = heads['storm-simulated-hurricane']
+    metadata  = get_metadata(hurr_hash)
+    base_url = f"{GATEWAY_URL}/ipfs/{hurr_hash}/"
+    files = [base_url + f for f in metadata['files'] if basin in f]
+    return files
+
+def get_ibracs_hurricane_file():
+    heads = get_heads()
+    hurr_hash = heads['ibtracs-tropical-storm']
+    metadata = get_metadata(hurr_hash)
+    base_url = f"{GATEWAY_URL}/ipfs/{hurr_hash}/"
+    return base_url + metadata["files"][0]
+
+def traverse_ll(head):
+    release_itr = head
+    release_ll = deque()
+    while True:
+        release_ll.appendleft(release_itr)
+        prev_release = get_metadata(release_itr)['previous hash']
+        if prev_release != None:
+            release_itr = prev_release
+        else:
+            return release_ll
+
+def flask_query(dataset, lat, lon, base_url=GATEWAY_URL):
+    if dataset not in FLASK_DATASETS:
+        raise ValueError(f"Valid flask datasets are {FLASK_DATASETS}")
+
+    url = f"{base_url}/linked-list/{dataset}/{lat}_{lon}"
+    r = requests.get(url)
+    r.raise_for_status()
+    resp = r.json()
+
+    data_dict = {}
+    if "hourly" in dataset:
+        for k, v in resp["data"].items():
+            data_dict[datetime.datetime.fromisoformat(k)] = v
+    elif "daily" in dataset:
+        for k, v in resp["data"].items():
+            data_dict[datetime.datetime.fromisoformat(k).date()] = v
+
+    return ((resp["lat"], resp["lon"]), data_dict)
diff --git a/dweather_client/ipfs_datasets.py b/dweather_client/ipfs_datasets.py
deleted file mode 100644
index 8fcaa5d..0000000
--- a/dweather_client/ipfs_datasets.py
+++ /dev/null
@@ -1,33 +0,0 @@
-### How this file works
-''' How to use this file
-Each entry is a sorted list of versions of the dataset, from most accurate to least
-'''
-
-datasets = {
-	"prism_precip" : [
-		"prism_rev_6_precip-daily",
-		"prism_rev_3_precip-daily",
-		#"prism_rev_1_precip-daily"
-	],
-	"prism_temp" : [
-		"prism_rev_6_temp-daily",
-		"prism_rev_3_temp-daily",
-		"prism_rev_1_temp-daily"
-	],
-	"chirps_05": [
-		"chirps_05-daily",
-		"chirps_prelim_05-daily"
-	],
-	"chirps_25": [
-		"chirps_25-daily"
-	],
-	"cpc_global": [
-		"cpc_global-daily"
-	],
-	"cpc_us": [
-		"cpc_us-daily"
-	],
-	"cpc_temp": [
-		"cpc_temp-daily"
-	]
-}
diff --git a/dweather_client/ipfs_errors.py b/dweather_client/ipfs_errors.py
index b90534d..63a407f 100644
--- a/dweather_client/ipfs_errors.py
+++ b/dweather_client/ipfs_errors.py
@@ -16,4 +16,8 @@ class CoordinateNotFoundError(IPFSError):
 
 class DataMalformedError(IPFSError):
     """Raised when a grid cell text file is unable to be parsed according to metadata"""
+    pass
+
+class AliasNotFound(IPFSError):
+    """The alias was not found in the lookup"""
     pass
\ No newline at end of file
diff --git a/dweather_client/ipfs_client.py b/dweather_client/ipfs_queries.py
similarity index 87%
rename from dweather_client/ipfs_client.py
rename to dweather_client/ipfs_queries.py
index 2a04ee6..ab7f9e6 100644
--- a/dweather_client/ipfs_client.py
+++ b/dweather_client/ipfs_queries.py
@@ -1,35 +1,14 @@
+"""
+Queries associated with the ipfs protocol option.
+"""
+
 import ipfshttpclient, json, requests, datetime, io, gzip
 from dweather_client.ipfs_errors import *
 from dweather_client.utils import listify_period
 from dweather_client.df_utils import get_station_ids_with_icao
 import dweather_client.ipfs_datasets
 import pandas as pd
-
-MM_TO_INCHES = 0.0393701
-RAINFALL_PRECISION = 5
-GATEWAY_URL = 'https://gateway.arbolmarket.com'
-
-
-def get_heads(url=GATEWAY_URL):
-    """
-    Get heads.json for a given IPFS gateway.
-    Args:
-        url (str): base url of the IPFS gateway url
-    Returns (example heads.json):
-        {
-            'chirps_05-daily': 'Qm...',
-            'chirps_05-monthly': 'Qm...',
-            'chirps_25-daily': 'Qm...',
-            'chirps_25-monthly': 'Qm...',
-            'cpc_us-daily': 'Qm...',
-            'cpc_us-monthly': 'Qm...'
-        }
-    """
-    hashes_url = url + "/climate/hashes/heads.json"
-    r = requests.get(hashes_url)
-    r.raise_for_status()
-    return r.json()
-
+from dweather_client.http_queries import get_heads
 
 def cat_metadata(hash_str, client=None, pin=True):
     """
@@ -340,6 +319,53 @@ def cat_station_csv(station_id, station_dataset="ghcnd-imputed-daily", client=No
         if (client is None):
             session_client.close()
 
+def cat_icao_stations(station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
+    """
+    For every station that has an icao code, load it into a dataframe and
+    return them all as a list.
+    """
+    station_ids = get_station_ids_with_icao()
+    return cat_station_df_list(station_ids, station_dataset=station_dataset, pin=pin, force_hash=force_hash)
+
+def cat_n_closest_station_dfs(lat, lon, n, station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
+    """
+    Load the closest n stations to a given point into a list of dataframes.
+    """
+    if (force_hash is None):
+        metadata = cat_metadata(get_heads()[station_dataset])
+    else:
+        metadata = cat_metadata(force_hash)
+    station_ids = get_n_closest_station_ids(lat, lon, metadata, n)
+    return cat_station_df_list(station_ids, station_dataset=station_dataset, pin=pin, force_hash=force_hash)
+
+def cat_station_df_list(station_ids, station_dataset="ghcnd-imputed-daily", pin=True, force_hash=None):
+    batch_hash = force_hash
+    if (force_hash is None):
+        batch_hash = get_heads()[station_dataset]
+    metadata = cat_metadata(batch_hash, pin=pin)
+    station_content = []
+    with ipfshttpclient.connect() as client:
+        for station_id in station_ids:
+            logging.info("(%i of %i): Loading station %s from %s into DataFrame%s" % ( \
+                station_ids.index(station_id) + 1,
+                len(station_ids),
+                station_id, 
+                "dWeather head" if force_hash is None else "forced hash",
+                " and pinning to ipfs datastore" if pin else ""
+            ))
+            try:
+                station_content.append(cat_station_df( \
+                    station_id,
+                    station_dataset=station_dataset,
+                    client=client,
+                    pin=pin,
+                    force_hash=batch_hash
+            ))
+            except ipfshttpclient.exceptions.ErrorResponse:
+                logging.warning("Station %s not found" % station_id)
+                
+    return station_content 
+
 
 def cat_icao_stations(client=None, pin=True):
     """ Get a list of station dataframes for all stations that have an icao"""
diff --git a/dweather_client/struct_utils.py b/dweather_client/struct_utils.py
new file mode 100644
index 0000000..f7ed7bf
--- /dev/null
+++ b/dweather_client/struct_utils.py
@@ -0,0 +1,13 @@
+import math
+
+def tupleify(args):
+    if isinstance(args, tuple):
+        return args
+    return (args,)
+
+def find_closest_lat_lon(lst, K):
+    """
+    Find the closest (lat, lon) tuple in a list to a given 
+    (lat, lon) tuple K. Use euclidian distance for performance reasons.
+    """
+    return lst[min(range(len(lst)), key = lambda i: math.sqrt((float(lst[i][0]) - float(K[0]))**2 + (float(lst[i][1]) - float(K[1]))**2 ))] 
diff --git a/dweather_client/timeseries_utils.py b/dweather_client/timeseries_utils.py
new file mode 100644
index 0000000..862e849
--- /dev/null
+++ b/dweather_client/timeseries_utils.py
@@ -0,0 +1,58 @@
+from datetime import datetime
+import pandas as pd
+
+
+def listify_period(start_date, end_date):
+    """
+    Make a list of all dates from start_date to end_date, inclusive.
+    Args:
+        start_date (datetime.date): first date to include
+        end_date (datetime.date): last date to include
+    Returns:
+        list of datetime.date objects
+    """
+    days_in_range = (end_date - start_date).days
+    return [start_date + datetime.timedelta(n) for n in range(days_in_range + 1)]
+
+def period_slice_df(df, ps, pe, yrs):
+    """ 
+    Slice a dataframe by period where n is based on years lookback 
+    
+    return:
+        pd.DataFrame()
+    
+    args:
+        df = any pd.DataFrame() with a datetime.date() index
+        ps = datetime.date object (contract period start)
+        ps = datetime.date object (contract period end)
+        yrs = 30 (years lookback)
+        
+    limitations:
+        max period is 364 days
+        
+    """
+
+    mydf = pd.DataFrame()
+    
+    leftDay = ~((df.index.day < ps.day) & (df.index.month == ps.month))
+    rightDay = ~((df.index.day > pe.day) & (df.index.month == pe.month))
+    #first year just get end
+    if ps.year < pe.year:
+        firstYear = ps.year - yrs
+        yr = df.index.year == firstYear
+        mon = df.index.month >= ps.month
+        #a boolean mask to cut the left and right half month days if need be
+        mydf = mydf.append(df[(yr)&(mon)&(leftDay)])
+        #everything in betweeng
+        for yrss in range(ps.year - (yrs - 1), pe.year - 1):
+            ##boolean mask for now, better method needed##
+            yearChunk = (df.index.year == yrss)&((df.index.month<=pe.month)|(df.index.month>=ps.month))
+            yearChunk = (yearChunk)&leftDay&rightDay
+            mydf = mydf.append(df[yearChunk])
+        #build last year
+        mydf = mydf.append(df[rightDay&(df.index.month<=pe.month)&(df.index.year==pe.year-1)])
+    else:
+        mydf = df[leftDay&rightDay&(df.index.month>=ps.month)&(df.index.month<=pe.month)&(df.index.year>=ps.year-yrs)]
+            
+    return mydf
+
diff --git a/dweather_client/utils.py b/dweather_client/utils.py
deleted file mode 100644
index 3037ba7..0000000
--- a/dweather_client/utils.py
+++ /dev/null
@@ -1,185 +0,0 @@
-"""
-Helpful auxilliary functions that don't directly interact with IPFS.
-This module specifically excludes pandas
-"""
-import math, datetime, zeep
-from heapq import heappush, heappushpop
-from geopy.distance import geodesic
-
-def find_closest_lat_lon(lst, K):
-    """
-    Find the closest (lat, lon) tuple in a list to a given 
-    (lat, lon) tuple K. Use euclidian distance for performance reasons.
-    """
-    return lst[min(range(len(lst)), key = lambda i: math.sqrt((float(lst[i][0]) - float(K[0]))**2 + (float(lst[i][1]) - float(K[1]))**2 ))] 
-
-def build_rtma_lookup(grid_history):
-    """
-    turn the string representation of the grid history into a data structure.
-    {timestamp: (lat_list, lon_list), timestamp: (lat_list, lon_list)}
-    """
-    grid_history = grid_history.split('\n\n')
-    grid_dict = {grid_history[0]: [grid_history[1], grid_history[2]], grid_history[3]: [grid_history[4], grid_history[5]]}
-    for timestamp in grid_dict:
-        for dimension in (0, 1):
-            grid_dict[timestamp][dimension] = [y.strip('][').split(', ') for y in grid_dict[timestamp][dimension].split('\n')]
-    return grid_dict
-
-def build_rtma_reverse_lookup(grid_history):
-    """
-    Reverse index the rtma lookup data structure to enable querying by lat lon.
-    {timestamp: {'lat': {latitude: (x, y)}}, {'lon': {longitude: (x, y)}}}
-    Example query: 
-        rev_grid_dict['2011-01-01T00:00:00']['lat']['20.191999000000006']
-        rev_grid_dict['2011-01-01T00:00:00']['lon']['238.445999']
-    """
-    grid_dict = build_rtma_lookup(grid_history)
-    rev_grid_dict = {}
-    for timestamp in grid_dict:
-        rev_grid_dict[timestamp] = {'lat': {}, 'lon': {}}
-        # reindex lat
-        for y in range(0, len(grid_dict[timestamp][0])):
-            for x in range(0, len(grid_dict[timestamp][0][y])):
-                if grid_dict[timestamp][0][y][x] in rev_grid_dict[timestamp]['lat']:
-                    logging.error("Conflict in parsing rtma grid history.")
-                rev_grid_dict[timestamp]['lat'][grid_dict[timestamp][0][y][x]] = (x, y)
-        # reindex lon
-        for y in range(0, len(grid_dict[timestamp][1])):
-            for x in range(0, len(grid_dict[timestamp][1][y])):
-                if grid_dict[timestamp][1][y][x] in rev_grid_dict[timestamp]['lon']:
-                    logging.error('Conflict in Parsing rtma grid history.')
-                rev_grid_dict[timestamp]['lon'][grid_dict[timestamp][1][y][x]] = (x, y)
-    return rev_grid_dict
-
-def lat_lon_to_rtma_grid(lat, lon, grid_history):
-    grid_dict = build_rtma_reverse_lookup(grid_history)
-    response = {}
-    for timestamp in grid_dict:
-        try:
-            response[timestamp] = (grid_dict[timestamp]['lat'][lat], grid_dict[timestamp]['lon'][lon])
-        except KeyError:
-            response[timestamp] = (None, None)
-            continue
-    return response
-
-def rtma_grid_to_lat_lon(x, y, grid_history):
-    """
-    x: the x coordinate of the rtma grid
-    y: the y coordinate of the rtma grid
-    grid_history: the raw string representation of the rtma grid history to be
-        read directly from a file
-    """
-    grid_dict = build_rtma_lookup(grid_history)
-
-    # get the lat/lon associated with x and y.
-    return [(grid_dict[timestamp][0][y][x], grid_dict[timestamp][1][y][x]) for timestamp in grid_dict]
-
-def snap_to_grid(lat, lon, metadata):
-    """ 
-    Find the nearest (lat,lon) on IPFS for a given metadata file.
-    return: lat, lon
-    args:
-        lat = -90 < lat < 90, float
-        lon = -180 < lon < 180, float
-        metadata: a dWeather metadata file
-
-    """
-    resolution = metadata['resolution']
-    min_lat = metadata['latitude range'][0] #start [lat, lon]
-    min_lon = metadata['longitude range'][0] #end [lat, lon]
-    category = metadata['climate category']
-
-    if 'cpc' in metadata['source data url']:
-        min_lat, min_lon = conventional_lat_lon_to_cpc(min_lat, min_lon)
-
-    # check that the lat lon is in the bounding box
-    snap_lat = round(round((lat - min_lat)/resolution) * resolution + min_lat, 3)
-    snap_lon = round(round((lon - min_lon)/resolution) * resolution + min_lon, 3)
-    return snap_lat, snap_lon
-
-def get_n_closest_station_ids(lat, lon, metadata, n):
-    """
-    Get the station ids for the <n> closest stations to a given lat lon. 
-    Requires metadata of ghcnd to get station coordinates.
-    """
-    pq = []
-    for feature in metadata["stations"]["features"]:
-        s_lat = float(feature["geometry"]["coordinates"][0])
-        s_lon = float(feature["geometry"]["coordinates"][1])
-        distance = geodesic([lat, lon], [s_lat, s_lon]).miles
-        station_id = feature["properties"]["station id"]
-        if (len(pq) >= n):
-            heappushpop(pq, (1 / distance, station_id))
-        else:
-            heappush(pq, (1 / distance, station_id))
-    return [pair[1] for pair in pq]
-
-def cpc_lat_lon_to_conventional(lat, lon):
-    """
-    Convert a pair of coordinates from the idiosyncratic CPC lat lon
-    format to the conventional lat lon format.
-    """
-    lat, lon = float(lat), float(lon)
-    if (lon >= 180):
-        return lat, lon - 360
-    else:
-        return lat, lon
-
-
-def conventional_lat_lon_to_cpc(lat, lon):
-    """
-    Convert a pair of coordinates from conventional (lat,lon)
-    to the idiosyncratic CPC (lat,lon) format.
-    """
-    lat, lon = float(lat), float(lon)
-    if (lon < 0):
-        return lat, lon + 360
-    else:
-        return lat, lon
-
-
-def listify_period(start_date, end_date):
-    """
-    Make a list of all dates from start_date to end_date, inclusive.
-    Args:
-        start_date (datetime.date): first date to include
-        end_date (datetime.date): last date to include
-    Returns:
-        list of datetime.date objects
-    """
-    days_in_range = (end_date - start_date).days
-    return [start_date + datetime.timedelta(n) for n in range(days_in_range + 1)]
-
-
-def is_revision_final(dataset, revision_to_check, last_acceptable_revision):
-    """
-    See if a given dataset revision should be considered 'final'
-    a final dataset is any dataset at or before the last acceptable one
-    Args:
-        dataset (str): the dataset name (e.g. prism_precip)
-        revision_to_check (str): the revision tag under consideration for final status
-        last_acceptable_revision (str): the last 'final' revision tag
-    returns:
-        bool true if the revision is considered final
-    """
-    # The dataset revision lists are ordered by accuracy so we simply compare the indicies in the list
-    dataset_list = IPFSDatasets.datasets[dataset]
-    return dataset_list.index(revision_to_check) <= dataset_list.index(last_acceptable_revision)
-
-def snotel_to_ghcnd(snotel_id, state_fips):
-    """
-    Convert a SnoTEL ID to a station id that can be passed into GHCND or
-    GHCNDi. snotel ids are a 3 or 4 digit integer, and the state_fips is 
-    a two character abbrevation for the state -- for example CO.
-    """
-    client = zeep.Client('https://wcc.sc.egov.usda.gov/awdbWebService/services?WSDL')
-    result = client.service.getStationMetadata( \
-        '%s:%s:SNTL' % (str(snotel_id), str(state_fips)))
-    ghcnd_id = 'USS00%s' % result['actonId']
-    return ghcnd_id
-
-def celcius_to_fahrenheit(deg_c):
-    return round((deg_c * 9/5) + 32, 5)
-
-def mms_to_inches(mms):
-    return round((mms / 25.4), 5)
diff --git a/requirements.txt b/requirements.txt
index b679f4b..e92f130 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,5 @@ pytest>=6.1.2
 requests
 geopy
 zeep
+astropy
+timezonefinder
diff --git a/setup.py b/setup.py
index 76b9d2f..ed026d6 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ def load_requirements(fname):
     name="dweather_client",
     include_package_data=True,
     install_requires=load_requirements("requirements.txt"),
-    version="1.9.3",
+    version="2.0.0",
     author="Arbol",
     author_email="info@arbolmarket.com",
     description="Python client for interacting with weather data on IPFS.",


Test patch:
diff --git a/dweather_client/tests/test_aliases_and_units.py b/dweather_client/tests/test_aliases_and_units.py
new file mode 100644
index 0000000..2f9f341
--- /dev/null
+++ b/dweather_client/tests/test_aliases_and_units.py
@@ -0,0 +1,6 @@
+from dweather_client.aliases_and_units import *
+
+def test_snotel_to_ghcnd():
+    assert snotel_to_ghcnd(602, 'CO') == 'USS0005K05S'
+    assert snotel_to_ghcnd(838, 'CO') == 'USS0005J08S'
+
diff --git a/dweather_client/tests/test_arithmetic.py b/dweather_client/tests/test_arithmetic.py
deleted file mode 100644
index ca50a58..0000000
--- a/dweather_client/tests/test_arithmetic.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from dweather_client import arithmetic, df_arithmetic, df_loader, utils, df_utils
-import datetime
-
-TODAY = datetime.date.today()
-ONE_MONTH = datetime.timedelta(days=30)
-
-
-def test_build_historical_rainfall_dict():
-    historical_raindict = arithmetic.build_historical_rainfall_dict(
-        41.175, 
-        -75.125, 
-        'chirps_05-daily', 
-        TODAY - (ONE_MONTH * 8),
-        TODAY - (ONE_MONTH * 7),
-    )
-
-
-def test_sum_period_rainfall():
-    a_sum = arithmetic.sum_period_rainfall(
-         41.175, 
-        -75.125, 
-        'chirps_05-daily', 
-        TODAY - (ONE_MONTH * 8),
-        TODAY - (ONE_MONTH * 7),
-    )
-
-
-def test_sum_period_df():
-    rainfall_df = df_loader.get_rainfall_df(41.125, -75.125, 'chirps_05-daily')
-    sliced_period = df_utils.period_slice_df(
-        rainfall_df, 
-        TODAY - (ONE_MONTH * 8), 
-        TODAY - (ONE_MONTH * 7), 
-        30,
-    )
-    df_sum = df_arithmetic.sum_period_df(
-        rainfall_df, 
-        TODAY - (ONE_MONTH * 8), 
-        TODAY - (ONE_MONTH * 7),
-        30, 
-        "PRCP")
-
-
-def test_avg_period_df():
-    rainfall_df = df_loader.get_rainfall_df(41.125, -75.125, 'chirps_05-daily')
-    sliced_period = df_utils.period_slice_df(
-        rainfall_df, 
-        TODAY - (ONE_MONTH * 8), 
-        TODAY - (ONE_MONTH * 7), 
-        30,
-    )
-    df_avg = df_arithmetic.avg_period_df(
-        rainfall_df, 
-        TODAY - (ONE_MONTH * 8), 
-        TODAY - (ONE_MONTH * 7),
-        30, 
-        "PRCP")
diff --git a/dweather_client/tests/test_client.py b/dweather_client/tests/test_client.py
new file mode 100644
index 0000000..b5af761
--- /dev/null
+++ b/dweather_client/tests/test_client.py
@@ -0,0 +1,183 @@
+from dweather_client.client import get_station_history, get_gridcell_history, get_tropical_storms
+from dweather_client.aliases_and_units import snotel_to_ghcnd
+import numpy as np
+import datetime
+from astropy import units as u
+from astropy.units import imperial
+import pytest
+
+DAILY_DATASETS = [
+    "chirpsc_final_05-daily",
+    "chirpsc_final_25-daily",
+    "chirpsc_prelim_05-daily",
+    "cpcc_precip_global-daily",
+    "cpcc_precip_us-daily",
+    "cpcc_temp_max-daily",
+    "cpcc_temp_min-daily",
+    "prismc-tmax-daily",
+    "prismc-tmin-daily",
+    "prismc-precip-daily"
+]
+
+HOURLY_DATASETS = [
+    "rtma_pcp-hourly",
+    "era5_land_wind_v-hourly",
+    "era5_land_wind_u-hourly"
+]
+
+def test_get_gridcell_history_units():
+    for s in DAILY_DATASETS + HOURLY_DATASETS:
+        for use_imperial in [True, False]:
+            res = get_gridcell_history(37, -83, s, use_imperial_units=use_imperial)
+            for k in res:
+                if use_imperial and ("precip" in s or "chirps" in s):
+                    assert res[k].unit == imperial.inch
+                elif use_imperial and s == "rtma_pcp-hourly":
+                    assert res[k].unit == imperial.pound / imperial.foot**2
+                elif use_imperial and "wind" in s:
+                    assert res[k].unit == imperial.mile / u.hour
+                elif use_imperial:
+                    assert res[k].unit == imperial.deg_F
+                elif "precip" in s or "chirps" in s:
+                    assert res[k].unit == u.mm
+                elif s == "rtma_pcp-hourly":
+                    assert res[k].unit == u.kg / u.m**2
+                elif "wind" in s:
+                    assert res[k].unit == u.m / u.s
+                else:
+                    assert res[k].unit == u.deg_C
+
+def test_get_gridcell_history_snap():
+    lat_range = np.linspace(35, 40, 3)
+    lon_range = np.linspace(-100, -80, 3)
+
+    for s in DAILY_DATASETS:
+        for lat in lat_range:
+            for lon in lon_range:
+                res = get_gridcell_history(lat, lon, s, also_return_snapped_coordinates=True, also_return_metadata=True)
+                resolution, (snapped_lat, snapped_lon) = res[1]["metadata"]["resolution"], res[2]["snapped to"]
+                assert abs(snapped_lat - lat) <= resolution
+                assert abs(snapped_lon - lon) <= resolution
+
+def test_get_gridcell_history_date_range():
+    for s in DAILY_DATASETS:
+        res = get_gridcell_history(37, -83, s)
+        first_date, last_date = sorted(res)[0], sorted(res)[-1]
+        diff = last_date - first_date
+        assert diff.days == len(res) - 1
+    for s in HOURLY_DATASETS:
+        res = get_gridcell_history(37, -83, s)
+        first_date, last_date = sorted(res)[0], sorted(res)[-1]
+        time_diff = last_date - first_date
+        time_diff_hours = time_diff.days * 24 + time_diff.seconds // 3600
+        assert time_diff_hours + 1 == len(res)
+
+def test_get_gridcell_nans():
+    prism_r = get_gridcell_history(31.083, -120, "prismc-precip-daily")
+    assert np.isnan(prism_r[datetime.date(1981, 8, 29)].value)
+
+    rtma_r = get_gridcell_history(40.694754071664825, -73.93445989160746, "rtma_pcp-hourly")
+    tz = next(iter(rtma_r)).tzinfo
+    assert np.isnan(rtma_r[datetime.datetime(2011, 1, 29, 17, tzinfo=tz)].value)
+
+def test_station():
+    get_station_history('USW00014820', 'SNOW')
+    get_station_history(snotel_to_ghcnd(838, 'CO'), 'snow water equivalent')
+    get_station_history('USW00014820', 'TMAX', dataset='ghcnd-imputed-daily')
+    get_station_history('USW00014820', 'TMIN', dataset='ghcnd-imputed-daily')
+    get_station_history(snotel_to_ghcnd(602, 'CO'), 'WESD')#, return_result_as_dataframe=True)
+
+def test_storms_bad_args():
+    with pytest.raises(ValueError):
+        get_tropical_storms('simulated', 'NI', radius=100)
+    with pytest.raises(ValueError):
+        get_tropical_storms('simulated', 'NI', lat=100)
+    with pytest.raises(ValueError):
+        get_tropical_storms('simulated', 'NI', radius=500, lat=21, lon=65, min_lat=21, max_lat=22, min_lon=65, max_lon=66)
+
+def test_simulated_storms():
+    df_all_ni = get_tropical_storms('simulated', 'NI')
+    df_subset_circle_ni = get_tropical_storms('simulated', 'NI', radius=500, lat=21, lon=65)
+    df_subset_box_ni = get_tropical_storms('simulated', 'NI', min_lat=21, max_lat=22, min_lon=65, max_lon=66)
+
+    assert len(df_all_ni.columns) == len(df_subset_circle_ni.columns) == len(df_subset_box_ni.columns) == 10
+    assert len(df_subset_circle_ni) < len(df_all_ni)
+    assert len(df_subset_box_ni) < len(df_all_ni)
+
+def test_atcf_storms():
+    df_all_al = get_tropical_storms('atcf', 'AL')
+    df_subset_circle_al = get_tropical_storms('atcf', 'AL', radius=50, lat=26, lon=-90)
+    df_subset_box_al = get_tropical_storms('atcf', 'AL', min_lat=26, max_lat=26.5, min_lon=-91, max_lon=-90.5)
+
+    assert len(df_all_al.columns) == len(df_subset_circle_al.columns) == len(df_subset_box_al.columns) == 37
+    assert len(df_subset_circle_al) < len(df_all_al)
+    assert len(df_subset_box_al) < len(df_all_al)
+
+def test_historical_storms():
+    df_all_na = get_tropical_storms('historical', 'NA')
+    df_subset_circle_na = get_tropical_storms('historical', 'NA', radius=50, lat=26, lon=-90)
+    df_subset_box_na = get_tropical_storms('historical', 'NA', min_lat=26, max_lat=26.5, min_lon=-91, max_lon=-90.5)
+
+    assert len(df_all_na.columns) == len(df_subset_circle_na.columns) == len(df_subset_box_na.columns) == 163
+    assert len(df_subset_circle_na) < len(df_all_na)
+    assert len(df_subset_box_na) < len(df_all_na)
+
+''' TODO some tests for RTMA behavior to be integrated into the new system
+def test_lat_lon_to_grid():
+    heads = http_client.get_heads()
+    rtma_hash = heads['rtma_pcp-hourly']
+    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
+    r.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
+        grid_history = grid_history_file.read().decode('utf-8')
+    assert utils.lat_lon_to_rtma_grid('40.752907470419586', '247.66162774628384', grid_history) == {'2011-01-01T00:00:00': ((491, 841), (491, 841)), '2016-01-06T14:00:00': (None, None)}
+    assert utils.lat_lon_to_rtma_grid('20.191999000000006', '238.445999', grid_history) == {'2011-01-01T00:00:00': ((0 ,0), (0, 0)), '2016-01-06T14:00:00': ((0, 0), (0, 0))}
+
+def test_rtma_grid_to_lat_lon():
+    heads = http_client.get_heads()
+    rtma_hash = heads['rtma_pcp-hourly']
+    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
+    r.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
+        grid_history = grid_history_file.read().decode('utf-8')
+
+        # case where lat/lon are the same
+        assert utils.rtma_grid_to_lat_lon(0, 0, grid_history) == [('20.191999000000006', '238.445999'), ('20.191999000000006', '238.445999')]
+
+        # random cases where lat/lon are different
+        assert utils.rtma_grid_to_lat_lon(50, 54, grid_history) == [('21.61726877222153', '239.39106426923487'), ('21.617275250933048', '239.39106861956924')]
+        assert utils.rtma_grid_to_lat_lon(130, 42, grid_history) == [('21.677552644312303', '241.3744282380296'), ('21.67755927656665', '241.37444172371673')]
+        assert utils.rtma_grid_to_lat_lon(491, 841, grid_history) == [('40.752907470419586', '247.66162774628384'), ('40.75299702642884', '247.66167780662005')]
+
+def test_rtma_lookup():
+    heads = http_client.get_heads()
+    rtma_hash = heads['rtma_pcp-hourly']
+    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
+    r.raise_for_status()
+    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
+        grid_history = grid_history_file.read().decode('utf-8')
+    lookup = utils.build_rtma_lookup(grid_history)
+    reverse_lookup = utils.build_rtma_reverse_lookup(grid_history)
+    for rev_lookup_lon in reverse_lookup['2016-01-06T14:00:00']['lon']:
+        rev_lookup_x, rev_lookup_y = reverse_lookup['2016-01-06T14:00:00']['lon'][rev_lookup_lon]
+        assert (rev_lookup_x, rev_lookup_y) == reverse_lookup['2016-01-06T14:00:00']['lon'][rev_lookup_lon]
+        assert lookup['2016-01-06T14:00:00'][1][rev_lookup_y][rev_lookup_x] == rev_lookup_lon
+
+'''
+
+
+
+
+
+'''
+def test_storm(): TODO
+   atcf_btk-seasonal
+   ibtracs-tropical-storm
+   storm-simulated-hurricane
+   teleconnections-el-nino-monthly
+
+def test_yield(): TODO
+    nass_corn-yearly
+    nass_soybeans-yearly
+    sco-yearly
+'''
diff --git a/dweather_client/tests/test_data_loader.py b/dweather_client/tests/test_data_loader.py
deleted file mode 100644
index 9287213..0000000
--- a/dweather_client/tests/test_data_loader.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from dweather_client import http_client, utils
-from dweather_client.data_loader import GridCellDataLoader, StationDataLoader
-import datetime
-
-def test_get_revision_rainfall():
-    dataset = "chirps_05"
-    dataset_revision = "chirps_prelim_05-daily"
-    coords = 41.175, -75.125
-    loader = GridCellDataLoader(dataset, *coords, "rainfall")
-    http_client_meta, http_client_dict = http_client.get_rainfall_dict(*coords, dataset_revision, return_metadata=True)
-    loader_data = loader.get_revision(dataset_revision)
-    loader_dict = loader_data["data"]
-    loader_meta = loader_data["metadata"]
-    assert http_client_dict == loader_dict
-    assert http_client_meta == loader_meta
-
-
-def test_get_revision_temperature():
-    dataset = "prism_temp"
-    dataset_revision = 'prism_temp-daily'
-    coords=41.25, -77.75
-    loader = GridCellDataLoader(dataset, *coords, "temperature")
-    http_client_meta, http_client_highs, http_client_lows = http_client.get_temperature_dict(*coords, dataset_revision, return_metadata=True)
-    loader_data = loader.get_revision(dataset_revision)
-    loader_dict = loader_data["data"]
-    loader_meta = loader_data["metadata"]
-    assert http_client_highs == loader_dict["highs"]
-    assert http_client_lows == loader_dict["lows"]
-    assert http_client_meta == loader_meta
-
-def test_multi_revision_dict_rainfall():
-    dataset = "prism_precip"
-    coords = 41.25, -77.75
-    loader = GridCellDataLoader(dataset, *coords, "rainfall")
-    loader_rain = loader.build_multi_revision_dict()
-    http_rain, _ = http_client.get_rev_rainfall_dict(*coords, dataset, datetime.date(2020, 1, 1), "prism_rev_1_precip-daily")
-
-    for date in http_rain:
-        assert loader_rain[date][0] == http_rain[date]
-
-
-def test_multi_revision_dict_temperature():
-    dataset = "prism_temp"
-    coords = 41.25, -77.75
-    loader = GridCellDataLoader(dataset, *coords, "temperature")
-    loader_temps = loader.build_multi_revision_dict()
-    http_highs, http_lows = http_client.get_rev_tagged_temperature_dict(*coords, dataset, datetime.date(2020, 1, 1))
-
-    for date in http_highs:
-        assert loader_temps["highs"][date][0] == http_highs[date][0]
-        assert loader_temps["lows"][date][0] == http_lows[date][0]
-
-def test_single_instance_enforcement_gridcell():
-    chirps_args = "chirps_05", 41.625, -93.125, "rainfall"
-    loader_a = GridCellDataLoader(*chirps_args)
-    loader_b = GridCellDataLoader(*chirps_args)
-    assert loader_a is loader_b
-    prism_args = "prism_precip", 41.25, -77.75, "rainfall"
-    loader_c = GridCellDataLoader(*prism_args)
-    assert loader_a is not loader_c
-
-def test_single_instance_enforcement_station():
-    station_one_args = 'USW00024285'
-    loader_a = StationDataLoader(station_one_args)
-    loader_b = StationDataLoader(station_one_args)
-    assert loader_a is loader_b
-    station_two_args = 'USW00024285', "ghcnd"
-    loader_c = StationDataLoader(*station_two_args)
-    assert loader_a is not loader_c
-
-def test_station_temp_and_snow():
-    loader = StationDataLoader('USW00014820')
-    loader.get_temperatures()
-    loader.get_temperatures(use_fahrenheit=False)
-    loader = StationDataLoader('USW00014820', station_dataset='ghcnd')
-    loader.get_snowfall()
-    loader.get_snow_depth()
diff --git a/dweather_client/tests/test_df_arithmetic.py b/dweather_client/tests/test_df_arithmetic.py
deleted file mode 100644
index e8944a0..0000000
--- a/dweather_client/tests/test_df_arithmetic.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from dweather_client.df_arithmetic import cat_icao_stations, cat_n_closest_station_dfs, get_polygon_df
-from dweather_client.http_client import get_heads, get_metadata
-from dweather_client.utils import snap_to_grid
-from shapely.geometry import Point
-import os
-
-def test_cat_n_closest_station_dfs():
-    ghcndi_hash = get_heads()["ghcnd-imputed-daily"] 
-    stations = cat_n_closest_station_dfs( \
-        36, 
-        -94.5, 
-        10, 
-        pin=False, 
-        force_hash=ghcndi_hash
-    )
diff --git a/dweather_client/tests/test_df_loader.py b/dweather_client/tests/test_df_loader.py
index 977e126..8a548db 100644
--- a/dweather_client/tests/test_df_loader.py
+++ b/dweather_client/tests/test_df_loader.py
@@ -1,41 +1,28 @@
-from dweather_client import df_loader, utils
-import datetime
-
-def test_rtma_df():
-    client = df_loader.RTMADFClient()
-    lat_lon, rtma_df = client.get_best_rtma_df('42.01', '-117.01')
-    assert client.get_best_rtma_df(40.0001, -69.000001)[0] == (39.999662751557175, -68.9922083077804)
-    assert client.get_best_rtma_df(40.99999, -69.000001)[0] == (41.00782433956041, -68.9996167647198)
-    assert client.get_best_rtma_df(41, -69.000001)[0] == (41.00782433956041, -68.9996167647198)
-    assert client.get_best_rtma_df(41, -71.99999)[0] == (40.99988297329153, -72.01208284896211)
-
-def test_rainfall_df():
-    rainfall_df = df_loader.get_rainfall_df(41.125, -75.125, 'chirps_05-daily')
-
-def test_temperature_df():
-    temp_df = df_loader.get_temperature_df(43.083, -92.000, 'prism_temp-daily')
-
-def test_station_dfs():
-    station_df = df_loader.get_station_rainfall_df('USW00024285')
-    station_df2 = df_loader.get_station_temperature_df('USW00024285')
-    station_df3 = df_loader.get_station_snow_df('USW00024285')
+from dweather_client import df_loader
 
 def test_get_simulated_hurricane_df():
     df_all_ni = df_loader.get_simulated_hurricane_df('NI')
-    df_subset_ni = df_loader.get_simulated_hurricane_df('NI', radius=500, lat= 21, lon=65)
+    df_subset_circle_ni = df_loader.get_simulated_hurricane_df('NI', radius=500, lat=21, lon=65)
+    df_subset_box_ni = df_loader.get_simulated_hurricane_df('NI', min_lat=21, max_lat=22, min_lon=65, max_lon=66)
 
-    assert len(df_all_ni.columns) == len(df_subset_ni.columns) == 10
-    assert len(df_subset_ni) < len(df_all_ni)
+    assert len(df_all_ni.columns) == len(df_subset_circle_ni.columns) == len(df_subset_box_ni.columns) == 10
+    assert len(df_subset_circle_ni) < len(df_all_ni)
+    assert len(df_subset_box_ni) < len(df_all_ni)
 
 def test_get_historical_hurricane_df():
-    df_all_al = df_loader.get_historical_hurricane_df('AL')
-    df_subset_al = df_loader.get_historical_hurricane_df('AL', radius=500, lat= 27, lon=-93)
-
-    assert len(df_all_al.columns) == len(df_subset_al.columns) == 37
-    assert len(df_subset_al) < len(df_all_al)
-
-def test_get_era5_df():
-    lat, lon = 35.70284883765463, -81.29880863239713
-    df = df_loader.get_era5_df(lat, lon, 'era5_land_wind_u-hourly')[1]
-    first_time, last_time = df.index[0], df.index[-1]
-    assert (last_time - first_time).days * 24 == len(df) - 1
+    df_all_na = df_loader.get_historical_hurricane_df('NA')
+    df_subset_circle_na = df_loader.get_historical_hurricane_df('NA', radius=50, lat=26, lon=-90)
+    df_subset_box_na = df_loader.get_historical_hurricane_df('NA', min_lat=26, max_lat=26.5, min_lon=-91, max_lon=-90.5)
+
+    assert len(df_all_na.columns) == len(df_subset_circle_na.columns) == len(df_subset_box_na.columns) == 163
+    assert len(df_subset_circle_na) < len(df_all_na)
+    assert len(df_subset_box_na) < len(df_all_na)
+
+def test_get_atcf_hurricane_df():
+    df_all_al = df_loader.get_atcf_hurricane_df('AL')
+    df_subset_circle_al = df_loader.get_atcf_hurricane_df('AL', radius=50, lat=26, lon=-90)
+    df_subset_box_al = df_loader.get_atcf_hurricane_df('AL', min_lat=26, max_lat=26.5, min_lon=-91, max_lon=-90.5)
+
+    assert len(df_all_al.columns) == len(df_subset_circle_al.columns) == len(df_subset_box_al.columns) == 37
+    assert len(df_subset_circle_al) < len(df_all_al)
+    assert len(df_subset_box_al) < len(df_all_al)
diff --git a/dweather_client/tests/test_df_utils.py b/dweather_client/tests/test_df_utils.py
deleted file mode 100644
index 18c1f77..0000000
--- a/dweather_client/tests/test_df_utils.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from dweather_client import df_utils
-
-def test_icao():
-    ids = df_utils.get_station_ids_with_icao()
diff --git a/dweather_client/tests/test_grid_utils.py b/dweather_client/tests/test_grid_utils.py
new file mode 100644
index 0000000..9ceb669
--- /dev/null
+++ b/dweather_client/tests/test_grid_utils.py
@@ -0,0 +1,55 @@
+from dweather_client.grid_utils import *
+from dweather_client.http_queries import get_heads, get_metadata
+
+def test_grid():
+    get_n_closest_station_ids(38, -94, 5)
+
+def test_snap_to_grid_chirps():
+    heads = get_heads()
+    chirps_metadata = get_metadata(heads['chirps_05-daily'])
+    lat, lon = snap_to_grid(41.1842, -75.11, chirps_metadata)
+    assert lat == 41.175
+    assert lon == -75.125
+
+def test_snap_to_grid_prism():
+    heads = get_heads()
+    prism_metadata = get_metadata(heads['prism_precip-daily'])
+    lat, lon = snap_to_grid(39.8398, -104.193, prism_metadata)
+    assert lat == 39.833
+    assert lon == -104.208
+
+def test_snap_to_grid_cpc_global_daily():
+    # tests snap_to_grid with ideosyncratic cpc lat/lon format
+    heads = get_heads()
+    prism_metadata = get_metadata(heads['cpc_global-daily'])
+    lat, lon = snap_to_grid(69.754, 330.759, prism_metadata)
+    assert lat == 69.750
+    assert lon == 330.750
+
+def test_cpc_lat_lon_to_conventional():
+    # case where coords are ok:
+    lat = 25.000
+    lon = 45.000
+    new_lat, new_lon = cpc_lat_lon_to_conventional(lat, lon)
+    assert new_lat == lat
+    assert new_lon == lon
+    # case where lon needs to be converted
+    lat = 25.000
+    lon = 262.000
+    new_lat, new_lon = cpc_lat_lon_to_conventional(lat, lon)
+    assert new_lat == lat
+    assert new_lon == -98.000
+
+def test_conventional_lat_lon_to_cpc():
+    # case where coords are ok:
+    lat = 25.000
+    lon = 45.000
+    new_lat, new_lon = conventional_lat_lon_to_cpc(lat, lon)
+    assert new_lat == lat
+    assert new_lon == lon
+    # case where lon needs to be converted
+    lat = 25.000
+    lon = -98.000
+    new_lat, new_lon = conventional_lat_lon_to_cpc(lat, lon)
+    assert new_lat == lat
+    assert new_lon == 262.000    
\ No newline at end of file
diff --git a/dweather_client/tests/test_http_client.py b/dweather_client/tests/test_http_client.py
deleted file mode 100644
index 80a036e..0000000
--- a/dweather_client/tests/test_http_client.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from dweather_client import http_client, utils
-from datetime import date
-
-def test_get_best_rtma_dict():
-    client = http_client.RTMAClient()
-    client.get_best_rtma_dict('49.01', '-125.01')
-
-def test_http_client_rainfall():
-    dataset = 'chirps_05'
-    dataset_revision = 'chirps_05-daily'
-    heads = http_client.get_heads()
-    chirps_metadata = http_client.get_metadata(heads[dataset_revision])
-    rainfall_dict = http_client.get_rainfall_dict(41.175, -75.125, dataset_revision)
-    rainfall_rev_dict = http_client.get_rev_rainfall_dict(41.175, -75.125, dataset, date.today(), dataset_revision)
-
-def test_http_client_temperature():
-    dataset = "cpc_temp"
-    dataset_revision = 'cpc_temp-daily'
-    lat, lon = utils.conventional_lat_lon_to_cpc(41.25, -77.75)
-    heads = http_client.get_heads()
-    cpc_metadata = http_client.get_metadata(heads[dataset_revision])
-    temperature_dict = http_client.get_temperature_dict(lat, lon, dataset_revision)
-    temperature_rev_dict = http_client.get_rev_temperature_dict(lat, lon, dataset, date.today(), dataset_revision)
-    tagged_temperature_rev_dict = http_client.get_rev_tagged_temperature_dict(lat, lon, dataset)
-
-def test_get_station_csv():
-    test_station = "ZI000067969"
-    csv_str = http_client.get_station_csv(test_station)
-    assert (len(csv_str.split()) > 100)
-
-def test_parse_station_temps_as_dict():
-    test_station = "ZI000067969"
-    csv_str = http_client.get_station_csv(test_station)
-    tmins, tmaxs = http_client.parse_station_temps_as_dict(csv_str)
-    assert len(tmaxs) > 100
-    assert len(tmins) > 100 
-
-
-def test_get_full_prismc_history():
-    lat, lon = 25.000, -80.750
-    res = http_client.get_prismc_dict(lat, lon, "precip")
-    first_date, last_date = sorted(res)[0], sorted(res)[-1]
-    diff = last_date - first_date
-    assert diff.days == len(res) - 1
-    
-def test_get_full_rtma_history():
-    lat, lon = 27.5343, -75.2341
-    res = http_client.get_full_rtma_history(27.5343, -75.2341)
-    assert abs(lat - res[0][0]) < 1
-    assert abs(lon - res[0][1]) < 1
-    first_date, last_date = sorted(res[1])[0], sorted(res[1])[-1]
-    time_diff = last_date - first_date
-    time_diff_hours = time_diff.days * 24 + time_diff.seconds // 3600
-    assert time_diff_hours == len(res[1])
-
-def test_get_era5_dict():
-    lat, lon = 35.70284883765463, -81.29880863239713
-    res = http_client.get_era5_dict(lat, lon, 'era5_land_wind_u-hourly')
-    first_time, last_time = sorted(res[1])[0], sorted(res[1])[-1]
-    time_diff = last_time - first_time
-    time_diff_hours = time_diff.days * 24 + time_diff.seconds // 3600
-    assert time_diff_hours + 1 == len(res[1])
-
diff --git a/dweather_client/tests/test_hurricane.py b/dweather_client/tests/test_hurricane.py
deleted file mode 100644
index 3d41f72..0000000
--- a/dweather_client/tests/test_hurricane.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from dweather_client import http_client
-
-def test_hurricane():
-    default_dict = http_client.get_hurricane_dict()
-    assert(len(default_dict['features']) >= 93058)
\ No newline at end of file
diff --git a/dweather_client/tests/test_ipfs_client.py b/dweather_client/tests/test_ipfs_client.py
deleted file mode 100644
index 1e49532..0000000
--- a/dweather_client/tests/test_ipfs_client.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from dweather_client import ipfs_client, http_client, utils
-from datetime import date
-
-
-def test_ipfs_client_rainfall():
-    dataset = 'chirps_05'
-    dataset_revision = 'chirps_05-daily'
-    heads = http_client.get_heads()
-    chirps_metadata = ipfs_client.cat_metadata(heads[dataset_revision])
-    ipfs_client.cat_rainfall_dict(41.175, -75.125, dataset_revision)
-    rainfall_rev_dict = ipfs_client.cat_rev_rainfall_dict(41.175, -75.125, dataset, date.today(), dataset_revision)
-
-def test_ipfs_client_temperature():
-    dataset = "cpc_temp"
-    dataset_revision = 'cpc_temp-daily'
-    lat, lon = utils.conventional_lat_lon_to_cpc(41.25, -77.75)
-    heads = http_client.get_heads()
-    cpc_metadata = ipfs_client.cat_metadata(heads[dataset_revision])
-    temperature_dict = ipfs_client.cat_temperature_dict(lat, lon, dataset_revision)
-    temperature_rev_dict = ipfs_client.cat_rev_temperature_dict(lat, lon, dataset, date.today(), dataset_revision)
-   
-def test_cat_station_csv():
-    test_station = "ZI000067969"
-    csv_str = ipfs_client.cat_station_csv(test_station)
-    assert(len(csv_str.split()) > 100)
-
-def test_cat_station_df():
-    test_station = "ZI000067969"
-    csv_df = ipfs_client.cat_station_df(test_station)
-    assert(csv_df.shape[0] > 100)
-
-
-# TODO: test pin all stations?
-# def test_pin_all_stations():
-#    ipfs_client.pin_all_stations()
-
-# This test takes a while.
-'''def test_cat_icao_stations():
-    station_dfs = ipfs_client.cat_icao_stations()
-    assert len(station_dfs > 100)'''
\ No newline at end of file
diff --git a/dweather_client/tests/test_scripts.py b/dweather_client/tests/test_scripts.py
index acdf5b1..0d43c92 100644
--- a/dweather_client/tests/test_scripts.py
+++ b/dweather_client/tests/test_scripts.py
@@ -1,5 +1,5 @@
 import pickle, gzip, os, math
-from dweather_client.utils import find_closest_lat_lon
+from dweather_client.struct_utils import find_closest_lat_lon
 
 def test_generate_rtma_lat_lons():
     with gzip.open(os.path.join(os.path.dirname(__file__), "../etc/rtma_lat_lons.p.gz"), "rb") as gz:
diff --git a/dweather_client/tests/test_station.py b/dweather_client/tests/test_station.py
deleted file mode 100644
index c2d36e9..0000000
--- a/dweather_client/tests/test_station.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from dweather_client import http_client, utils
-from datetime import date
-
-def test_station():
-    station = http_client.get_station_csv("ASN00010073")
-    assert(len(station.split()) > 365)
diff --git a/dweather_client/tests/test_timeseries_utils.py b/dweather_client/tests/test_timeseries_utils.py
new file mode 100644
index 0000000..ca71933
--- /dev/null
+++ b/dweather_client/tests/test_timeseries_utils.py
@@ -0,0 +1,8 @@
+from dweather_client.timeseries_utils import *
+import datetime
+
+TODAY = datetime.date.today()
+ONE_MONTH = datetime.timedelta(days=30)
+
+
+
diff --git a/dweather_client/tests/test_utils.py b/dweather_client/tests/test_utils.py
deleted file mode 100644
index 44e8000..0000000
--- a/dweather_client/tests/test_utils.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from dweather_client import utils, http_client
-import os, io, requests, gzip
-'''
-def test_lat_lon_to_grid():
-    heads = http_client.get_heads()
-    rtma_hash = heads['rtma_pcp-hourly']
-    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
-    r.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
-        grid_history = grid_history_file.read().decode('utf-8')
-    assert utils.lat_lon_to_rtma_grid('40.752907470419586', '247.66162774628384', grid_history) == {'2011-01-01T00:00:00': ((491, 841), (491, 841)), '2016-01-06T14:00:00': (None, None)}
-    assert utils.lat_lon_to_rtma_grid('20.191999000000006', '238.445999', grid_history) == {'2011-01-01T00:00:00': ((0 ,0), (0, 0)), '2016-01-06T14:00:00': ((0, 0), (0, 0))}
-
-
-def test_rtma_grid_to_lat_lon():
-    heads = http_client.get_heads()
-    rtma_hash = heads['rtma_pcp-hourly']
-    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
-    r.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
-        grid_history = grid_history_file.read().decode('utf-8')
-
-        # case where lat/lon are the same
-        assert utils.rtma_grid_to_lat_lon(0, 0, grid_history) == [('20.191999000000006', '238.445999'), ('20.191999000000006', '238.445999')]
-
-        # random cases where lat/lon are different
-        assert utils.rtma_grid_to_lat_lon(50, 54, grid_history) == [('21.61726877222153', '239.39106426923487'), ('21.617275250933048', '239.39106861956924')]
-        assert utils.rtma_grid_to_lat_lon(130, 42, grid_history) == [('21.677552644312303', '241.3744282380296'), ('21.67755927656665', '241.37444172371673')]
-        assert utils.rtma_grid_to_lat_lon(491, 841, grid_history) == [('40.752907470419586', '247.66162774628384'), ('40.75299702642884', '247.66167780662005')]
-'''
-
-def test_snotel_to_ghcnd():
-    assert utils.snotel_to_ghcnd(602, 'CO') == 'USS0005K05S'
-    assert utils.snotel_to_ghcnd(838, 'CO') == 'USS0005J08S'
-
-def test_rtma_lookup():
-    heads = http_client.get_heads()
-    rtma_hash = heads['rtma_pcp-hourly']
-    r = requests.get('https://gateway.arbolmarket.com/ipfs/%s/grid_history.txt.gz' % rtma_hash)
-    r.raise_for_status()
-    with gzip.GzipFile(fileobj=io.BytesIO(r.content)) as grid_history_file:
-        grid_history = grid_history_file.read().decode('utf-8')
-    lookup = utils.build_rtma_lookup(grid_history)
-    reverse_lookup = utils.build_rtma_reverse_lookup(grid_history)
-    for rev_lookup_lon in reverse_lookup['2016-01-06T14:00:00']['lon']:
-        rev_lookup_x, rev_lookup_y = reverse_lookup['2016-01-06T14:00:00']['lon'][rev_lookup_lon]
-        assert (rev_lookup_x, rev_lookup_y) == reverse_lookup['2016-01-06T14:00:00']['lon'][rev_lookup_lon]
-        assert lookup['2016-01-06T14:00:00'][1][rev_lookup_y][rev_lookup_x] == rev_lookup_lon
-
-def test_snap_to_grid_chirps():
-    heads = http_client.get_heads()
-    chirps_metadata = http_client.get_metadata(heads['chirps_05-daily'])
-    lat, lon = utils.snap_to_grid(41.1842, -75.11, chirps_metadata)
-    assert lat == 41.175
-    assert lon == -75.125
-
-def test_snap_to_grid_prism():
-    heads = http_client.get_heads()
-    prism_metadata = http_client.get_metadata(heads['prism_precip-daily'])
-    lat, lon = utils.snap_to_grid(39.8398, -104.193, prism_metadata)
-    assert lat == 39.833
-    assert lon == -104.208
-
-def test_snap_to_grid_cpc_global_daily():
-    # tests snap_to_grid with ideosyncratic cpc lat/lon format
-    heads = http_client.get_heads()
-    prism_metadata = http_client.get_metadata(heads['cpc_global-daily'])
-    lat, lon = utils.snap_to_grid(69.754, 330.759, prism_metadata)
-    assert lat == 69.750
-    assert lon == 330.750
-
-def test_get_n_closest_station_ids():
-    heads = http_client.get_heads()
-    ghcnd_metadata = http_client.get_metadata(heads["ghcnd-imputed-daily"])
-
-    # get the 20 closest station ids to a spot in Kentucky
-    ids = utils.get_n_closest_station_ids(37, -85, ghcnd_metadata, 20)
-
-def test_cpc_lat_lon_to_conventional():
-    # case where coords are ok:
-    lat = 25.000
-    lon = 45.000
-    new_lat, new_lon = utils.cpc_lat_lon_to_conventional(lat, lon)
-    assert new_lat == lat
-    assert new_lon == lon
-    # case where lon needs to be converted
-    lat = 25.000
-    lon = 262.000
-    new_lat, new_lon = utils.cpc_lat_lon_to_conventional(lat, lon)
-    assert new_lat == lat
-    assert new_lon == -98.000
-    
-
-def test_conventional_lat_lon_to_cpc():
-    # case where coords are ok:
-    lat = 25.000
-    lon = 45.000
-    new_lat, new_lon = utils.conventional_lat_lon_to_cpc(lat, lon)
-    assert new_lat == lat
-    assert new_lon == lon
-    # case where lon needs to be converted
-    lat = 25.000
-    lon = -98.000
-    new_lat, new_lon = utils.conventional_lat_lon_to_cpc(lat, lon)
-    assert new_lat == lat
-    assert new_lon == 262.000