fetch: Loggerize, use argparse

banking-pekao
informatic 2018-03-24 17:26:44 +01:00
parent b8d4dc17e8
commit 05ac0356cf
2 changed files with 139 additions and 135 deletions

View File

@ -27,13 +27,15 @@
# POSSIBILITY OF SUCH DAMAGE.
from datetime import date, datetime
from getopt import getopt, GetoptError
import sys
from time import sleep, time
import csv
import os
import random
import re
import logging
import logging.config
import argparse
import enum
import bs4
@ -52,6 +54,11 @@ config = {
Base = declarative_base()
if config.get('LOGGING'):
logging.config.dictConfig(config['LOGGING'])
else:
logging.basicConfig(level=logging.DEBUG)
class RawTransfer(Base):
__tablename__ = 'raw_transfer'
id = Column(Integer, primary_key=True)
@ -94,7 +101,7 @@ class IBRow(RawTransfer):
return str(self)
def __init__(self, row, on_account, raw):
self.raw = raw
self.raw = raw.decode('utf-8')
self.uid = row[IBField.uid]
self.index = 1
self.date = datetime.strptime(row[IBField.date_completed], "%Y%m%d").date()
@ -254,6 +261,7 @@ class IBFetcher(object):
BASE = "https://secure.ideabank.pl/"
START_DATE = "01.11.2016"
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
self._soup = None
self.token = None
self.s = requests.Session()
@ -274,13 +282,14 @@ class IBFetcher(object):
def _dump(self):
fn = config["DUMP_FILE"]
print("[w] Dumping the last page to {}".format(fn))
self.logger.warning("Dumping the last page to %f", fn)
open(fn, 'w').write(unicode(self._soup).encode('utf-8'))
def _getraw(self, page, params={}):
url = self.BASE + page
r = self.s.get(url, params=params)
print("[i] GET {}?{} -> {}".format(page, "&".join([str(k)+"="+str(v) for k,v in params.items()]), r.status_code))
self.logger.debug("GET %s?%s -> %d", page, "&".join([
str(k)+"="+str(v) for k, v in params.items()]), r.status_code)
if r.status_code != 200:
raise Exception("return code %i" % r.status_code)
return r
@ -302,7 +311,7 @@ class IBFetcher(object):
})
r = self.s.post(url, data)
print("[i] POST {} -> {}".format(page, r.status_code))
self.logger.debug("POST %s -> %d", page, r.status_code)
if r.status_code != 200:
self._dump()
raise Exception("return code %i" % r.status_code)
@ -323,7 +332,7 @@ class IBFetcher(object):
return soup
def _wait(self, seconds):
print("[i] Waiting {} seconds".format(seconds))
self.logger.debug("Waiting %d seconds", seconds)
sleep(seconds)
def _gettoken(self, soup):
@ -338,16 +347,16 @@ class IBFetcher(object):
if t is not None:
self.token = t
print("[i] Token: {}".format(self.token))
self.logger.debug("Token: %s", self.token)
else:
print("[i] No new token found")
self.logger.debug("No new token found")
def _hitjstoken(self, soup):
m = re.search("\/main\/index\/token\/([0-9]+)\/time\/", str(soup.head))
if m is not None:
t = m.group(1)
r = self._getraw("main/index/token/{}/time/{:.0f}.js".format(t, time()*1000), params={"t": "{:.16f}".format(random.random())})
print("[i] Fetched JS timestamp token: \"{}\"".format(r.text))
self.logger.debug("Fetched JS timestamp token: %r", r.text)
def process_wallet_page(self, soup):
wallet = {"accounts": {}}
@ -380,7 +389,7 @@ class IBFetcher(object):
wallet["accounts"][account["number"]] = account
if len(wallet["accounts"]) == 0:
print("[e] Empty accounts list. Undetected failed login? Aborting.")
self.logger.error("Empty accounts list. Undetected failed login? Aborting.")
self._dump()
sys.exit(4)
@ -400,17 +409,10 @@ class IBFetcher(object):
password2_input = login2_page.find("input", attrs={"name": "password2"})
if password2_input is None:
print("[e] Masked password screen encountered - aborting")
self.logger.error("Masked password screen encountered - aborting")
sys.exit(4)
#data["log"] = username
#data["log2"] = ""
#part_inputs = login2_page.find_all("input", class_="password_parts_inputs")
#print("[i] Filling out {} characters".format(len(part_inputs)))
#for input in part_inputs:
#_,pos = input["name"].split("_")
#data["pass_"+str(pos)] = password[int(pos)]
else:
print("[i] Regular password screen encountered")
self.logger.debug("Regular password screen encountered")
data["log2"] = username
data["password2"] = password2_input["value"]
data["password"] = password
@ -418,10 +420,10 @@ class IBFetcher(object):
wallet_page = self._post("main/index", data)
if wallet_page.find("div", class_="login_form"):
print("[e] Login failed, aborting")
self.logger.error("Login failed, aborting")
self._dump()
try:
print("[e] Possible reason: {}".format(','.join(wallet_page.find("ul", class_="error_list").stripped_strings)))
self.logger.error("Possible reason: %r", ','.join(wallet_page.find("ul", class_="error_list").stripped_strings))
except:
pass # screw it, we're fucked anyway
sys.exit(4)
@ -430,33 +432,6 @@ class IBFetcher(object):
return self.process_wallet_page(wallet_page)
def fetch_account_history(self, account_id):
#account_page = self._get("accounts/index/{}/2".format(account_id))
#self._wait(4)
#data = {
#"code": account_id,
#"basic": 1,
#"date_from": self.START_DATE,
#"date_to": '{:02d}.{:02d}.{:04d}'.format(date.today().day, date.today().month, date.today().year),
#"interval_time": "",
#"interval_type": "",
#"last": "",
#"advanced[0]": "0",
#"advanced[1]": "1",
#"operation_type": "3",
#"amount_from": "",
#"amount_to": "",
#"transaction_type": "",
#"from": "",
#"title": "",
#"send": "send",
#"ajaxSend": "true"
#}
#history_page = self._post("accounts/history/{}".format(account_id), data)
#self._wait(2)
#r = self._getraw("accounts/printHistoryFile")
data = {
"code": account_id,
"report_type": "csv_dr",
@ -475,143 +450,134 @@ def usage():
def lock():
fn = config["LOCK_FILE"]
if os.path.isfile(fn):
print("[e] Lock file {} exists, aborting".format(fn))
logging.error("Lock file %s exists, aborting", fn)
sys.exit(3)
print("[i] Setting up lock file {}".format(fn))
logging.debug("Setting up lock file %s", fn)
open(fn,'w').close()
if not os.path.isfile(fn):
print("[e] Lock file {} somehow does not exist, aborting".format(fn))
logging.error("Lock file %s somehow does not exist, aborting", fn)
sys.exit(3)
def release():
fn = config["LOCK_FILE"]
print("[i] Removing lock file {}".format(fn))
logging.debug("Removing lock file %s", fn)
if not os.path.isfile(fn):
print("[e] Lock file {} somehow does not exist, WTF?".format(fn))
logging.error("Lock file %s somehow does not exist, WTF?", fn)
sys.exit(3)
os.remove(fn)
if os.path.isfile(fn):
print("[e] Lock file {} somehow still exists, WTF?".format(fn))
logging.error("Lock file %s somehow still exists, WTF?", fn)
sys.exit(3)
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--no-action', action="store_true", help='do not commit any database changes')
parser.add_argument('-c', '--cached', action="store_true", help='use cached data (test)')
parser.add_argument('-l', '--load', action='append', help='process specified files (test)')
parser.add_argument('--print-schema', action="store_true", help='print table schema and quit')
if __name__ == "__main__":
try:
opts, args = getopt(sys.argv[1:], "hcl:n", ["help", "cached", "load=", "no-action", "print-schema"])
except GetoptError as err:
# print help information and exit:
print(str(err)) # will print something like "option -a not recognized"
usage()
sys.exit(2)
args = parser.parse_args()
CACHE_DIR = config["CACHE_DIR"]
engine = create_engine(config["SQLALCHEMY_DATABASE_URI"])
session = sessionmaker(bind=engine)()
cached = False
noaction = False
load_files = {}
for o, a in opts:
if o in ("-h", "--help"):
usage()
sys.exit()
elif o in ("--print-schema"):
print("[i] Called with --print-schema, will print the create statement and quit.")
m = MetaData()
print(CreateTable(IBRow.__table__).compile(engine),";")
for index in IBRow.__table__.indexes:
print(CreateIndex(index).compile(engine),";")
sys.exit()
elif o in ("-c", "--cached"):
cached = True
elif o in ("-l", "--load"):
an, f = a.split(":")
if an is None or f is None:
print("[e] --load argument \"{}\" appears malformed, could not split account number and file name".format(a))
sys.exit(2)
account_number = IBParser.parse_account_number(an)
if account_number is None:
print("[e] Account number \"{}\" unparseable".format(an))
sys.exit(2)
history = open(f,'r').read()
load_files[account_number] = history
cached = True
print("[i] Loading \"{}\" as \"{}\"".format(f, account_number))
elif o in ("-n", "--no-action"):
print("[i] Called with --no-action, will not do any database operations.")
noaction = True
else:
assert False, "unhandled option"
if args.print_schema:
logging.debug("Called with --print-schema, will print the create statement and quit.")
m = MetaData()
print('%s;' % CreateTable(IBRow.__table__).compile(engine))
for index in IBRow.__table__.indexes:
print('%s;' % CreateIndex(index).compile(engine))
sys.exit()
lock()
balances = {}
history_logs = {}
if cached:
print("[i] Cached run - will not connect to the bank")
if len(load_files) > 0:
print("[i] Using manually supplied files")
history_logs = load_files
else:
print("[i] Loading cached files from {}".format(CACHE_DIR))
for f in os.listdir(CACHE_DIR):
account_number = IBParser.parse_account_number(f)
if account_number is None:
print("[e] File name number \"{}\" unparseable".format(f))
continue
history = open(CACHE_DIR + "/" + f,'r').read()
load_files[account_number] = history
print("[i] Loading \"{}\" as \"{}\"".format(f, account_number))
if len(load_files) == 0:
print("[e] No cached files to process")
sys.exit(2)
history_logs = load_files
if args.load:
logging.debug("Using manually supplied files")
for fn in args.load:
an, f = fn.split(':')
account_number = IBParser.parse_account_number(an)
if account_number is None:
logging.error("File name number \"{}\" unparseable".format(f))
continue
logging.debug('Loading "%s" as "%s"', f, account_number)
with open(f, 'r') as fd:
history_logs[account_number] = fd.read()
elif args.cached:
logging.debug("Loading cached files from {}".format(CACHE_DIR))
for f in os.listdir(CACHE_DIR):
if f.startswith('balance-'):
continue
account_number = IBParser.parse_account_number(f)
if account_number is None:
logging.error("File name number \"{}\" unparseable".format(f))
continue
with open(CACHE_DIR + "/" + f,'r') as fd:
history_logs[account_number] = fd.read()
logging.debug("Loading \"{}\" as \"{}\"".format(f, account_number))
else:
print("[i] Normal run - will connect to the bank")
logging.debug("Normal run - will connect to the bank")
fetcher = IBFetcher()
history_logs = {}
if "IB_LOGIN" not in config.keys() or "IB_PASSWORD" not in config.keys():
wallet = fetcher.login(raw_input("[?] ID: "), raw_input("[?] Password: "))
else:
print("[i] Using saved credentials")
logging.debug("Using saved credentials")
wallet = fetcher.login(config["IB_LOGIN"], config["IB_PASSWORD"])
for account_number, account in wallet["accounts"].items():
print("[i] Fetching history for account {} ({})".format(account_number, account["id"]))
logging.debug("Fetching history for account {} ({})".format(account_number, account["id"]))
history = fetcher.fetch_account_history(account["id"])
cachefile = open(CACHE_DIR+"/"+account_number,'w')
cachefile.write(history)
cachefile.close()
history_logs[account_number] = history
balances[account_number] = (account["available_balance"],account["currency"])
balancefile = open(CACHE_DIR+"/balance-"+account_number,'w')
balancefile.write("{} {}\n".format(account["available_balance"],account["currency"]))
balancefile.close()
with open(CACHE_DIR+"/"+account_number,'w') as fd:
fd.write(history)
balances[account_number] = (account["available_balance"], account["currency"])
with open(CACHE_DIR+"/balance-"+account_number,'w') as fd:
fd.write("{} {}\n".format(
account["available_balance"],account["currency"]))
if not history_logs:
logging.error('Nothing to process')
sys.exit()
parsed = {}
stats = {}
for account_number, history in history_logs.items():
print("[i] Parsing history for account {}".format(account_number))
logging.debug("Parsing history for account {}".format(account_number))
parser = IBParser(account_number)
parser.parse(history)
stats[account_number] = {}
stats[account_number]["added"] = 0
stats[account_number]["skipped"] = 0
for row in parser.get():
if not session.query(IBRow).filter_by(uid=row.uid).first() and not noaction:
if not session.query(IBRow).filter_by(uid=row.uid).first():
session.add(row)
stats[account_number]["added"] += 1
else:
stats[account_number]["skipped"] += 1
if noaction:
print("[i] --no-action set, skipping row {}".format(row))
session.commit()
if args.no_action:
logging.info('Running with --no-action, not commiting.')
else:
session.commit()
if balances:
print("[i] Account balances:")
logging.debug("Account balances:")
for account_number,v in balances.items():
balance,currency = v
print("\t{}: {} {}".format(account_number, balance, currency))
print("[i] Done: ", stats)
logging.debug("\t{}: {} {}".format(account_number, balance, currency))
if any(v['added'] for v in stats.values()):
logging.info("Done: %r", stats)
release()

View File

@ -2,6 +2,44 @@ class Config(object):
DEBUG = False
TESTING = False
SQLALCHEMY_DATABASE_URI = "sqlite:///data.db"
OWN_ACCOUNTS = []
SECRET = 'setme'
CACHE_DIR = 'cache/'
LOCK_FILE = '/tmp/kasownik.lock'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'standard',
},
'file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'level': 'DEBUG',
'formatter': 'standard',
'filename': 'kasownik.log',
'backupCount': 7,
'when': 'midnight',
}
},
'loggers': {
'': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': True,
},
},
}
class DevelopmentConfig(Config):