Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python 3 support and unit tests #539

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
41 changes: 24 additions & 17 deletions lendingbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
import time
import traceback
from decimal import Decimal
from httplib import BadStatusLine
from urllib2 import URLError
try:
from httplib import BadStatusLine
from urllib2 import URLError
except ImportError:
# Python 3
from http.client import BadStatusLine
from urllib.error import URLError

import modules.Configuration as Config
import modules.Data as Data
Expand Down Expand Up @@ -82,7 +87,7 @@
# load plugins
PluginsManager.init(Config, api, log, notify_conf)

print 'Welcome to ' + Config.get("BOT", "label", "Lending Bot") + ' on ' + exchange
print("Welcome to {0} on {1}".format(Config.get("BOT", "label", "Lending Bot"), exchange))

try:
while True:
Expand All @@ -102,37 +107,39 @@
# allow existing the main bot loop
raise
except Exception as ex:
if not hasattr(ex, 'message'):
ex.message = str(ex)
log.log_error(ex.message)
log.persistStatus()
if 'Invalid API key' in ex.message:
print "!!! Troubleshooting !!!"
print "Are your API keys correct? No quotation. Just plain keys."
print("!!! Troubleshooting !!!")
print("Are your API keys correct? No quotation. Just plain keys.")
exit(1)
elif 'Nonce must be greater' in ex.message:
print "!!! Troubleshooting !!!"
print "Are you reusing the API key in multiple applications? Use a unique key for every application."
print("!!! Troubleshooting !!!")
print("Are you reusing the API key in multiple applications? Use a unique key for every application.")
exit(1)
elif 'Permission denied' in ex.message:
print "!!! Troubleshooting !!!"
print "Are you using IP filter on the key? Maybe your IP changed?"
print("!!! Troubleshooting !!!")
print("Are you using IP filter on the key? Maybe your IP changed?")
exit(1)
elif 'timed out' in ex.message:
print "Timed out, will retry in " + str(Lending.get_sleep_time()) + "sec"
print("Timed out, will retry in {0} sec".format(Lending.get_sleep_time()))
elif isinstance(ex, BadStatusLine):
print "Caught BadStatusLine exception from Poloniex, ignoring."
print("Caught BadStatusLine exception from Poloniex, ignoring.")
elif 'Error 429' in ex.message:
additional_sleep = max(130.0-Lending.get_sleep_time(), 0)
additional_sleep = max(130.0 - Lending.get_sleep_time(), 0)
sum_sleep = additional_sleep + Lending.get_sleep_time()
log.log_error('IP has been banned due to many requests. Sleeping for {} seconds'.format(sum_sleep))
time.sleep(additional_sleep)
# Ignore all 5xx errors (server error) as we can't do anything about it (https://httpstatuses.com/)
elif isinstance(ex, URLError):
print "Caught {0} from exchange, ignoring.".format(ex.message)
print("Caught {0} from exchange, ignoring.".format(ex.message))
elif isinstance(ex, ApiError):
print "Caught {0} reading from exchange API, ignoring.".format(ex.message)
print("Caught {0} reading from exchange API, ignoring.".format(ex.message))
else:
print traceback.format_exc()
print "Unhandled error, please open a Github issue so we can fix it!"
print(traceback.format_exc())
print("Unhandled error, please open a Github issue so we can fix it!")
if notify_conf['notify_caught_exception']:
log.notify("{0}\n-------\n{1}".format(ex, traceback.format_exc()), notify_conf)
sys.stdout.flush()
Expand All @@ -144,5 +151,5 @@
WebServer.stop_web_server()
PluginsManager.on_bot_exit()
log.log('bye')
print 'bye'
print('bye')
os._exit(0) # Ad-hoc solution in place of 'exit(0)' TODO: Find out why non-daemon thread(s) are hanging on exit
15 changes: 9 additions & 6 deletions modules/Bitfinex.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def __init__(self, cfg, log):
self.log = log
self.lock = threading.RLock()
self.req_per_min = 60
self.req_period = 15 # seconds
self.req_per_period = int(self.req_per_min / ( 60.0 / self.req_period))
self.req_period = 15 # seconds
self.req_per_period = int(self.req_per_min / (60.0 / self.req_period))
self.req_time_log = RingBuffer(self.req_per_period)
self.url = 'https://api.bitfinex.com'
self.key = self.cfg.get("API", "apikey", None)
Expand Down Expand Up @@ -51,7 +51,7 @@ def limit_request_rate(self):
time_since_oldest_req = now - self.req_time_log[0]
# check if oldest request is more than self.req_period ago
if time_since_oldest_req < self.req_period:
# print self.req_time_log.get()
# print(self.req_time_log.get())
# uncomment to debug
# print("Waiting {0} sec, {1} to keep api request rate".format(self.req_period - time_since_oldest_req,
# threading.current_thread()))
Expand All @@ -61,7 +61,7 @@ def limit_request_rate(self):
return
# uncomment to debug
# else:
# print self.req_time_log.get()
# print(self.req_time_log.get())
# print("Not Waiting {0}".format(threading.current_thread()))
# print("Req:{0} Oldest req:{1} Diff:{2} sec".format(now, self.req_time_log[0], time_since_oldest_req))
# append current request time to the log, pushing out the 60th request time before it
Expand Down Expand Up @@ -103,6 +103,9 @@ def _request(self, method, request, payload=None, verify=True):

