diff --git a/bin/fetch_moon_data.py b/bin/fetch_moon_data.py new file mode 100755 index 0000000..6bf5f44 --- /dev/null +++ b/bin/fetch_moon_data.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# Eryn Wells + +''' +Fetch moon data from api.met.no. +''' + +import argparse +import datetime +import json +import os.path +import requests +import sys +import pytz + +API_URL = 'https://api.met.no/weatherapi/sunrise/2.0/.json' + +# From https://en.wikipedia.org/wiki/San_Francisco +SAN_FRANCISCO_LAT_LON = (37.7775, -122.416389) +SAN_FRANCISCO_TIMEZONE = 'America/Los_Angeles' + +def parse_args(argv, *a, **kw): + parser = argparse.ArgumentParser(*a, **kw) + parser.add_argument('--date', + help=''' + Starting date for number of days to fetch in YYYY-MM-DD format. + Defaults to today's date. + ''') + parser.add_argument('--days', type=int, default=1, + help=''' + Number of days of data to fetch. The max is 15 per API rules. See + https://api.met.no/weatherapi/sunrise/2.0/documentation#Output_format + for details. + ''') + parser.add_argument('-l', '--location', + default='{0[0]},{0[1]}'.format(SAN_FRANCISCO_LAT_LON), + help=''' + Geolocation in latitude and longitude coordinates separated by a + comma. If this argument is omitted, the geolocation of San + Francisco, CA, USA will be used instead. + ''') + parser.add_argument('-n', '--dry-run', action='store_true', + help="Do everything but write downloaded data to the file.") + parser.add_argument('-t', '--timezone', default=SAN_FRANCISCO_TIMEZONE, + help=''' + Timezone to query for. If this argument is omitted, + 'America/Los_Angeles' will be used. + ''') + parser.add_argument('file') + args = parser.parse_args(argv) + return args + +def offset_to_timedelta(offset): + hours, minutes = offset.split(':') + return datetime.timedelta(hours=hours, minutes=minutes) + +def lat_long_from_location(location): + try: + lat_lon = tuple(round(float(x), 1) for x in location.split(',')) + except ValueError: + print("Unable to parse '{location}' into a lat/lon pair.") + return None, None + + if len(lat_lon) != 2: + print("Unable to parse '{location}' into a lat/lon pair.") + return None, None + + return lat_lon[0], lat_lon[1] + +def download_moon_data(latitude, longitude, date, utc_offset, number_of_days, dry_run): + print(f"Downloading {number_of_days} {'days' if number_of_days != 1 else 'day'} of moon data starting {date} {utc_offset}.", file=sys.stderr) + + moon_data = { + 'location': {} + } + + for _ in range(number_of_days): + if not dry_run: + print(f"Fetching data for {date}.", file=sys.stderr) + response = requests.get(API_URL, params={ + 'lat': latitude, + 'lon': longitude, + 'date': date.strftime('%Y-%m-%d'), + 'offset': utc_offset, + 'days': 1, + }) + + try: + response_json = response.json() + except json.JSONDecodeError as e: + print(f'Error decoding JSON response: {e}', file=sys.stderr) + print(response.text, file=sys.stderr) + raise + + moon_data.setdefault('meta', response_json['meta']) + moon_data['location'].setdefault('height', response_json['location']['height']) + moon_data['location'].setdefault('latitude', response_json['location']['latitude']) + moon_data['location'].setdefault('longitude', response_json['location']['longitude']) + moon_data['location'].setdefault('time', []) + moon_data['location']['time'].append(response_json['location']['time'][0]) + else: + print(f"Dry run. Fetching data for {date} will be skipped.", file=sys.stderr) + + date = date + datetime.timedelta(days=1) + + return moon_data + +def main(argv): + args = parse_args(argv[1:], prog=argv[0]) + + timezone = pytz.timezone(args.timezone) + + date = None + try: + date = datetime.datetime.strptime(args.date, '%Y-%m-%d') + except ValueError: + date = datetime.datetime.now(tz=timezone).date() + print("Date must be in YYYY-MM-DD format.", file=sys.stderr) + except TypeError: + date = datetime.datetime.now(tz=timezone).date() + print("No date was given.", file=sys.stderr) + + print(f"Using date: {date}.", file=sys.stderr) + + # Check args.file for some moon data that has already been downloaded. + moon_data = None + filename = args.file + if os.path.isfile(filename): + try: + with open(filename, 'r') as f: + moon_data = json.load(f) + except json.JSONDecodeError: + print(f"{filename} contains invalid JSON data.", file=sys.stderr) + moon_data = None + + if args.location: + lat, lon = lat_long_from_location(args.location) + else: + print("No location given.", file=sys.stderr) + lat, lon = None, None + + utc_offset = datetime.datetime.now(tz=pytz.timezone(args.timezone)).strftime('%z') + utc_offset = utc_offset[:3] + ':' + utc_offset[3:] + + if moon_data: + should_fetch_new_data = False + + moon_data_location = moon_data['location'] + try: + moon_data_lat, moon_data_lon = ( + float(moon_data_location['latitude']), + float(moon_data_location['longitude']) + ) + except KeyError: + print(f'{filename} is missing lat,lon information.') + should_fetch_new_data = True + + if not should_fetch_new_data and (moon_data_lat != lat or moon_data_lon != lon): + print(f"Data for {lat},{lon} requested, but {filename} has data for {moon_data_lat},{moon_data_lon}.", file=sys.stderr) + should_fetch_new_data = True + + if not should_fetch_new_data: + for record in moon_data_location['time']: + try: + record_date = datetime.datetime.strptime(record['date'], '%Y-%m-%d').date() + except (ValueError, TypeError): + continue + + if record_date == date and record.get('moonphase'): + print(f"{filename} has existing moon phase data for {date}.", file=sys.stderr) + #break + else: + should_fetch_new_data = True + + if should_fetch_new_data: + print(f"{filename} doesn't contain moon data for the given date.", + file=sys.stderr) + + if not lat or not lon: + print(f"No location given. Using {moon_data_lat},{moon_data_lon} from {filename}.", file=sys.stderr) + lat = moon_data_lat + lon = moon_data_lon + + try: + moon_data = download_moon_data(lat, lon, date, utc_offset, args.days, args.dry_run) + if not args.dry_run: + with open(filename, 'w') as f: + json.dump(moon_data, f, indent=2) + else: + print(f"Dry run. Dumping moon data to {filename} will be skipped.") + except json.JSONDecodeError: + return -1 + + if not moon_data: + print(f"{filename} doesn't contain any moon data.", file=sys.stderr) + try: + moon_data = download_moon_data(lat, lon, date, utc_offset, args.days, args.dry_run) + with open(filename, 'w') as f: + json.dump(moon_data, f, indent=2) + except json.JSONDecodeError: + return -1 + + return 0 + +if __name__ == '__main__': + import sys + result = main(sys.argv) + sys.exit(0 if not result else result)