#!/usr/bin/env python3 """ A shell script that converts one currency to another. Usage: shekel [-rh] AMOUNT FROM TO [TO ...] FROM and TO are either currency codes or a fuzzy substring (requires iterfzf and fzf). Examples: shekel 200 USD CAD """ import argparse import sys import json from urllib import request from datetime import datetime from pathlib import Path API_KEY = "92a7ccb3b6038a7151fc15e99f2de83d" # please use your own! API_URL = "http://apilayer.net/api/live?access_key={}¤cies={},{}" DEFAULT_CODES = ('CAD', 'USD') CACHE_FILE = "/tmp/shekel-hist" CACHE_TTL = 3600 class CurrencyCache(): def __init__(self, path=CACHE_FILE, ttl=CACHE_TTL): self.path = Path(path) self.ttl = ttl cache = {} if self.path.exists(): cache = json.loads(self.path.read_text()) self.cache = cache def __enter__(self): return self def __exit__(self, *args): self.path.write_text(json.dumps(self.cache)) def clear(self): db = self.cache['_DB'] self.cache.clear() self.cache['_DB'] = db def store(self, from_code, to_code, rate): self._set("-".join([from_code, to_code]), rate) self._set("-".join([to_code, from_code]), 1 / rate) def get(self, from_code, to_code): key = "-".join([from_code, to_code]) if key in self.cache: return self.cache[key]['value'] return None def _set(self, key, value): self.cache[key] = {'time': datetime.now().timestamp(), 'value': value} def _request_currency_rate(url=API_URL, *args): """Fetch currency rates from remote API.""" real_url = url.format(*map(request.quote, map(str, args))) data = json.loads(request.urlopen(real_url).read().decode('utf-8')) if not data['success']: raise Exception(data['error']['info']) return data["quotes"] def format_currency(curr): """Format a currency entry for the FZF prompt.""" return "{} {}".format(curr['id'], curr['currencyName']) def currency_rate(from_code, to_code): """ Retrieves the exchange rate between two currencies. :param from_code: an ISO 4217 currency code of the source currency :param to_code: an ISO 4217 currency code of the destination currency :returns: an exchange rate (a float) """ if from_code == to_code: return 1.0 elif not from_code or not to_code: return None rates = _request_currency_rate(API_URL, API_KEY, from_code, to_code) try: return (1 / rates["USD" + from_code]) * rates["USD" + to_code] except KeyError as e: print("Could not find currency named {}".format(from_code)) return None def each_currency_rate(from_code, to_codes, refresh=False): """ Returns a cached generator for each exchange rate. :param from_code: the ISO 4217 currency code of the source currency :param to_codes: a list of ISO 4217 currency codes to convert to :yields: a tuple containg (TO_CODE, EXCHANGE_RATE) """ with CurrencyCache(path=CACHE_FILE, ttl=CACHE_TTL) as cache: if refresh: cache.clear() for code in to_codes: rate = cache.get(from_code, code) if not rate: rate = currency_rate(from_code, code) if rate: cache.store(from_code, code, rate) if not rate: yield (None, None) else: yield (code, rate) def main(): opts = argparse.ArgumentParser( description="Convert one amount of currency into another, or multiple.") opts.add_argument('amount', type=float, help="The amount to convert") opts.add_argument('src', metavar='from', type=str, help="Currency to convert from") opts.add_argument('dest', metavar='to', type=str, nargs='+', help="Currency(ies) to convert to") opts.add_argument('-r', '--refresh', action='store_true', help="Refresh the cache") args = opts.parse_args() try: for code, rate in each_currency_rate(args.src, args.dest, refresh=args.refresh): if code: print("{:.4f} {}".format(rate * args.amount, code)) except KeyboardInterrupt: print("Aborted!") sys.exit(1) if __name__ == '__main__': main()