Skip to content

Commit

Permalink
Improved exception handling for data source config
Browse files Browse the repository at this point in the history
Data source names in config are now case-insensitive.
Accounting/Price tool: display error message if data source name unrecognised.
Price tool: display error message if date is invalid.
  • Loading branch information
nanonano committed Aug 11, 2020
1 parent 8ba7959 commit d570c56
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 72 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
- Etherscan parser: added ERC-20 tokens and ERC-721 NFTs exports.
### Changed
- Sort wallet names in audit debug as case-insensitive.
- Data source names in config are now case-insensitive.
- Accounting/Price tool: display error message if data source name unrecognised.
- Price tool: display error message if date is invalid.

## Version [0.4.1] Beta (2020-07-25)
### Fixed
Expand Down
88 changes: 53 additions & 35 deletions bittytax/bittytax.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from .transactions import TransactionHistory
from .audit import AuditRecords
from .price.valueasset import ValueAsset
from .price.exceptions import UnexpectedDataSourceError
from .tax import TaxCalculator
from .report import ReportLog, ReportPdf
from .exceptions import ImportFailureError

if sys.stdout.encoding != 'UTF-8':
if sys.version_info[0] < 3:
Expand Down Expand Up @@ -70,18 +72,48 @@ def main():
print("%ssystem: %s, release: %s" % (Fore.GREEN, platform.system(), platform.release()))
config.output_config()

try:
transaction_records = do_import(config.args.filename)
except IOError:
parser.exit("%sERROR%s File could not be read: %s" % (
Back.RED+Fore.BLACK, Back.RESET+Fore.RED, config.args.filename))
except ImportFailureError:
parser.exit()

if not config.args.skipaudit and not config.args.summary:
audit = AuditRecords(transaction_records)
else:
audit = None

try:
tax, value_asset = do_tax(transaction_records,
config.args.taxyear,
config.args.summary)
except UnexpectedDataSourceError as e:
parser.exit("%sERROR%s %s" % (
Back.RED+Fore.BLACK, Back.RESET+Fore.RED, e))

if config.args.nopdf:
ReportLog(audit,
tax.tax_report,
value_asset.price_report,
tax.holdings_report)
else:
ReportPdf(parser.prog,
audit,
tax.tax_report,
value_asset.price_report,
tax.holdings_report)

def do_import(filename):
import_records = ImportRecords()

if config.args.filename:
if filename:
try:
try:
import_records.import_excel(config.args.filename)
except xlrd.XLRDError:
with io.open(config.args.filename, newline='', encoding='utf-8') as csv_file:
import_records.import_csv(csv_file)
except IOError:
parser.exit("%sERROR%s File could not be read: %s" % (
Back.RED+Fore.BLACK, Back.RESET+Fore.RED, config.args.filename))
import_records.import_excel(filename)
except xlrd.XLRDError:
with io.open(filename, newline='', encoding='utf-8') as csv_file:
import_records.import_csv(csv_file)
else:
if sys.version_info[0] < 3:
import_records.import_csv(codecs.getreader('utf-8')(sys.stdin))
Expand All @@ -93,15 +125,11 @@ def main():
import_records.success_cnt, import_records.failure_cnt))

if import_records.failure_cnt > 0:
parser.exit()
raise ImportFailureError

transaction_records = import_records.get_records()

if not config.args.skipaudit and not config.args.summary:
audit = AuditRecords(transaction_records)
else:
audit = None
return import_records.get_records()

def do_tax(transaction_records, tax_year, summary):
value_asset = ValueAsset()
transaction_history = TransactionHistory(transaction_records, value_asset)

Expand All @@ -115,35 +143,25 @@ def main():

tax.process_unmatched()

if not config.args.summary:
if not summary:
tax.process_income()

