2017-04-20 18:37:22 -07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2017-10-03 10:05:00 -07:00
|
|
|
"""
|
|
|
|
Retrieves the time (and time difference) at a location (can be an address,
|
|
|
|
postal code, country name, etc). Uses Google Maps APIs to map locations to
|
|
|
|
coordinates, and coordinates to timezones.
|
|
|
|
|
|
|
|
Requires: pytz, tzlocal
|
|
|
|
Usage: time-in Copenhagen "Toronto, ON" Vancouver
|
|
|
|
"""
|
2017-04-20 18:37:22 -07:00
|
|
|
|
|
|
|
import sys
|
|
|
|
import json
|
2017-12-05 22:39:10 -08:00
|
|
|
import os
|
2017-04-20 18:37:22 -07:00
|
|
|
from base64 import b64encode
|
|
|
|
from datetime import datetime
|
|
|
|
from urllib import request
|
2017-10-03 10:05:00 -07:00
|
|
|
from pathlib import Path
|
2017-04-20 18:37:22 -07:00
|
|
|
|
|
|
|
from pytz import timezone
|
|
|
|
from tzlocal import get_localzone
|
|
|
|
|
|
|
|
URL_BASE = "https://maps.googleapis.com/maps/api/"
|
|
|
|
URL_TZ = URL_BASE + "timezone/json?location={},{}×tamp={}"
|
|
|
|
URL_GEOCODE = URL_BASE + "geocode/json?address={}"
|
|
|
|
|
2017-12-05 22:39:10 -08:00
|
|
|
CACHE_FILE = "{}/timein".format(os.environ.get("XDG_CACHE_HOME") or "/tmp")
|
2017-04-20 18:37:22 -07:00
|
|
|
CACHE = {}
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
def _generate_url(url, *args):
|
|
|
|
return url.format(*map(request.quote, map(str, args)))
|
|
|
|
|
2017-04-21 22:58:55 -07:00
|
|
|
def _request(url, *args):
|
2017-10-03 10:05:00 -07:00
|
|
|
real_url = url.format(*map(request.quote, map(str, args)))
|
|
|
|
resp = request.urlopen(real_url).read()
|
|
|
|
result = json.loads(resp.decode('utf-8'))
|
|
|
|
if result['status'] == 'OK':
|
|
|
|
return result
|
|
|
|
elif result['status'] != 'UNKNOWN_ERROR':
|
|
|
|
raise Exception(result['error_message'])
|
|
|
|
|
|
|
|
def get_timezone(coords, timestamp):
|
|
|
|
return _request(URL_TZ, coords['lat'], coords['lng'], timestamp)['timeZoneId']
|
2017-04-20 18:37:22 -07:00
|
|
|
|
|
|
|
def get_coords(desc):
|
2017-04-21 22:58:55 -07:00
|
|
|
result = _request(URL_GEOCODE, desc)
|
2017-10-03 10:05:00 -07:00
|
|
|
if 'results' in result:
|
|
|
|
return {"lat": result['results'][0]['geometry']['location']['lat'],
|
|
|
|
"lng": result['results'][0]['geometry']['location']['lng'],
|
|
|
|
"name": result['results'][0]['formatted_address']}
|
2017-04-20 18:37:22 -07:00
|
|
|
|
|
|
|
def cache(key, fn, ttl):
|
|
|
|
if key in CACHE and CACHE[key]['time'] < CACHE[key]['time'] + ttl:
|
|
|
|
return CACHE[key]['value']
|
|
|
|
value = fn()
|
|
|
|
if value is not None:
|
|
|
|
CACHE[key] = {
|
|
|
|
'time': datetime.now().timestamp(),
|
2017-10-03 10:05:00 -07:00
|
|
|
'value': value
|
2017-04-20 18:37:22 -07:00
|
|
|
}
|
|
|
|
return value
|
|
|
|
|
2017-10-03 10:05:00 -07:00
|
|
|
def diff(a, b, absolute=False):
|
|
|
|
"""Returns a timedelta object"""
|
2017-04-20 18:37:22 -07:00
|
|
|
a = a.replace(tzinfo=None)
|
|
|
|
b = b.replace(tzinfo=None)
|
2017-10-03 10:05:00 -07:00
|
|
|
return (max(a, b) - min(a, b), b > a)
|
2017-04-20 18:37:22 -07:00
|
|
|
|
|
|
|
def encode(s):
|
|
|
|
return str(b64encode(s.encode('ascii')))
|
|
|
|
|
|
|
|
|
|
|
|
#
|
2017-10-03 10:05:00 -07:00
|
|
|
def each_location(locations):
|
|
|
|
"""
|
|
|
|
Enumerates over and maps a list of location strings to time difference data.
|
|
|
|
"""
|
|
|
|
here = get_localzone().localize(datetime.now())
|
|
|
|
for location in locations:
|
|
|
|
coords = cache("coords-" + encode(location),
|
|
|
|
lambda l=location: get_coords(l),
|
|
|
|
86400)
|
|
|
|
if coords:
|
|
|
|
tzname = cache("tz-" + encode(location),
|
|
|
|
lambda c=coords: get_timezone(c, here.timestamp()),
|
|
|
|
1800)
|
|
|
|
then = here.astimezone(timezone(tzname))
|
|
|
|
tdiff, direction = diff(here, then)
|
|
|
|
if tdiff.seconds == 0:
|
|
|
|
suffix = "(same timezone)"
|
|
|
|
elif direction:
|
|
|
|
suffix = "ahead"
|
|
|
|
else:
|
|
|
|
suffix = "behind"
|
|
|
|
|
|
|
|
yield {
|
|
|
|
"name": coords['name'],
|
|
|
|
"then": then.strftime("%b %d, %H:%M:%S %Z%z"),
|
|
|
|
"diff": int(tdiff.seconds / 60 / 60),
|
|
|
|
"suffix": suffix,
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
yield None
|
|
|
|
|
|
|
|
|
2017-04-20 18:37:22 -07:00
|
|
|
if __name__ == '__main__':
|
2017-10-03 10:05:00 -07:00
|
|
|
assert sys.argv[1:], "Usage: {} [LOCATION]".format(sys.argv[0])
|
2017-04-20 18:37:22 -07:00
|
|
|
|
2017-10-03 10:05:00 -07:00
|
|
|
cachefp = Path(CACHE_FILE)
|
|
|
|
CACHE = json.loads(cachefp.read_text()) if cachefp.exists() else {}
|
2017-04-20 18:37:22 -07:00
|
|
|
|
2017-10-03 10:05:00 -07:00
|
|
|
for data in each_location(sys.argv[1:]):
|
|
|
|
if data:
|
|
|
|
print("{name:24} {then}\t{diff}h {suffix}".format(**data))
|
2017-04-20 18:37:22 -07:00
|
|
|
else:
|
2017-10-03 10:05:00 -07:00
|
|
|
print("Couldn't find {name} or something went wrong".format(**data), file=sys.stderr)
|
2017-04-20 18:37:22 -07:00
|
|
|
|
2017-10-03 10:05:00 -07:00
|
|
|
cachefp.write_text(json.dumps(CACHE))
|