import requests import pandas as pd import datetime import time import gradio as gr import os ########### # other API's of interest: https://medium.com/@imdipto/best-free-alternatives-to-the-wunderground-weather-api-21acb22450e6 ########## OPENWEATHER_API_KEY = os.environ.get('OPENWEATHER_API_KEY') WEATHERAPI_KEY = os.environ.get('WEATHERAPI_KEY') def openweather_to_result(lat, lon, gmt_time): """ API docs: https://openweathermap.org/api/one-call-api#current Parameters ------------ lat [float]: decimal valued latitude lon [float]: decimal valued longitude gmt_time [datetime object]: time of desired forecast, in gmt and as python datetime object Returns -------- cloud_pct Tuple(List, List): list of cloud percent and corresponding time for times within 1.5 hours of input GMT time """ exclude_parts = 'current,minutely,daily,alerts' request_url = f'https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&exclude={exclude_parts}&appid={OPENWEATHER_API_KEY}' response = requests.get(request_url) data = response.json() cloud_pct = [] forecast_times = [] # timeframe around input time to check cloud % for timeframe = datetime.timedelta(hours=1, minutes=30) for hour in data['hourly']: # dt property is unix utc time of forecasted data - convert this to python datetime object forecast_time = datetime.datetime.fromtimestamp( hour['dt'], tz=datetime.timezone.utc) if abs(forecast_time - gmt_time) <= timeframe: # cloud pct is stored in each hour at top level cloud_pct.append(hour['clouds']) forecast_times.append(forecast_time) return cloud_pct, forecast_times def weatherapi_to_result(lat, lon, gmt_time): """ API docs: https://www.weatherapi.com/docs/ TODO: implement wrapper instead https://github.com/weatherapicom/weatherapi-Python Parameters ------------ lat [float]: decimal valued latitude lon [float]: decimal values longitude gmt_time [datetime object]: time of desired forecast, in gmt and as python datetime object Returns -------- cloud_pct Tuple(List, List): list of cloud percent and corresponding time for times within 1.5 hours of input GMT time """ request_url = f'http://api.weatherapi.com/v1/forecast.json?key={WEATHERAPI_KEY}&q={lat},{lon}&days=2&alerts=no' response = requests.get(request_url) data = response.json() timezone = data['location']['tz_id'] cloud_pct = [] forecast_times = [] # quick error handling to make sure input time python object has "timezone" property attached try: gmt_time = gmt_time.astimezone(datetime.timezone.utc) except: gmt_time = gmt_time.tz_localize('utc') # timeframe around input time to check cloud % for timeframe = datetime.timedelta(hours=1, minutes=30) # this api is first divided into days, then hours for day in data['forecast']['forecastday']: for hour in day['hour']: # time_epoch contains unix epoch time in GMT/UTC #forecast_time = datetime.datetime.fromtimestamp(hour['time_epoch'], ZoneInfo(timezone)) forecast_time = datetime.datetime.fromtimestamp( hour['time_epoch'], datetime.timezone.utc) if abs(forecast_time - gmt_time) <= timeframe: cloud_pct.append(hour['cloud']) forecast_times.append( forecast_time.astimezone(datetime.timezone.utc)) return cloud_pct, forecast_times def met_to_result(lat, lon, gmt_time): """ API doc: https://api.met.no/weatherapi/locationforecast/2.0/documentation How to: https://api.met.no/doc/locationforecast/HowTO Parameters ------------ lat [float]: decimal valued latitude lon [float]: decimal values longitude gmt_time [datetime object]: time of desired forecast, in gmt and as python datetime object Returns -------- cloud_pct Tuple(List, List): list of cloud percent and corresponding time for times within 1.5 hours of input GMT time """ # set user agent https://stackoverflow.com/questions/10606133/sending-user-agent-using-requests-library-in-python # must be unique per API Terms of Service https://api.met.no/doc/TermsOfService headers = { 'User-Agent': 'NASAEarthScienceRemoteSensingUnit alex.h.stoken@nasa.gov'} request_url = f'https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={lat}&lon={lon}' response = requests.get(request_url, headers=headers) data = response.json() cloud_pct = [] forecast_times = [] # timeframe around input time to check cloud % for timeframe = datetime.timedelta(hours=1, minutes=30) # walk through json return for hour in data['properties']['timeseries']: # time is utc formatted time https://api.met.no/doc/locationforecast/FAQ forecast_time = datetime.datetime.strptime( hour['time'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=datetime.timezone.utc) # check if time of forecast is withing "timeframe" of desired time if abs(forecast_time - gmt_time) <= timeframe: # grab cloud pct from location within the nested json, add to list cloud_pct.append(hour['data']['instant'] ['details']['cloud_area_fraction']) # add time of forecast to list. Should be an "on the hour" time forecast_times.append(forecast_time) return cloud_pct, forecast_times ################ # generate text ################ def file_to_cloud_listing(input_file, services): """ Args: input_file (Union[str, gradio FileType]): input csv file with LAT, LON, SITE, GMT cols services (List): list of weather api servies to check Returns: str: formatted string with weather predictions for locations """ # this works if the input is from gradio. Then the file has an name property try: sites = pd.read_csv(input_file.name, parse_dates=['GMT']) using_gradio = True except: # this is for input from a script or command line sites = pd.read_csv(input_file, parse_dates=['GMT']) using_gradio = False start = time.perf_counter() date_format = "%H:%M" text = '' # each row is a site. Get weather data and then print it for each service for each site. for row_idx, row in sites.iterrows(): #time_of_interest = datetime.datetime.strptime(row.GMT, '%m/%d/%y %H:%M') text += check_row(row, services, date_format) text += f'{"="*60}\n' return text def check_row(row, services, date_format="%H:%M"): """Check a row of data (a pd.Series with LAT, LON, GMT, SITE cols) Args: row (pd.Series): pd.Series with LAT, LON, GMT, SITE cols) services (List): List of weather services (['OpenWeather', 'MET (Norwegian)', 'WeatherAPI'] or subset) date_format (str, optional): Format for printing time of site pass over. Defaults to "%H:%M". Returns: str: formatted str of text for weather vals """ text = "" text += f'{"Location":13}:\t\t{row.SITE} @ {row["GMT"].strftime(date_format)} GMT\n' if not isinstance(row.GMT, datetime.datetime): GMT = row["GMT"].to_pydatetime() else: GMT = row["GMT"] GMT = GMT.replace(tzinfo=datetime.timezone.utc) if 'OpenWeather' in services: try: cldp, times = openweather_to_result(row.LAT, row.LON, GMT) text += format_cldp_and_time("OpenWeather", cldp=cldp, times=times) except Exception as e: text += f'OpenWeather:\t\tError {e} in API processing\n' if 'MET (Norwegian)' in services: try: cldp, times = met_to_result(row.LAT, row.LON, GMT) text += format_cldp_and_time("Norwegian", cldp=cldp) except Exception as e: text += f'Norwegian:\t\tError {e} in API processing\n' if 'WeatherAPI' in services: try: cldp, times = weatherapi_to_result(row.LAT, row.LON, GMT) text += format_cldp_and_time("WeatherAPI", cldp=cldp) except Exception as e: text += f'WeatherAPI:\t\tError {e} in API processing\n' return text def format_cldp_and_time(api_name, cldp, times=None): """Formats output text for lists of cloud percents and forecast times Args: api_name ([type]): Name of weather source. cldp (List): List of floating point cloud percentage values. times (List, optional): List of forecast times, as datetime objects. Defaults to None. Returns: str: formatted text for printing """ text = '' date_format = "%H:%M" if times is not None: text += f'{"Forecast Time:":13}\t\t' + ' '.join(time.strftime(date_format) for time in times) + "\n" text += f'{api_name:13}:\t\t{" ".join(f"{p:<6.0f}" for p in cldp)}\n' return text inputs = [gr.inputs.File(label='Site File with Lat/Lon and GMT Time'), gr.inputs.CheckboxGroup(label='Weather Services', choices=['OpenWeather', 'MET (Norwegian)', 'WeatherAPI'], default=['OpenWeather', 'MET (Norwegian)'])] outputs = gr.outputs.Textbox(label ='Cloud % for hour before, hour of, hour after') css = """* {font-family: "Lucida Console", "Courier New", monospace !important;/* <-- fonts */ }""" gr.Interface(fn=file_to_cloud_listing, inputs=inputs, css=css, outputs=outputs, allow_screenshot=False).launch()