if config.args.taxyear:
if tax_year:
print("%scalculating tax year %d-%d" % (
Fore.CYAN, config.args.taxyear - 1, config.args.taxyear))
tax.calculate_capital_gains(config.args.taxyear)
if not config.args.summary:
tax.calculate_income(config.args.taxyear)
Fore.CYAN, tax_year - 1, tax_year))
tax.calculate_capital_gains(tax_year)
if not summary:
tax.calculate_income(tax_year)
else:
# Calculate for all years
for year in sorted(tax.tax_events):
print("%scalculating tax year %d-%d" % (
Fore.CYAN, year - 1, year))
tax.calculate_capital_gains(year)
if not config.args.summary:
if not summary:
tax.calculate_income(year)

if not config.args.summary:
if not summary:
tax.calculate_holdings(value_asset)

if config.args.nopdf:
ReportLog(audit,
tax.tax_report,
value_asset.price_report,
tax.holdings_report)
else:
ReportPdf(parser.prog,
audit,
tax.tax_report,
value_asset.price_report,
tax.holdings_report)
return tax, value_asset
4 changes: 4 additions & 0 deletions bittytax/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ def __str__(self):
class MissingDataError(TransactionParserError):
def __str__(self):
return "Missing data for %s" % self.col_name

class ImportFailureError(Exception):
def __str__(self):
return "Import failure"
27 changes: 21 additions & 6 deletions bittytax/price/bittytax_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ..version import __version__
from ..config import config
from .valueasset import ValueAsset
from .exceptions import UnexpectedDataSourceError

if sys.stdout.encoding != 'UTF-8':
if sys.version_info[0] < 3:
Expand Down Expand Up @@ -62,12 +63,26 @@ def main():
if asset == config.CCY:
return

if config.args.date:
timestamp = dateutil.parser.parse(config.args.date)
timestamp = timestamp.replace(tzinfo=config.TZ_LOCAL)
price_ccy, name, data_source = value_asset.get_historical_price(asset, timestamp)
else:
price_ccy, name, data_source = value_asset.get_latest_price(asset)
try:
if config.args.date:
try:
timestamp = dateutil.parser.parse(config.args.date)
except ValueError as e:
if sys.version_info[0] < 3:
err_msg = ' '.join(e)
else:
err_msg = ' '.join(e.args)

parser.exit("%sERROR%s Invalid date: %s" % (
Back.RED+Fore.BLACK, Back.RESET+Fore.RED, err_msg))

timestamp = timestamp.replace(tzinfo=config.TZ_LOCAL)
price_ccy, name, data_source = value_asset.get_historical_price(asset, timestamp)
else:
price_ccy, name, data_source = value_asset.get_latest_price(asset)
except UnexpectedDataSourceError as e:
parser.exit("%sERROR%s %s" % (
Back.RED+Fore.BLACK, Back.RESET+Fore.RED, e))