return r.json()

except ApiError as ex:
ex.message = "{0} Requesting {1}".format(str(ex), self.url + request)
raise ex
except Exception as ex:
ex.message = ex.message if ex.message else str(ex)
ex.message = "{0} Requesting {1}".format(ex.message, self.url + request)
Expand Down Expand Up @@ -259,7 +262,7 @@ def create_loan_offer(self, currency, amount, duration, auto_renew, lending_rate
payload = {
"currency": currency,
"amount": str(amount),
"rate": str(round(float(lending_rate),10) * 36500),
"rate": str(round(float(lending_rate),10) * 36500),
"period": int(duration),
"direction": "lend"
}
Expand Down Expand Up @@ -346,7 +349,7 @@ def return_lending_history(self, start, stop, limit=500):
"amount": "0.0",
"duration": "0.0",
"interest": str(amount / 0.85),
"fee": str(amount-amount / 0.85),
"fee": str(amount - amount / 0.85),
"earned": str(amount),
"open": Bitfinex2Poloniex.convertTimestamp(entry['timestamp']),
"close": Bitfinex2Poloniex.convertTimestamp(entry['timestamp'])
Expand Down
2 changes: 1 addition & 1 deletion modules/Bitfinex2Poloniex.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def convertOpenLoanOffers(bfxOffers):
if offer['direction'] == 'lend' and float(offer['remaining_amount']) > 0:
plxOffers[offer['currency']].append({
"id": offer['id'],
"rate": str(float(offer['rate'])/36500),
"rate": str(float(offer['rate']) / 36500),
"amount": offer['remaining_amount'],
"duration": offer['period'],
"autoRenew": 0,
Expand Down
43 changes: 26 additions & 17 deletions modules/Configuration.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# coding=utf-8
from ConfigParser import SafeConfigParser
try:
from ConfigParser import SafeConfigParser
except ImportError:
# Python 3
from configparser import SafeConfigParser
import json
import os
from decimal import Decimal
from builtins import input

config = SafeConfigParser()
Data = None
Expand All @@ -19,9 +24,9 @@ def init(file_location, data=None):
# Copy default config file if not found
try:
shutil.copy('default.cfg.example', file_location)
print '\ndefault.cfg.example has been copied to ' + file_location + '\n' \
'Edit it with your API key and custom settings.\n'
raw_input("Press Enter to acknowledge and exit...")
print("\ndefault.cfg.example has been copied to ".format(file_location))
print("Edit it with your API key and custom settings.\n")
input("Press Enter to acknowledge and exit...")
exit(1)
except Exception as ex:
ex.message = ex.message if ex.message else str(ex)
Expand All @@ -33,14 +38,16 @@ def init(file_location, data=None):
def has_option(category, option):
try:
return True if os.environ["{0}_{1}".format(category, option)] else _
except (KeyError, NameError): # KeyError for no env var, NameError for _ (empty var) and then to continue
except (KeyError, NameError): # KeyError for no env var, NameError for _ (empty var) and then to continue
return config.has_option(category, option)


def getboolean(category, option, default_value=False):
if has_option(category, option):
try:
return bool(os.environ["{0}_{1}".format(category, option)])
v = os.environ["{0}_{1}".format(category, option)]
return v.lower() in ['true', '1', 't', 'y', 'yes']

except KeyError:
return config.getboolean(category, option)
else:
Expand All @@ -55,22 +62,24 @@ def get(category, option, default_value=False, lower_limit=False, upper_limit=Fa
value = config.get(category, option)
try:
if lower_limit and float(value) < float(lower_limit):
print "WARN: [%s]-%s's value: '%s' is below the minimum limit: %s, which will be used instead." % \
(category, option, value, lower_limit)
print("WARN: [{0}]-{1}'s value: '{2}' is below the minimum limit: {3}, which will be used instead."
.format(category, option, value, lower_limit))
value = lower_limit
if upper_limit and float(value) > float(upper_limit):
print "WARN: [%s]-%s's value: '%s' is above the maximum limit: %s, which will be used instead." % \
(category, option, value, upper_limit)
print("WARN: [{0}]-{1}'s value: '{2}' is above the maximum limit: {3}, which will be used instead."
.format(category, option, value, upper_limit))
value = upper_limit
return value
except ValueError:
if default_value is None:
print "ERROR: [%s]-%s is not allowed to be left empty. Please check your config." % (category, option)
print("ERROR: [{0}]-{1} is not allowed to be left empty. Please check your config."
.format(category, option))
exit(1)
return default_value
else:
if default_value is None:
print "ERROR: [%s]-%s is not allowed to be left unset. Please check your config." % (category, option)
print("ERROR: [{0}]-{1} is not allowed to be left unset. Please check your config."
.format(category, option))
exit(1)
return default_value

Expand Down Expand Up @@ -162,8 +171,8 @@ def get_gap_mode(category, option):
full_list = ['raw', 'rawbtc', 'relative']
value = get(category, 'gapmode', False).lower().strip(" ")
if value not in full_list:
print "ERROR: Invalid entry '%s' for [%s]-gapMode. Please check your config. Allowed values are: %s" % \
(value, category, ", ".join(full_list))
print("ERROR: Invalid entry '{0}' for [{1}]-gapMode. Please check your config. Allowed values are: {2}"
.format(value, category, ", ".join(full_list)))
exit(1)
return value.lower()
else:
Expand Down Expand Up @@ -193,8 +202,8 @@ def get_notification_config():
notify_conf = {'enable_notifications': config.has_section('notifications')}

# For boolean parameters
for conf in ['notify_tx_coins', 'notify_xday_threshold', 'notify_new_loans', 'notify_caught_exception', 'email', 'slack', 'telegram',
'pushbullet', 'irc']:
for conf in ['notify_tx_coins', 'notify_xday_threshold', 'notify_new_loans', 'notify_caught_exception', 'email',
'slack', 'telegram', 'pushbullet', 'irc']:
notify_conf[conf] = getboolean('notifications', conf)

# For string-based parameters
Expand Down Expand Up @@ -239,5 +248,5 @@ def get_notification_config():
def get_plugins_config():
active_plugins = []
if config.has_option("BOT", "plugins"):
active_plugins = map(str.strip, config.get("BOT", "plugins").split(','))
active_plugins = list(map(str.strip, config.get("BOT", "plugins").split(',')))
return active_plugins
2 changes: 2 additions & 0 deletions modules/ConsoleUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import platform
import subprocess


def get_terminal_size():
""" getTerminalSize()
- get width and height of console
Expand Down Expand Up @@ -45,6 +46,7 @@ def _get_terminal_size_windows():
except:
pass


def _get_terminal_size_tput():
# get terminal width
# src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
Expand Down
17 changes: 10 additions & 7 deletions modules/Data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import datetime
from decimal import Decimal
from urllib import urlopen
try:
from urllib import urlopen
except ImportError:
# Python 3
from urllib.request import urlopen

import json

api = None
Expand All @@ -27,15 +32,15 @@ def get_max_duration(end_date, context):
return ""
try:
now_time = datetime.date.today()
config_date = map(int, end_date.split(','))
config_date = list(map(int, end_date.split(',')))
end_time = datetime.date(*config_date) # format YEAR,MONTH,DAY all ints, also used splat operator
diff_days = (end_time - now_time).days
if context == "order":
return diff_days # Order needs int
if context == "status":
return " - Days Remaining: " + str(diff_days) # Status needs string
except Exception as ex:
ex.message = ex.message if ex.message else str(ex)
ex.message = ex.message if hasattr(ex, 'message') and ex.message else str(ex)
print("ERROR: There is something wrong with your endDate option. Error: {0}".format(ex.message))
exit(1)

Expand All @@ -45,10 +50,8 @@ def get_total_lent():
total_lent = {}
rate_lent = {}
for item in crypto_lent["provided"]:
item_str = item["amount"].encode("utf-8")
item_float = Decimal(item_str)
item_rate_str = item["rate"].encode("utf-8")
item_rate_float = Decimal(item_rate_str)
item_float = Decimal(item["amount"])
item_rate_float = Decimal(item["rate"])
if item["currency"] in total_lent:
crypto_lent_sum = total_lent[item["currency"]] + item_float
crypto_lent_rate = rate_lent[item["currency"]] + (item_rate_float * item_float)
Expand Down
4 changes: 2 additions & 2 deletions modules/ExchangeApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"""

import abc
import six
import calendar
import time


@six.add_metaclass(abc.ABCMeta)
class ExchangeApi(object):
__metaclass__ = abc.ABCMeta

def __str__(self):
return self.__class__.__name__.upper()

Expand Down
Loading