-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathPyExchangeRates.py
404 lines (323 loc) · 17.7 KB
/
PyExchangeRates.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# -------------------------------------------------------------------------------------------------------------------
# PyExchangeRates : An unoffcial interface to the Open Exchange Rates API
# -------------------------------------------------------------------------------------------------------------------
#
# Author: Ashok Fernandez - https://github.com/ashokfernandez/
# Date : 23 / 09 / 2013
#
# Description:
# Downloads the latest exchange rates from http://openexchangerates.org/ and saves them to a file. If the file can be
# found the module will load the rates from the file if the file is less than one day old. Otherwise the file is updated
# with the newest rates. There are money objects which can be manipulated with addition, substraction, multiplication
# and division - with the results always being in USD. Money values can be converted to other currencies. See the example
# below for more details
#
# Licence:
# Copyright Ashok Fernandez 2013
# Released under the MIT license - http://opensource.org/licenses/MIT
#
# Usage example:
# >>> import PyExchangeRates
# >>> exchange = PyExchangeRates.Exchange('YOUR API KEY HERE')
# >>> a = exchange.withdraw(1000, 'USD')
# >>> b = exchange.withdraw(1000, 'EUR')
# >>> print(a + b)
# 2352.363797 USD
#
# >>> print(a - b)
# -352.363797 USD
#
# >>> print(a * b)
# 1352363.796680 USD
#
# >>> print(a * 2)
# 2000.000000 USD
#
# >>> print(a + b)
# 2352.363797 USD
#
# >>> print(b / 2)
# 500.000000 USD
#
# >>> print(a.convert('AUD'))
# 1061.079000 AUD
# -------------------------------------------------------------------------------------------------------------------
try: # Python3
from urllib.request import urlopen
from urllib.error import URLError, HTTPError
except ImportError: # Python2
from urllib2 import urlopen, URLError, HTTPError
import sys # sys.setdefaultencoding is cancelled by site.py
reload(sys) # to re-enable sys.setdefaultencoding()
sys.setdefaultencoding('utf-8')
import json # Allows the data to be decoded
from datetime import datetime # For timestamping
import time # For timestamping
import numbers # For detecting if number
# --------------------------------------------------------------------------------------------------------------------
# CONSTANTS
# --------------------------------------------------------------------------------------------------------------------
UPDATE_THRESHOLD_DAYS = 1 # Updates the currencies once a day
BASE_URL = 'http://openexchangerates.org/api/' # Base API for the Open Exchange Rates API
GET_LATEST = 'latest.json' # Add this to the Base URL for the lastest rates
GET_NAMES = 'currencies.json' # Add this to the Base URL for the names of the currencies
UNITED_STATES_DOLLARS_KEY = 'USD' # Currency key for United States Dollars
DEBUG_MODE = False # Set to False to turn off debugging messages
# --------------------------------------------------------------------------------------------------------------------
# MISC FUNCTIONS
# --------------------------------------------------------------------------------------------------------------------
def internet_on():
''' Returns True if the internet is avaliable, otherwise returns false'''
connectionAvaliable = False
try:
# Check if Google is online
response = urlopen('http://www.google.com',timeout=5)
connectionAvaliable = True
except URLError as err:
# Connection failed, either Google is down or more likely the internet connection is down
pass
return connectionAvaliable
def debug(message):
''' Prints debugging messages if debug mode is set to true'''
if DEBUG_MODE:
print(message)
# --------------------------------------------------------------------------------------------------------------------
# EXCEPTIONS
# --------------------------------------------------------------------------------------------------------------------
class AccessDataFailure(Exception):
''' Exception for when there is no data on the currencies - either from file or from the internet '''
pass
class BadAppID(Exception):
''' Thrown when the app id for the open currency exchange is invalid'''
pass
class InvalidCurrencyKey(Exception):
''' Thrown when a currency key is given that is not in the Exchange '''
pass
# --------------------------------------------------------------------------------------------------------------------
# CLASSES
# --------------------------------------------------------------------------------------------------------------------
class SimpleObjectEncoder(json.JSONEncoder):
''' Allows simple objects which contain no methods to be encoded by returning them as a dictionary '''
def default(self, obj):
return obj.__dict__
class Currency(object):
''' Class to store information about a currency '''
def __init__(self, key, baseKey, rate, name):
''' Initialise a currency object'''
self.key = key # Key to identify the currency (i.e UNITED_STATES_DOLLARS_KEY for United States Dollars)
self.baseKey = baseKey # Key of the base currency that this currency is measured against
self.rate = rate # Value of the currency in relation to the base
self.name = name # String which contains the name of the currency (i.e 'United States Dollars' for UNITED_STATES_DOLLARS_KEY)
def __str__(self):
return "%s (%s) - %f [base is %s]" % (self.name, self.key, self.rate, self.baseKey)
class Money(object):
''' Object to represent a certain amount of a currency withdrawn from an exchange '''
def __init__(self, amount, currencyKey, exchange):
self.amount = amount # Amount
self.currencyKey = currencyKey # Currency key
self.exchange = exchange # Pointer to the exchange from which the money was withdrawn
def getAmount(self):
''' Returns the amount '''
return self.amount
def setAmount(self, amount):
''' Sets the amount '''
self.amount = amount
def getCurrencyKey(self):
''' Returns the currency key '''
return self.currencyKey
def getExchange(self):
''' Returns the pointer to the exhange that the money was withdrawn from '''
return self.exchange
def convert(self, currencyKey):
''' Takes a given currency key and returns a new Money object of the given currency'''
# Convert the key to uppercase
currencyKey = currencyKey.upper()
# Check if the given currencies are valid
if currencyKey not in self.exchange.currencies.keys():
raise InvalidCurrencyKey("%s was not found in the exchange" % currencyKey)
elif self.getCurrencyKey() not in self.exchange.currencies.keys():
raise InvalidCurrencyKey("%s was not found in the exchange" % self.getCurrencyKey())
# If they are, convert the amount and return the new Money object
else:
# Get the two currency objects
oldCurrency = self.exchange.currencies[self.getCurrencyKey()]
newCurrency = self.exchange.currencies[currencyKey]
# Convert the old amount to the new amount
oldAmount = self.getAmount()
newAmount = oldAmount * (newCurrency.rate / oldCurrency.rate)
# Create the new Money object with the appropriate currency and amount
return Money(newAmount, currencyKey, self.exchange)
def __str__(self):
''' Allows the object to be printed using "print" '''
return "%f %s" % (self.amount, self.currencyKey)
def __add__(self, other):
''' Adds two currencies together. The resulting currency is United States Dollars'''
if isinstance(other, Money):
firstAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
secondAmount = other.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
finalAmount = firstAmount + secondAmount
return Money(finalAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Throw an exception if an amount is given that isn't a Money object
else:
raise TypeError("unsupported operand type(s) for +: '%s' and '%s'" % (type(self), type(other)))
def __sub__(self, other):
''' Subtracts two currencies together. The resulting currency is United States Dollars'''
if isinstance(other, Money):
firstAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
secondAmount = other.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
finalAmount = firstAmount - secondAmount
return Money(finalAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Throw an exception if an amount is given that isn't a Money object
else:
raise TypeError("unsupported operand type(s) for -: '%s' and '%s'" % (type(self), type(other)))
def __mul__(self, other):
''' Multiplies currencies together. The resulting currency is United States Dollars by default'''
if isinstance(other,Money):
firstAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
secondAmount = other.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
finalAmount = firstAmount * secondAmount
return Money(finalAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Allow scalar multiplication
elif isinstance(other,int) or isinstance(other,float):
currentAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
newAmount = currentAmount * other
return Money(newAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Throw an exception if an amount is given that isn't a Money object
else:
raise TypeError("unsupported operand type(s) for *: '%s' and '%s'" % (type(self), type(other)))
# Allow the Money object to come second in scalar multiplication i.e 2 * Money()
__rmul__ = __mul__
def __div__(self, other):
''' Divides two currencies together. The resulting currency is United States Dollars by default'''
if isinstance(other,Money):
firstAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
secondAmount = other.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
finalAmount = firstAmount / secondAmount
return Money(finalAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Allow scalar multiplication
elif isinstance(other,int) or isinstance(other,float):
currentAmount = self.convert(UNITED_STATES_DOLLARS_KEY).getAmount()
newAmount = currentAmount / other
return Money(newAmount, UNITED_STATES_DOLLARS_KEY, self.exchange)
# Throw an exception if an amount is given that isn't a Money object
else:
raise TypeError("unsupported operand type(s) for /: '%s' and '%s'" % (type(self), type(other)))
# Division wrapper for Python3
def __truediv__(self, other):
return self.__div__(other)
class Exchange(object):
''' Object to store currencies and update them with the OpenExchangeRates API'''
def __init__(self, appID):
self.appID = appID # The OpenExchangeRates API key to use
self.filename = appID + '.json' # Filename where the exchange rates are stored
self.currencies = {} # Stores the currencies
self.lastUpdated = None
# Load currencies from a JSON file that is storing the latest versions of the currencies if it exists
fileFound = self.__loadFromFile()
if fileFound:
debug("Currency file found")
# Check timestamp is above threshold
now = datetime.fromtimestamp(time.time())
difference = now - self.lastUpdated
# If the last time the file was updated was greater than the threshold, update the exhange rates
if difference.days > UPDATE_THRESHOLD_DAYS:
debug("Timestamp shows file is %d days old, which is older than %d day, updating the Exchange data..." % (difference.days, UPDATE_THRESHOLD_DAYS))
self.__loadFromOpenExhangeRatesAPI()
self.__saveToFile()
else:
debug("Timestamp shows file is %d days old, which is less than %d day, loading Exchange data from file..." % (difference.days, UPDATE_THRESHOLD_DAYS))
else: # No currency file found
# If the internet is connected, download the exhange rates and save them to a file
if internet_on():
debug("Internet connection found!")
self.__loadFromOpenExhangeRatesAPI()
self.__saveToFile()
else: # No currency file and no internet, thrown an exception
raise AccessDataFailure("No currency file or internet connection avaliable - cannot retreive currency data")
def __saveToFile(self):
''' Saves the current exchange to file '''
debug("Saving data to Exchange file")
# Get the currency data, convert to a JSON string
jsonExchange = json.dumps(self.currencies, cls=SimpleObjectEncoder)
# Remove the last bracket from the json and add on the timestamp
timestamp = json.dumps({'timestamp': time.time()})
jsonExchange = jsonExchange[:-1] + ',' + timestamp[1:]
# Write exchange data to a file
with open(self.filename, 'w') as f:
f.write(jsonExchange)
f.close()
def __loadFromFile(self):
''' Loads the latest currency data from the input file '''
debug("Loading data from Exchange file")
# Keep track of wether the file exists or not
fileFound = True
try:
# If the file exists open it
with open(self.filename, 'r') as f:
# Read the lines of the files and concatenate them into a single string
lines = f.readlines()
jsonExchange = ''.join(lines)
# Decode the JSON into a dictionary
decodedFile = json.loads(jsonExchange)
# Convert the dictionary items to currency objects
for key in decodedFile.keys():
# Check if the item isn't the timestamp
if key != 'timestamp':
# Get the parameters for the currency from the file
rate = float(decodedFile[key]['rate'])
name = str(decodedFile[key]['name'])
baseKey = str(decodedFile[key]['baseKey'])
# Add the currency to the Exchange
self.currencies[key] = Currency(key, baseKey, rate, name)
else: # It's the timestamp of the file
self.lastUpdated = datetime.fromtimestamp(decodedFile[key])
except IOError:
# Otherwise mark that the file wasn't found
fileFound = False
return fileFound
def __loadFromOpenExhangeRatesAPI(self):
debug("Loading data from Open Exchange Rates API")
# Access variables from API
API_KEY = '?app_id=' + self.appID
# Get the latest currency rates from the API
try:
# Get the latest rates
latestValues = urlopen(BASE_URL + GET_LATEST + API_KEY)
latestValuesJSON = latestValues.read().decode("utf-8")
latestValuesDecoded = json.loads(latestValuesJSON)
# Get the names for the rates
currencyNames = urlopen(BASE_URL + GET_NAMES + API_KEY)
currencyNamesJSON = currencyNames.read().decode("utf-8")
currencyNamesDecoded = json.loads(currencyNamesJSON)
# Extract the base curreny name
baseKey = latestValuesDecoded['base']
# Iterate over the currency keys
for key in latestValuesDecoded['rates'].keys():
# Get the name and rate of each currency code
rate = float(latestValuesDecoded['rates'][key])
name = str(currencyNamesDecoded[key])
# Create a currency object for each currency, then store it in the exchange
currency = Currency(key, baseKey, rate, name)
self.currencies[key] = currency
# Add the current timestamp to the currency dictionary so we know when it was updated
self.lastUpdated = time.time()
# Handle bad app ID keys
except HTTPError as e:
if e.code == 401:
raise BadAppID("%s is an invalid app ID for openexchangerates.org" % self.appID)
def withdraw(self, amount, currencyKey):
''' Creates a money object that points to this exchange with the given amount of the given currency '''
# Check the amount is a numeric value
if not issubclass(amount.__class__, numbers.Number) or isinstance(amount, bool):
raise TypeError('Amount must be of type int, float or long not %s' % type(amount))
# Check the currency key is a string
if not (isinstance(currencyKey, str)):
raise TypeError('Currency key must be of type str not %s' % type(currencyKey))
# Convert the key to uppercase
currencyKey = currencyKey.upper()
# Check if the given currency are valid
if currencyKey not in self.currencies.keys():
raise InvalidCurrencyKey("%s was not found in the exchange" % currencyKey)
# If it is return the Money requested
else:
return Money(amount, currencyKey, self)