if price_ccy is not None:
print("%s1 %s=%s %s %svia %s (%s)" % (
Expand Down
18 changes: 18 additions & 0 deletions bittytax/price/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# (c) Nano Nano Ltd 2020

import os

from ..config import config
from .datasource import DataSourceBase

class UnexpectedDataSourceError(Exception):
def __init__(self, value):
super(UnexpectedDataSourceError, self).__init__()
self.value = value

def __str__(self):
return "Invalid data source: \'%s\' in %s, use {%s}" % (
self.value,
os.path.join(config.BITTYTAX_PATH, config.BITTYTAX_CONFIG),
','.join([ds.__name__ for ds in DataSourceBase.__subclasses__()]))
61 changes: 30 additions & 31 deletions bittytax/price/pricedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from ..version import __version__
from ..config import config
from .datasource import ExchangeRatesAPI, RatesAPI, CoinDesk, CryptoCompare, CoinGecko, CoinPaprika
from .datasource import DataSourceBase
from .exceptions import UnexpectedDataSourceError

class PriceData(object):
def __init__(self):
Expand All @@ -20,11 +21,9 @@ def __init__(self):
config.data_source_crypto) | \
{x for v in config.data_source_select.values() for x in v}

for data_source in data_sources_required:
try:
self.data_sources[data_source] = globals()[data_source]()
except KeyError:
raise ValueError("data source: %s not recognised" % [data_source])
for data_source in DataSourceBase.__subclasses__():
if data_source.__name__.upper() in [ds.upper() for ds in data_sources_required]:
self.data_sources[data_source.__name__.upper()] = data_source()

@staticmethod
def data_source_priority(asset):
Expand All @@ -36,41 +35,41 @@ def data_source_priority(asset):
return config.data_source_crypto

def get_latest_ds(self, data_source, asset, quote):
if data_source in self.data_sources:
if asset in self.data_sources[data_source].assets:
return self.data_sources[data_source].get_latest(asset, quote), \
self.data_sources[data_source].assets[asset]
if data_source.upper() in self.data_sources:
if asset in self.data_sources[data_source.upper()].assets:
return self.data_sources[data_source.upper()].get_latest(asset, quote), \
self.data_sources[data_source.upper()].assets[asset]

return None, None
else:
raise ValueError("data source: %s not recognised" % [data_source])
raise UnexpectedDataSourceError(data_source)

def get_historical_ds(self, data_source, asset, quote, timestamp):
if data_source in self.data_sources:
if asset in self.data_sources[data_source].assets:
if data_source.upper() in self.data_sources:
if asset in self.data_sources[data_source.upper()].assets:
date = timestamp.strftime('%Y-%m-%d')
pair = asset + '/' + quote

if not config.args.nocache:
# check cache first
if pair in self.data_sources[data_source].prices and \
date in self.data_sources[data_source].prices[pair]:
return self.data_sources[data_source].prices[pair][date]['price'], \
self.data_sources[data_source].assets[asset], \
self.data_sources[data_source].prices[pair][date]['url']

self.data_sources[data_source].get_historical(asset, quote, timestamp)
if pair in self.data_sources[data_source].prices and \
date in self.data_sources[data_source].prices[pair]:
return self.data_sources[data_source].prices[pair][date]['price'], \
self.data_sources[data_source].assets[asset], \
self.data_sources[data_source].prices[pair][date]['url']
if pair in self.data_sources[data_source.upper()].prices and \
date in self.data_sources[data_source.upper()].prices[pair]:
return self.data_sources[data_source.upper()].prices[pair][date]['price'], \
self.data_sources[data_source.upper()].assets[asset], \
self.data_sources[data_source.upper()].prices[pair][date]['url']

self.data_sources[data_source.upper()].get_historical(asset, quote, timestamp)
if pair in self.data_sources[data_source.upper()].prices and \
date in self.data_sources[data_source.upper()].prices[pair]:
return self.data_sources[data_source.upper()].prices[pair][date]['price'], \
self.data_sources[data_source.upper()].assets[asset], \
self.data_sources[data_source.upper()].prices[pair][date]['url']
else:
return None, self.data_sources[data_source].assets[asset], None
return None, self.data_sources[data_source.upper()].assets[asset], None
else:
return None, None, None
else:
raise ValueError("data source: %s not recognised" % [data_source])
raise UnexpectedDataSourceError(data_source)

def get_latest(self, asset, quote):
price = name = data_source = None
Expand All @@ -83,11 +82,11 @@ def get_latest(self, asset, quote):
asset,
'{:0,f}'.format(price.normalize()),
quote,
data_source,
self.data_sources[data_source.upper()].name(),
name))
break

return price, name, data_source
return price, name, self.data_sources[data_source.upper()].name()

def get_historical(self, asset, quote, timestamp):
price = name = data_source = url = None
Expand All @@ -101,8 +100,8 @@ def get_historical(self, asset, quote, timestamp):
asset,
'{:0,f}'.format(price.normalize()),
quote,
data_source,
self.data_sources[data_source.upper()].name(),
name))
break

return price, name, data_source, url
return price, name, self.data_sources[data_source.upper()].name(), url

0 comments on commit d570c56

Please sign in to comment.