diff --git a/README.md b/README.md index 819345c..298726b 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,108 @@ -forex-python -============ -[![Build Status](https://travis-ci.org/MicroPyramid/forex-python.svg?branch=master)](https://travis-ci.org/MicroPyramid/forex-python) -[![Coverage Status](https://coveralls.io/repos/github/MicroPyramid/forex-python/badge.svg?branch=master)](https://coveralls.io/github/MicroPyramid/forex-python?branch=master) -[![Python Support](https://img.shields.io/badge/python-2.7%2C%203.3%2C%203.4%2C%203.5-blue.svg)](https://pypi.python.org/pypi/forex-python) -[![license](https://img.shields.io/github/license/MicroPyramid/forex-python.svg?maxAge=2592000)](https://pypi.python.org/pypi/forex-python) -[![Code Health](https://landscape.io/github/MicroPyramid/forex-python/master/landscape.svg?style=plastic)](https://landscape.io/github/MicroPyramid/forex-python/master) - -Free Foreign exchange rates, bitcoin prices and currency conversion. - -Features: ---------- -- List all currency rates. -- BitCoin price for all curuncies. -- Converting amount to BitCoins. -- Get historical rates for any day since 1999. -- Conversion rate for one currency(ex; USD to INR). -- Convert amount from one currency to other.('USD 10$' to INR). -- Currency symbols. -- Currency names. - -Currency Source: ---------------- -Fixer.io is a free API for current and historical foreign exchange rates published by European Central Bank. -The rates are updated daily 3PM CET. - -BitCoin Price Source: ---------------------- -Bitcoin prices calculated every minute. For more infomation visit [CoinDesk API](http://www.coindesk.com/api/). - -Installation: ------------- - -Install using python package -``` -$ pip install forex-python -``` +# forex-python + +[![travis-ci](https://travis-ci.org/MicroPyramid/forex-python.svg?branch=master)](https://travis-ci.org/MicroPyramid/forex-python) +[![coveralls](https://coveralls.io/repos/github/MicroPyramid/forex-python/badge.svg?branch=master)](https://coveralls.io/github/MicroPyramid/forex-python?branch=master) +[![Code Health](https://landscape.io/github/MicroPyramid/forex-python/master/landscape.svg?style=flat)](https://landscape.io/github/MicroPyramid/forex-python/master) +[![pypi](https://img.shields.io/badge/python-3.6%2B-blue.svg)](https://pypi.python.org/pypi/forex-python) + +**Forex Python** is a free library for foreign exchange rates and currency conversion, supporting Python 3.6 and above. + +> **Note:** Install the latest version (`forex-python>=1.6`) to avoid `RatesNotAvailableError`. + +## Features + +- List all currency rates +- Bitcoin price for all currencies +- Convert amounts to and from Bitcoin +- Get historical rates (since 1999) +- Currency conversion (e.g., USD to INR) +- Currency symbols and names + +## Currency Source + +[theratesapi.com](https://theratesapi.com) provides current and historical foreign exchange rates published by the European Central Bank. Rates are updated daily at 3PM CET. -Or directly cloning the repo: +## Bitcoin Price Source + +Bitcoin prices are updated every minute. For more information, visit [CoinDesk](http://www.coindesk.com). + +## Installation + +Install via pip: +```bash +pip install forex-python ``` -$ python setup.py install +Or clone the repository and install manually: +```bash +git clone https://github.com/MicroPyramid/forex-python.git +cd forex-python +python3 setup.py install ``` -Usage Examples: ------------------- +## Usage Examples -Initialize class +**Initialize the class:** ```python ->>> from forex_python.converter import CurrencyRates ->>> c = CurrencyRates() +from forex_python.converter import CurrencyRates +c = CurrencyRates() ``` -list all latest currency rates for "USD" +**List all latest currency rates for "USD":** ```python ->>> c.get_rates('USD') -{u'IDR': 13625.0, u'BGN': 1.7433, u'ILS': 3.8794, u'GBP': 0.68641, u'DKK': 6.6289, u'CAD': 1.3106, u'JPY': 110.36, u'HUF': 282.36, u'RON': 4.0162, u'MYR': 4.081, u'SEK': 8.3419, u'SGD': 1.3815, u'HKD': 7.7673, u'AUD': 1.3833, u'CHF': 0.99144, u'KRW': 1187.3, u'CNY': 6.5475, u'TRY': 2.9839, u'HRK': 6.6731, u'NZD': 1.4777, u'THB': 35.73, u'EUR': 0.89135, u'NOK': 8.3212, u'RUB': 66.774, u'INR': 67.473, u'MXN': 18.41, u'CZK': 24.089, u'BRL': 3.5473, u'PLN': 3.94, u'PHP': 46.775, u'ZAR': 15.747} +c.get_rates('USD') +# Example output: {'INR': 83.12, 'EUR': 0.92, ...} ``` -Get conversion rate from USD to INR +**Get conversion rate from USD to INR:** ```python ->>> c.get_rate('USD', 'INR') -67.473 +c.get_rate('USD', 'INR') +# Example output: 83.12 ``` -Convert amount from USD to INR: +**Convert amount from USD to INR:** ```python ->>> c.convert('USD', 'INR', 10) -674.73 +c.convert('USD', 'INR', 10) +# Example output: 831.2 ``` -Get latest Bitcoin price. +**Force use of Decimal:** ```python ->>> from forex_python.bitcoin import BtcConverter ->>> b = BtcConverter() ->>> b.get_latest_price('USD') -533.913 +from decimal import Decimal +c = CurrencyRates(force_decimal=True) +c.convert('USD', 'INR', Decimal('10.45')) +# Example output: 868.75 ``` -Convert Amount to Bitcoins based on latest exchange price. +**Get latest Bitcoin price:** ```python ->>> b.convert_to_btc(400, 'USD') -0.7492699301118473 +from forex_python.bitcoin import BtcConverter +b = BtcConverter() +b.get_latest_price('USD') +# Example output: 67000.0 ``` -Get currency symbol using currency code +**Convert amount to Bitcoins:** ```python ->>> from forex_python.converter import CurrencyCodes ->>> c = CurrencyCodes() ->>> print c.get_symbol('GBP') -£ +b.convert_to_btc(400, 'USD') +# Example output: 0.00597 ``` -Complete [Documentation](http://forex-python.readthedocs.org/en/latest/?badge=latest) +**Get currency symbol using currency code:** +```python +from forex_python.converter import CurrencyCodes +codes = CurrencyCodes() +codes.get_symbol('GBP') +# Example output: '£' +``` + +For complete documentation, see the [forex-python docs](http://forex-python.readthedocs.org/en/latest/?badge=latest). + +--- + +## Support & Feedback + +- Found a bug? Please [open a GitHub issue](https://github.com/MicroPyramid/forex-python/issues). +- Need a new feature or custom development? [Contact us here](https://micropyramid.com/contact-us/). +- Visit our [Python Development Services](https://micropyramid.com/python-development-services/) page for more information. -We welcome your feedback and support. found bug raise github issue. Need new features? Contact us at https://micropyramid.com/contact-us/ +--- \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 7f093c9..bc49dd2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = u'forex-python' -copyright = u'2016, MicroPyramid Informatics Pvt. Ltd.' +copyright = u'2021, MicroPyramid Informatics Pvt. Ltd.' author = u'MicroPyramid Informatics Pvt. Ltd.' # The version info for the project you're documenting, acts as replacement for @@ -59,9 +59,9 @@ # built documents. # # The short X.Y version. -version = u'0.3.0' +version = u'1.9.2' # The full version, including alpha/beta/rc tags. -release = u'0.3.0' +release = u'1.9.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/currencysource.rst b/docs/source/currencysource.rst index aeb445c..1c6dbf9 100644 --- a/docs/source/currencysource.rst +++ b/docs/source/currencysource.rst @@ -1,13 +1,13 @@ currency source =============== -fixer.io +https://theratesapi.com -------- -Fixer.io is a free API for current and historical foreign exchange rates published by European Central Bank. The rates are updated daily 3PM CET. +https://theratesapi.com is a free API for current and historical foreign exchange rates published by European Central Bank. The rates are updated daily 3PM CET. List of Supported Currency codes. --------------------------------- -**Updated On 2016-05-22** +**Updated On 2018-05-08** |EUR - Euro Member Countries |IDR - Indonesia Rupiah diff --git a/docs/source/index.rst b/docs/source/index.rst index 2b1ed73..6d42f63 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,17 +1,17 @@ forex-python ============ -Free Foreign exchange rates, bitcoin prices and currency conversion. +Free foreign exchange rates, Bitcoin prices and currency conversion. Features: --------- - List all currency rates. -- BitCoin price for all curuncies. -- Converting amount to BitCoins. +- Bitcoin price for all currencies. +- Converting amount to BitCoin. - Get historical rates for any day since 1999. -- Conversion rate for one currency(ex; USD to INR). -- Convert amount from one currency to other.('USD 10$' to INR) -- Currency Symbols +- Conversion rate for one currency (e.g. USD to INR). +- Convert amount from one currency to another (e.g. 'USD $10' to INR) +- Currency symbols - Currency names Contents: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 88d3b2d..d62607f 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -35,35 +35,52 @@ Currency Rates >>> c.convert('USD', 'INR', 10, date_obj) 585.09 +7. Force use of Decimal:: + >>> from forex_python.converter import CurrencyRates + >>> c = CurrencyRates(force_decimal=True) + >>> c.convert('USD', 'INR', Decimal('10.45')) + Decimal('705.09') + >>> c.convert('USD', 'INR', 10) + DecimalFloatMismatchError: convert requires amount parameter is of type Decimal when use_decimal=True + +8. Detect use of Decimal:: + >>> from forex_python.converter import CurrencyRates + >>> c = CurrencyRates() + >>> c.convert('USD', 'INR', Decimal('10.45')) + Decimal('705.09') + >>> c.convert('USD', 'INR', 10) + 674.73 + + Bitcoin Prices: --------------- 1. Get latest price of one Bitcoin:: >>> from forex_python.bitcoin import BtcConverter - >>> b = BtcConverter() + >>> b = BtcConverter() # add "force_decimal=True" parmeter to get Decimal rates >>> b.get_latest_price('EUR') # you can directly call get_latest_price('EUR') 476.5225 # return type float -2. Get price of Bitcoin based on prevois date:: +2. Get price of Bitcoin based on previous date:: >>> date_obj datetime.datetime(2016, 5, 18, 19, 39, 36, 815417) >>> b.get_previous_price('USD', date_obj) # get_previous_price('USD', date_obj) 453.378 -3. Convert Amout to bitcoins:: +3. Convert Amount to Bitcoin:: >>> b.convert_to_btc(5000, 'USD') # convert_to_btc(5000, 'USD') 9.36345369116708 -4. Convert Amount to bitcoins based on previous date prices:: +4. Convert Amount to Bitcoin based on previous date prices:: >>> date_obj datetime.datetime(2016, 5, 18, 19, 39, 36, 815417) >>> b.convert_to_btc_on(5000, 'USD', date_obj) # convert_to_btc_on(5000, 'USD', date_obj) 11.028325150316071 -5. Convert Bitcoins to valid currency amount based on lates price:: +5. Convert Bitcoin to valid currency amount based on latest price:: >>> b.convert_btc_to_cur(1.25, 'USD') # convert_btc_to_cur(1.25, 'USD') 668.1012499999999 -6. Convert Bitcoins to valid currency amount based on previous date price:: +6. Convert Bitcoin to valid currency amount based on previous date price:: >>> date_obj datetime.datetime(2016, 5, 18, 19, 39, 36, 815417) >>> b.convert_btc_to_cur_on(1.25, 'EUR', date_obj) @@ -77,13 +94,19 @@ Bitcoin Prices: >>> b.get_previous_price_list('INR', start_date, end_date) # get_previous_price_list('INR', start_date, end_date) {u'2016-05-19': 29371.7579, u'2016-05-18': 30402.3169, u'2016-05-22': 29586.3631, u'2016-05-23': 29925.3272, u'2016-05-20': 29864.0256, u'2016-05-21': 29884.7449} -8. Get Bitcoin symbol:: +8. Force use of Decimal:: + >>> from forex_python.bitcoin import BtcConverter + >>> b = BtcConverter(force_decimal=True) + >>> b.get_latest_price('EUR') # you can directly call get_latest_price('EUR') + Decimal('942.245000000000004547') # return type Decimal + +9. Get Bitcoin symbol:: >>> print(b.get_symbol()) # get_btc_symbol() ฿ Currency Symbols & Codes ------------------------- -1. Get Currency symbol Using currency code:: +1. Get currency symbol using currency code:: >>> from forex_python.converter import CurrencyCodes >>> c = CurrencyCodes() >>> c.get_symbol('GBP') @@ -93,11 +116,8 @@ Currency Symbols & Codes >>> print c.get_symbol('EUR') € -2. Get Currency Name using currency code:: +2. Get currency name using currency code:: >>> c.get_currency_name('EUR') u'European Euro' >>> c.get_currency_name('INR') - u'Indian rupee' - - - + u'Indian Rupee' diff --git a/forex_python/bitcoin.py b/forex_python/bitcoin.py index 0da234b..b699a4d 100644 --- a/forex_python/bitcoin.py +++ b/forex_python/bitcoin.py @@ -1,26 +1,40 @@ +from decimal import Decimal +import simplejson as json import requests +from .converter import RatesNotAvailableError, DecimalFloatMismatchError class BtcConverter(object): """ - Get bit coin rates and convertion + Get Bitcoin rates and conversion """ + def __init__(self, force_decimal=False): + self._force_decimal = force_decimal + + def _decode_rates(self, response, use_decimal=False): + if self._force_decimal or use_decimal: + decoded_data = json.loads(response.text, use_decimal=True) + else: + decoded_data = response.json() + return decoded_data def get_latest_price(self, currency): """ - Get Lates price of one bitcoin to valid Currency 1BTC => X USD + Get latest price of one Bitcoin to valid currency 1BTC => X USD """ url = 'https://api.coindesk.com/v1/bpi/currentprice/{}.json'.format(currency) response = requests.get(url) if response.status_code == 200: data = response.json() price = data.get('bpi').get(currency, {}).get('rate_float', None) + if self._force_decimal: + return Decimal(price) return price return None def get_previous_price(self, currency, date_obj): """ - Get Price for one bit coin on given date + Get price for one Bitcoin on given date """ start = date_obj.strftime('%Y-%m-%d') end = date_obj.strftime('%Y-%m-%d') @@ -34,12 +48,14 @@ def get_previous_price(self, currency, date_obj): if response.status_code == 200: data = response.json() price = data.get('bpi', {}).get(start, None) + if self._force_decimal: + return Decimal(price) return price - return None + raise RatesNotAvailableError("BitCoin Rates Source Not Ready For Given date") def get_previous_price_list(self, currency, start_date, end_date): """ - Get List of prices between two dates + Get list of Bitcoin prices between two dates """ start = start_date.strftime('%Y-%m-%d') end = end_date.strftime('%Y-%m-%d') @@ -51,45 +67,68 @@ def get_previous_price_list(self, currency, start_date, end_date): ) response = requests.get(url) if response.status_code == 200: - data = response.json() + data = self._decode_rates(response) price_dict = data.get('bpi', {}) return price_dict return {} def convert_to_btc(self, amount, currency): """ - Convert X amount to Bit Coins + Convert X amount to Bitcoin """ + if isinstance(amount, Decimal): + use_decimal = True + else: + use_decimal = self._force_decimal + url = 'https://api.coindesk.com/v1/bpi/currentprice/{}.json'.format(currency) response = requests.get(url) if response.status_code == 200: data = response.json() price = data.get('bpi').get(currency, {}).get('rate_float', None) if price: - converted_btc = amount/price - return converted_btc - return None - return None + if use_decimal: + price = Decimal(price) + try: + converted_btc = amount/price + return converted_btc + except TypeError: + raise DecimalFloatMismatchError("convert_to_btc requires amount parameter is of type Decimal when force_decimal=True") + raise RatesNotAvailableError("BitCoin Rates Source Not Ready For Given date") def convert_btc_to_cur(self, coins, currency): """ - Convert X bit coins to valid currency amount + Convert X Bitcoin to valid currency amount """ + if isinstance(coins, Decimal): + use_decimal = True + else: + use_decimal = self._force_decimal + url = 'https://api.coindesk.com/v1/bpi/currentprice/{}.json'.format(currency) response = requests.get(url) if response.status_code == 200: data = response.json() price = data.get('bpi').get(currency, {}).get('rate_float', None) if price: - converted_amount = coins * price - return converted_amount - return None - return None + if use_decimal: + price = Decimal(price) + try: + converted_amount = coins * price + return converted_amount + except TypeError: + raise DecimalFloatMismatchError("convert_btc_to_cur requires coins parameter is of type Decimal when force_decimal=True") + raise RatesNotAvailableError("BitCoin Rates Source Not Ready For Given date") def convert_to_btc_on(self, amount, currency, date_obj): """ - Convert X amount to BTC based on given date rate + Convert X amount to Bitcoin based on a given date's rate """ + if isinstance(amount, Decimal): + use_decimal = True + else: + use_decimal = self._force_decimal + start = date_obj.strftime('%Y-%m-%d') end = date_obj.strftime('%Y-%m-%d') url = ( @@ -103,15 +142,24 @@ def convert_to_btc_on(self, amount, currency, date_obj): data = response.json() price = data.get('bpi', {}).get(start, None) if price: - converted_btc = amount/price - return converted_btc - return None - return None + if use_decimal: + price = Decimal(price) + try: + converted_btc = amount/price + return converted_btc + except TypeError: + raise DecimalFloatMismatchError("convert_to_btc_on requires amount parameter is of type Decimal when force_decimal=True") + raise RatesNotAvailableError("Bitcoin rates source not ready for given date") def convert_btc_to_cur_on(self, coins, currency, date_obj): """ - Convert X BTC to valid currency amount based on given date + Convert X Bitcoin to valid currency amount based on given's date rate """ + if isinstance(coins, Decimal): + use_decimal = True + else: + use_decimal = self._force_decimal + start = date_obj.strftime('%Y-%m-%d') end = date_obj.strftime('%Y-%m-%d') url = ( @@ -125,14 +173,18 @@ def convert_btc_to_cur_on(self, coins, currency, date_obj): data = response.json() price = data.get('bpi', {}).get(start, None) if price: - converted_btc = coins*price - return converted_btc - return None - return None + if use_decimal: + price = Decimal(price) + try: + converted_btc = coins*price + return converted_btc + except TypeError: + raise DecimalFloatMismatchError("convert_btc_to_cur_on requires amount parameter is of type Decimal when force_decimal=True") + raise RatesNotAvailableError("Bitcoin rates source not ready for given date") def get_symbol(self): """ - Here is Unicode symbol for bit coin + Here is the Unicode symbol for Bitcoin: """ return "\u0E3F" diff --git a/forex_python/converter.py b/forex_python/converter.py index 36a57fe..d0c4f7a 100644 --- a/forex_python/converter.py +++ b/forex_python/converter.py @@ -1,22 +1,31 @@ import os -import json +from decimal import Decimal + import requests +import simplejson as json class RatesNotAvailableError(Exception): """ - Custome Exception when http://fixer.io/ is Down are not available for currency rates + Custom exception when https://theratesapi.com is down and not available for currency rates + """ + pass + + +class DecimalFloatMismatchError(Exception): + """ + A float has been supplied when force_decimal was set to True """ pass class Common: - def __init__(self): - pass + def __init__(self, force_decimal=False): + self._force_decimal = force_decimal def _source_url(self): - return "http://api.fixer.io/" + return "https://theratesapi.com/api/" def _get_date_string(self, date_obj): if date_obj is None: @@ -24,26 +33,45 @@ def _get_date_string(self, date_obj): date_str = date_obj.strftime('%Y-%m-%d') return date_str + def _decode_rates(self, response, use_decimal=False, date_str=None): + if self._force_decimal or use_decimal: + decoded_data = json.loads(response.text, use_decimal=True) + else: + decoded_data = response.json() + # if (date_str and date_str != 'latest' and date_str != decoded_data.get('date')): + # raise RatesNotAvailableError("Currency Rates Source Not Ready") + return decoded_data.get('rates', {}) + + def _get_decoded_rate( + self, response, dest_cur, use_decimal=False, date_str=None): + return self._decode_rates( + response, use_decimal=use_decimal, date_str=date_str).get( + dest_cur, None) + class CurrencyRates(Common): def get_rates(self, base_cur, date_obj=None): date_str = self._get_date_string(date_obj) - payload = {'base': base_cur} + payload = {'base': base_cur, 'rtype': 'fpy'} source_url = self._source_url() + date_str response = requests.get(source_url, params=payload) if response.status_code == 200: - rates = response.json().get('rates', {}) + rates = self._decode_rates(response, date_str=date_str) return rates raise RatesNotAvailableError("Currency Rates Source Not Ready") def get_rate(self, base_cur, dest_cur, date_obj=None): + if base_cur == dest_cur: + if self._force_decimal: + return Decimal(1) + return 1. date_str = self._get_date_string(date_obj) - payload = {'base': base_cur, 'symbols': dest_cur} + payload = {'base': base_cur, 'symbols': dest_cur, 'rtype': 'fpy'} source_url = self._source_url() + date_str response = requests.get(source_url, params=payload) if response.status_code == 200: - rate = response.json().get('rates', {}).get(dest_cur) + rate = self._get_decoded_rate(response, dest_cur, date_str=date_str) if not rate: raise RatesNotAvailableError("Currency Rate {0} => {1} not available for Date {2}".format( base_cur, dest_cur, date_str)) @@ -51,17 +79,35 @@ def get_rate(self, base_cur, dest_cur, date_obj=None): raise RatesNotAvailableError("Currency Rates Source Not Ready") def convert(self, base_cur, dest_cur, amount, date_obj=None): + if isinstance(amount, Decimal): + use_decimal = True + else: + use_decimal = self._force_decimal + + if base_cur == dest_cur: # Return same amount if both base_cur, dest_cur are same + if use_decimal: + return Decimal(amount) + return float(amount) + date_str = self._get_date_string(date_obj) - payload = {'base': base_cur, 'symbols': dest_cur} + payload = {'base': base_cur, 'symbols': dest_cur, 'rtype': 'fpy'} source_url = self._source_url() + date_str response = requests.get(source_url, params=payload) if response.status_code == 200: - rate = response.json().get('rates', {}).get(dest_cur, None) + rate = self._get_decoded_rate( + response, dest_cur, use_decimal=use_decimal, date_str=date_str) if not rate: raise RatesNotAvailableError("Currency {0} => {1} rate not available for Date {2}.".format( source_url, dest_cur, date_str)) - converted_amount = rate * amount - return converted_amount + # Ensure rate is numeric + if isinstance(rate, str): + rate = Decimal(rate) if use_decimal else float(rate) + try: + converted_amount = rate * amount + return converted_amount + except TypeError: + raise DecimalFloatMismatchError( + "convert requires amount parameter is of type Decimal when force_decimal=True") raise RatesNotAvailableError("Currency Rates Source Not Ready") @@ -75,13 +121,22 @@ def convert(self, base_cur, dest_cur, amount, date_obj=None): class CurrencyCodes: def __init__(self): - pass + self.__currency_data = None + + @property + def _currency_data(self): + if self.__currency_data is None: + file_path = os.path.dirname(os.path.abspath(__file__)) + with open(file_path + '/raw_data/currencies.json', encoding="utf-8") as f: + self.__currency_data = json.loads(f.read()) + return self.__currency_data def _get_data(self, currency_code): - file_path = os.path.dirname(os.path.abspath(__file__)) - with open(file_path+'/raw_data/currencies.json') as f: - currency_data = json.loads(f.read()) - currency_dict = next((item for item in currency_data if item["cc"] == currency_code), None) + currency_dict = next((item for item in self._currency_data if item["cc"] == currency_code), None) + return currency_dict + + def _get_data_from_symbol(self, symbol): + currency_dict = next((item for item in self._currency_data if item["symbol"] == symbol), None) return currency_dict def get_symbol(self, currency_code): @@ -96,8 +151,15 @@ def get_currency_name(self, currency_code): return currency_dict.get('name') return None -_CURRENCY_CODES = CurrencyCodes() + def get_currency_code_from_symbol(self, symbol): + currency_dict = self._get_data_from_symbol(symbol) + if currency_dict: + return currency_dict.get('cc') + return None +_CURRENCY_CODES = CurrencyCodes() + get_symbol = _CURRENCY_CODES.get_symbol get_currency_name = _CURRENCY_CODES.get_currency_name +get_currency_code_from_symbol = _CURRENCY_CODES.get_currency_code_from_symbol diff --git a/forex_python/raw_data/currencies.json b/forex_python/raw_data/currencies.json index aee370b..28baca0 100644 --- a/forex_python/raw_data/currencies.json +++ b/forex_python/raw_data/currencies.json @@ -20,6 +20,7 @@ {"cc":"BOB","symbol":"Bs.","name":"Bolivian boliviano"}, {"cc":"BRL","symbol":"R$","name":"Brazilian real"}, {"cc":"BSD","symbol":"B$","name":"Bahamian dollar"}, + {"cc":"BTC","symbol":"\u20bf","name":"Bitcoin"}, {"cc":"BTN","symbol":"Nu.","name":"Bhutanese ngultrum"}, {"cc":"BWP","symbol":"P","name":"Botswana pula"}, {"cc":"BYR","symbol":"Br","name":"Belarusian ruble"}, @@ -98,7 +99,7 @@ {"cc":"MWK","symbol":"MK","name":"Malawian kwacha"}, {"cc":"MXN","symbol":"$","name":"Mexican peso"}, {"cc":"MYR","symbol":"RM","name":"Malaysian ringgit"}, - {"cc":"MZM","symbol":"MTn","name":"Mozambican metical"}, + {"cc":"MZN","symbol":"MT","name":"Mozambican metical"}, {"cc":"NAD","symbol":"N$","name":"Namibian dollar"}, {"cc":"NGN","symbol":"\u20a6","name":"Nigerian naira"}, {"cc":"NIO","symbol":"C$","name":"Nicaraguan c\u00f3rdoba"}, @@ -116,7 +117,8 @@ {"cc":"QAR","symbol":"QR","name":"Qatari riyal"}, {"cc":"RON","symbol":"L","name":"Romanian leu"}, {"cc":"RSD","symbol":"din.","name":"Serbian dinar"}, - {"cc":"RUB","symbol":"R","name":"Russian ruble"}, + {"cc":"RUB","symbol":"\u20bd","name":"Russian ruble"}, + {"cc":"RWF","symbol":"FRw","name":"Rwandan franc"}, {"cc":"SAR","symbol":"SR","name":"Saudi riyal"}, {"cc":"SBD","symbol":"SI$","name":"Solomon Islands dollar"}, {"cc":"SCR","symbol":"SR","name":"Seychellois rupee"}, @@ -127,19 +129,22 @@ {"cc":"SLL","symbol":"Le","name":"Sierra Leonean leone"}, {"cc":"SOS","symbol":"Sh.","name":"Somali shilling"}, {"cc":"SRD","symbol":"$","name":"Surinamese dollar"}, + {"cc":"STD","symbol":"Db","name":"São Tomé and Príncipe dobra"}, + {"cc":"STN","symbol":"Db","name":"São Tomé and Príncipe dobra"}, {"cc":"SYP","symbol":"LS","name":"Syrian pound"}, {"cc":"SZL","symbol":"E","name":"Swazi lilangeni"}, {"cc":"THB","symbol":"\u0e3f","name":"Thai baht"}, {"cc":"TJS","symbol":"TJS","name":"Tajikistani somoni"}, {"cc":"TMT","symbol":"m","name":"Turkmen manat"}, {"cc":"TND","symbol":"DT","name":"Tunisian dinar"}, + {"cc":"TOP","symbol":"T$","name":"Tongan Pa'anga"}, {"cc":"TRY","symbol":"TRY","name":"Turkish new lira"}, {"cc":"TTD","symbol":"TT$","name":"Trinidad and Tobago dollar"}, {"cc":"TWD","symbol":"NT$","name":"New Taiwan dollar"}, {"cc":"TZS","symbol":"TZS","name":"Tanzanian shilling"}, {"cc":"UAH","symbol":"UAH","name":"Ukrainian hryvnia"}, {"cc":"UGX","symbol":"USh","name":"Ugandan shilling"}, - {"cc":"USD","symbol":"US$","name":"United States dollar"}, + {"cc":"USD","symbol":"$","name":"United States dollar"}, {"cc":"UYU","symbol":"$U","name":"Uruguayan peso"}, {"cc":"UZS","symbol":"UZS","name":"Uzbekistani som"}, {"cc":"VEB","symbol":"Bs","name":"Venezuelan bolivar"}, @@ -153,6 +158,6 @@ {"cc":"XPF","symbol":"F","name":"CFP franc"}, {"cc":"YER","symbol":"YER","name":"Yemeni rial"}, {"cc":"ZAR","symbol":"R","name":"South African rand"}, - {"cc":"ZMK","symbol":"ZK","name":"Zambian kwacha"}, + {"cc":"ZMW","symbol":"ZK","name":"Zambian kwacha"}, {"cc":"ZWR","symbol":"Z$","name":"Zimbabwean dollar"} -] \ No newline at end of file +] diff --git a/setup.py b/setup.py index a0c91e9..34dc559 100644 --- a/setup.py +++ b/setup.py @@ -2,35 +2,51 @@ import os from setuptools import setup, find_packages -VERSION = '0.3.0' - -with io.open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8', errors='ignore') as readme: - LONG_DESCRIPTION = readme.read() +# 1) Bump this version each release: +VERSION = '1.9.2' +# 2) Pull your README.md in as long_description: +here = os.path.abspath(os.path.dirname(__file__)) +with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() setup( name='forex-python', version=VERSION, author='Micro Pyramid Informatic Pvt. Ltd.', author_email='hello@micropyramid.com', + description='Free foreign exchange rates and currency conversion.', + long_description=long_description, + long_description_content_type='text/markdown', # so PyPI renders your README properly url='https://github.com/MicroPyramid/forex-python', - description='Foreign exchange rates and currency conversion.', - long_description=LONG_DESCRIPTION, - packages=find_packages(exclude=['tests', 'tests.*']), + packages=find_packages(exclude=['tests*']), include_package_data=True, install_requires=[ - 'requests', + 'requests>=2.0', + 'simplejson>=3.0', ], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <4', classifiers=[ + # audience 'Intended Audience :: Developers', - 'Operating System :: OS Independent', + 'Topic :: Software Development :: Internationalization', + # license 'License :: OSI Approved :: MIT License', + # OS + 'Operating System :: OS Independent', + # languages 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Topic :: Software Development :: Internationalization', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], + project_urls={ # these show up as “Project Links” on PyPI + 'Documentation': 'https://forex-python.readthedocs.io/', + 'Source': 'https://github.com/MicroPyramid/forex-python', + 'Tracker': 'https://github.com/MicroPyramid/forex-python/issues', + }, ) diff --git a/tests/test.py b/tests/test.py index c99e8e9..4add1db 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,6 +1,9 @@ import datetime +from decimal import Decimal from unittest import TestCase -from forex_python.converter import get_rates, get_rate, convert, get_symbol, get_currency_name, RatesNotAvailableError +from forex_python.converter import (get_rates, get_rate, convert, get_symbol, + get_currency_name, RatesNotAvailableError, + CurrencyRates, DecimalFloatMismatchError) class TestGetRates(TestCase): @@ -36,6 +39,10 @@ def test_get_rates_with_date(self): def test_get_rates_invalid_code(self): self.assertRaises(RatesNotAvailableError, get_rates, 'XYZ') + def test_get_rates_in_future(self): + future = datetime.date.today() + datetime.timedelta(days=1) + self.assertRaises(RatesNotAvailableError, get_rates, 'USD', future) + class TestGetRate(TestCase): """ @@ -47,6 +54,11 @@ def test_get_rate_with_valid_codes(self): # check if return value is float self.assertTrue(isinstance(rate, float)) + + def test_get_rate_with_valid_codes_same_currency(self): + rate = get_rate('USD', 'USD') + # rate should be 1. + self.assertEqual(1., rate) def test_get_rate_with_date(self): date_obj = datetime.datetime.strptime('2010-05-10', "%Y-%m-%d").date() @@ -59,6 +71,11 @@ def test_get_rate_with_invalid_codes(self): # raise exception for invalid currency codes self.assertRaises(RatesNotAvailableError, get_rate, 'ABCD', 'XYZ') + def test_get_rates_in_future(self): + future = datetime.date.today() + datetime.timedelta(days=1) + self.assertRaises( + RatesNotAvailableError, get_rate, 'EUR', 'USD', future) + class TestAmountConvert(TestCase): """ @@ -71,6 +88,11 @@ def test_amount_convert_valid_currency(self): # test if amount returned in float self.assertTrue(isinstance(amount, float)) + def test_amount_convert_valid_currency_same_currency(self): + amount = convert('USD', 'USD', 10) + self.assertEqual(amount, float(10)) + + def test_amount_convert_date(self): date_obj = datetime.datetime.strptime('2010-05-10', "%Y-%m-%d").date() amount = convert('USD', 'INR', 10, date_obj) @@ -83,6 +105,75 @@ def test_amount_convert_invalid_currency(self): self.assertRaises(RatesNotAvailableError, convert, 'ABC', 'XYZ', 10) +class TestForceDecimalAmountConvert(TestCase): + """ + Test the force_decimal=True type enforcing + """ + + def setUp(self): + self.c = CurrencyRates(force_decimal=True) + + def test_amount_decimal_convert(self): + amount = self.c.convert('USD', 'INR', Decimal('10.45')) + self.assertTrue(isinstance(amount, Decimal)) + + def test_amount_decimal_convert_same_currency(self): + amount = self.c.convert('USD', 'USD', Decimal('10.45')) + self.assertEqual(amount, Decimal('10.45')) + + def test_amount_decimal_convert_date(self): + date_obj = datetime.datetime.strptime('2010-05-10', "%Y-%m-%d").date() + amount = self.c.convert('USD', 'INR', Decimal('10.45'), date_obj) + + self.assertTrue(isinstance(amount, Decimal)) + + def test_amount_decimal_invalid_type(self): + self.assertRaises(DecimalFloatMismatchError, self.c.convert, 'USD', 'INR', 10.45) + + def test_decimal_get_rates_valid_code(self): + all_rates = self.c.get_rates('USD') + # Check if return value of get_rates dictionary + self.assertTrue(isinstance(all_rates, dict)) + # Test at least one rate value returned + self.assertTrue(len(all_rates.keys())) + # Test one rate in returned dict is now a Decimal + self.assertTrue(isinstance(all_rates.get('INR'), Decimal)) + + def test_decimal_get_rates_with_date(self): + date_obj = datetime.datetime.strptime('2010-05-10', "%Y-%m-%d").date() + all_rates = self.c.get_rates('USD', date_obj) + # Check if return value of get_rates dictionary + self.assertTrue(isinstance(all_rates, dict)) + # Test at least one rate value returned + self.assertTrue(len(all_rates.keys())) + # Test one rate in returned dict is now a Decimal + self.assertTrue(isinstance(all_rates.get('INR'), Decimal)) + + def test_decimal_get_rates_invalid_code(self): + self.assertRaises(RatesNotAvailableError, self.c.get_rates, 'XYZ') + + def test_decimal_get_rate_with_valid_codes(self): + rate = self.c.get_rate('USD', 'INR') + # check if return value is Decimal + self.assertTrue(isinstance(rate, Decimal)) + + def test_decimal_get_rate_with_valid_same_codes(self): + rate = self.c.get_rate('USD', 'USD') + # check if return value is Decimal + self.assertEqual(rate, Decimal(1)) + + + def test_decimal_get_rate_with_date(self): + date_obj = datetime.datetime.strptime('2010-05-10', "%Y-%m-%d").date() + rate = self.c.get_rate('USD', 'INR', date_obj) + # check if return value is Decimal + self.assertTrue(isinstance(rate, Decimal)) + + def test_decimal_get_rate_with_invalid_codes(self): + # raise exception for invalid currency codes + self.assertRaises(RatesNotAvailableError, self.c.get_rate, 'ABCD', 'XYZ') + + class TestCurrencySymbol(TestCase): """ test currency symbols from currency codes diff --git a/tests/test_bitcoin.py b/tests/test_bitcoin.py index c6cd341..f2eccfd 100644 --- a/tests/test_bitcoin.py +++ b/tests/test_bitcoin.py @@ -1,16 +1,12 @@ import datetime +from decimal import Decimal from unittest import TestCase -from forex_python.bitcoin import * +from forex_python.bitcoin import (get_btc_symbol, convert_btc_to_cur_on, convert_to_btc_on, + convert_btc_to_cur, convert_to_btc, get_latest_price, + get_previous_price, get_previous_price_list, BtcConverter) +from forex_python.converter import RatesNotAvailableError, DecimalFloatMismatchError -class TestCommon(TestCase): - """ - Common class with setUp method for all test cases - """ - def setUp(self): - self.b = BtcConverter() - - class TestLatestPrice(TestCase): """ Test get latest price using currency code @@ -35,8 +31,7 @@ def test_previous_price_valid_currency(self): def test_previous_price_invalid_currency(self): date_obj = datetime.datetime.today() - datetime.timedelta(days=15) - price = get_previous_price('XYZ', date_obj) - self.assertFalse(price) + self.assertRaises(RatesNotAvailableError, get_previous_price, 'XYZ', date_obj) class TestPreviousPriceList(TestCase): @@ -60,33 +55,31 @@ def test_previous_price_list_with_invalid_currency(self): class TestConvertBtc(TestCase): """ - Test Converting amount to Bit coins + Test converting amount to Bitcoin """ def test_convet_to_btc_with_valid_currency(self): coins = convert_to_btc(250, 'USD') self.assertEqual(type(coins), float) def test_convet_to_btc_with_invalid_currency(self): - coins = convert_to_btc(250, 'XYZ') - self.assertFalse(coins) + self.assertRaises(RatesNotAvailableError, convert_to_btc, 250, 'XYZ') class TestConvertBtcToCur(TestCase): """ - Convert Bit Coins to Valid Currency amount + Convert Bitcoin to valid currency amount """ def test_convert_btc_to_cur_valid_currency(self): amount = convert_btc_to_cur(2, 'USD') self.assertEqual(type(amount), float) def test_convert_btc_to_cur_invalid_currency(self): - amount = convert_btc_to_cur(2, 'XYZ') - self.assertFalse(amount) + self.assertRaises(RatesNotAvailableError, convert_btc_to_cur, 2, 'XYZ') class TestConvertToBtcOn(TestCase): """ - Convert To bit coin based on previous dates + Convert to Bitcoin based on previous date """ def test_convert_to_btc_on_with_valid_currency(self): date_obj = datetime.datetime.today() - datetime.timedelta(days=15) @@ -95,13 +88,12 @@ def test_convert_to_btc_on_with_valid_currency(self): def test_convert_to_btc_on_with_invalid_currency(self): date_obj = datetime.datetime.today() - datetime.timedelta(days=15) - coins = convert_to_btc_on(300, 'XYZ', date_obj) - self.assertFalse(coins) + self.assertRaises(RatesNotAvailableError, convert_to_btc_on, 300, 'XYZ', date_obj) class TestConvertBtcToCurOn(TestCase): """ - Convert BitCoins to valid Currency + Convert Bitcoin to valid currency """ def test_convert_to_btc_on_with_valid_currency(self): date_obj = datetime.datetime.today() - datetime.timedelta(days=15) @@ -110,13 +102,151 @@ def test_convert_to_btc_on_with_valid_currency(self): def test_convert_to_btc_on_with_invalid_currency(self): date_obj = datetime.datetime.today() - datetime.timedelta(days=15) - amount = convert_btc_to_cur_on(3, 'XYZ', date_obj) - self.assertFalse(amount) + self.assertRaises(RatesNotAvailableError, convert_btc_to_cur_on, 3, 'XYZ', date_obj) class TestBitCoinSymbol(TestCase): """ - Bit Coin symbol + Bitcoin symbol """ def test_bitcoin_symbol(self): self.assertEqual(get_btc_symbol(), "\u0E3F") + + +class TestBitCoinWithoutForceDecimal(TestCase): + + def setUp(self): + self.b = BtcConverter() + + def test_latest_price_valid_currency(self): + price = self.b.get_latest_price('USD') + self.assertEqual(type(price), float) + + def test_latest_price_invalid_currency(self): + price = self.b.get_latest_price('XYZ') + self.assertFalse(price) + + def test_previous_price_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + price = self.b.get_previous_price('USD', date_obj) + self.assertEqual(type(price), float) + + def test_previous_price_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.get_previous_price, 'XYZ', date_obj) + + def test_previous_price_list_with_valid_currency(self): + start_date = datetime.datetime.today() - datetime.timedelta(days=15) + end_date = datetime.datetime.today() + price_list = self.b.get_previous_price_list('USD', start_date, end_date) + self.assertTrue(price_list) + self.assertEqual(type(price_list), dict) + + def test_previous_price_list_with_invalid_currency(self): + start_date = datetime.datetime.today() - datetime.timedelta(days=15) + end_date = datetime.datetime.today() + price_list = self.b.get_previous_price_list('XYZ', start_date, end_date) + self.assertFalse(price_list) + self.assertEqual(type(price_list), dict) + + def test_convet_to_btc_with_valid_currency(self): + coins = self.b.convert_to_btc(250, 'USD') + self.assertEqual(type(coins), float) + + def test_convet_to_btc_with_invalid_currency(self): + self.assertRaises(RatesNotAvailableError, self.b.convert_to_btc, 250, 'XYZ') + + def test_convert_btc_to_cur_valid_currency(self): + amount = self.b.convert_btc_to_cur(2, 'USD') + self.assertEqual(type(amount), float) + + def test_convert_btc_to_cur_invalid_currency(self): + self.assertRaises(RatesNotAvailableError, self.b.convert_btc_to_cur, 2, 'XYZ') + + def test_convert_to_btc_on_with_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + coins = self.b.convert_to_btc_on(300, 'USD', date_obj) + self.assertEqual(type(coins), float) + + def test_convert_to_btc_on_with_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.convert_to_btc_on, 300, 'XYZ', date_obj) + + def test_convert_to_btc_on_with_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + amount = self.b.convert_btc_to_cur_on(3, 'USD', date_obj) + self.assertEqual(type(amount), float) + + def test_convert_to_btc_on_with_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.convert_btc_to_cur_on, 3, 'XYZ', date_obj) + + +class TestBitCoinForceDecimal(TestCase): + + def setUp(self): + self.b = BtcConverter(force_decimal=True) + + def test_latest_price_valid_currency(self): + price = self.b.get_latest_price('USD') + self.assertEqual(type(price), Decimal) + + def test_latest_price_invalid_currency(self): + price = self.b.get_latest_price('XYZ') + self.assertFalse(price) + + def test_previous_price_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + price = self.b.get_previous_price('USD', date_obj) + self.assertEqual(type(price), Decimal) + + def test_previous_price_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.get_previous_price, 'XYZ', date_obj) + + def test_previous_price_list_with_valid_currency(self): + start_date = datetime.datetime.today() - datetime.timedelta(days=15) + end_date = datetime.datetime.today() + price_list = self.b.get_previous_price_list('USD', start_date, end_date) + self.assertTrue(price_list) + self.assertEqual(type(price_list), dict) + + def test_previous_price_list_with_invalid_currency(self): + start_date = datetime.datetime.today() - datetime.timedelta(days=15) + end_date = datetime.datetime.today() + price_list = self.b.get_previous_price_list('XYZ', start_date, end_date) + self.assertFalse(price_list) + self.assertEqual(type(price_list), dict) + + def test_convert_to_btc_with_valid_currency(self): + coins = self.b.convert_to_btc(Decimal('250'), 'USD') + self.assertEqual(type(coins), Decimal) + + def test_convert_to_btc_with_invalid_currency(self): + self.assertRaises(RatesNotAvailableError, self.b.convert_to_btc, Decimal('250'), 'XYZ') + + def test_convert_btc_to_cur_valid_currency(self): + amount = self.b.convert_btc_to_cur(Decimal('2'), 'USD') + self.assertEqual(type(amount), Decimal) + + def test_convert_btc_to_cur_invalid_currency(self): + self.assertRaises(RatesNotAvailableError, self.b.convert_btc_to_cur, Decimal('250'), 'XYZ') + + def test_convert_to_btc_on_with_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + coins = self.b.convert_to_btc_on(Decimal('300'), 'USD', date_obj) + self.assertEqual(type(coins), Decimal) + + def test_convert_to_btc_on_with_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.convert_to_btc_on, Decimal('250'), 'XYZ', date_obj) + + def test_convert_to_btc_on_with_valid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + amount = self.b.convert_btc_to_cur_on(Decimal('250'), 'USD', date_obj) + self.assertEqual(type(amount), Decimal) + + def test_convert_to_btc_on_with_invalid_currency(self): + date_obj = datetime.datetime.today() - datetime.timedelta(days=15) + self.assertRaises(RatesNotAvailableError, self.b.convert_btc_to_cur_on, Decimal('3'), 'XYZ', date_obj) +