diff --git a/src/cardano_account_pandas_dumper/__main__.py b/src/cardano_account_pandas_dumper/__main__.py index 719704b..b670744 100644 --- a/src/cardano_account_pandas_dumper/__main__.py +++ b/src/cardano_account_pandas_dumper/__main__.py @@ -9,7 +9,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt from blockfrost import ApiError, BlockFrostApi -from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE from .cardano_account_pandas_dumper import AccountData, AccountPandasDumper # Error codes due to project key rate limiting or capping @@ -71,13 +70,14 @@ def _create_arg_parser(): "--graph_output", help="Path to graphics output file.", type=str, - ) result.add_argument( "--matplotlib_rc", help="Path to matplotlib defaults file.", type=str, - default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "matplotlib.rc"), + default=os.path.join( + os.path.dirname(os.path.abspath(__file__)), "matplotlib.rc" + ), ) result.add_argument( "--detail_level", @@ -129,7 +129,9 @@ def main(): message="Following addresses do not look like valid staking addresses: " + " ".join(invalid_staking_addresses), ) - if not any([args.checkpoint_output, args.csv_output, args.xlsx_output, args.graph_output]): + if not any( + [args.checkpoint_output, args.csv_output, args.xlsx_output, args.graph_output] + ): parser.exit( status=1, message="No output specified, neeed at least one of --checkpoint_output," @@ -199,11 +201,11 @@ def main(): known_dict=known_dict_from_file, truncate_length=args.truncate_length, unmute=args.unmute, - detail_level=args.detail_level, ) if args.csv_output: try: reporter.make_transaction_frame( + detail_level=args.detail_level, with_total=args.with_total, raw_values=args.raw_values, ).to_csv( @@ -214,13 +216,8 @@ def main(): if args.xlsx_output: try: frame = reporter.make_transaction_frame( + detail_level=args.detail_level, with_total=args.with_total, - text_cleaner=lambda x: ILLEGAL_CHARACTERS_RE.sub( - lambda y: "".join( - ["\\x0" + hex(ord(y.group(0))).removeprefix("0x")] - ), - x, - ), raw_values=args.raw_values, ) frame.to_excel( @@ -239,8 +236,10 @@ def main(): with mpl.rc_context(fname=args.matplotlib_rc): try: reporter.plot_balance() - plt.savefig(args.graph_output, - metadata=reporter.get_graph_metadata(args.graph_output)) + plt.savefig( + args.graph_output, + metadata=reporter.get_graph_metadata(args.graph_output), + ) except OSError as exception: warnings.warn(f"Failed to write graph file: {exception}") print("Done.") diff --git a/src/cardano_account_pandas_dumper/cardano_account_pandas_dumper.py b/src/cardano_account_pandas_dumper/cardano_account_pandas_dumper.py index e3f2e3a..6ae8656 100644 --- a/src/cardano_account_pandas_dumper/cardano_account_pandas_dumper.py +++ b/src/cardano_account_pandas_dumper/cardano_account_pandas_dumper.py @@ -1,14 +1,12 @@ """ Cardano Account To Pandas Dumper.""" -from base64 import b64decode import datetime -from io import BytesIO import itertools import os +from base64 import b64decode from collections import defaultdict -from textwrap import wrap +from io import BytesIO from typing import ( Any, - Callable, Dict, FrozenSet, List, @@ -22,13 +20,12 @@ import blockfrost.utils import matplotlib as mpl -from matplotlib import pyplot -from matplotlib.axes import Axes -from matplotlib.image import AxesImage, BboxImage -from matplotlib.transforms import TransformedBbox import numpy as np import pandas as pd from blockfrost import BlockFrostApi +from matplotlib import pyplot +from matplotlib.axes import Axes +from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE class AccountData: @@ -169,12 +166,10 @@ def __init__( known_dict: Any, truncate_length: int, unmute: bool, - detail_level: int, ): self.data = data self.truncate_length = truncate_length self.unmute = unmute - self.detail_level = detail_level self.address_names = pd.Series( {a: " wallet" for a in self.data.own_addresses} | known_dict.get("addresses", {}) @@ -227,7 +222,11 @@ def _decode_asset_name(self, asset: blockfrost.utils.Namespace) -> str: return asset.metadata.name asset_hex_name = asset.asset.removeprefix(asset.policy_id) try: - return bytes.fromhex(asset_hex_name).decode() + decoded = bytes.fromhex(asset_hex_name).decode() + return ILLEGAL_CHARACTERS_RE.sub( + lambda y: "".join(["\\x0" + hex(ord(y.group(0))).removeprefix("0x")]), + decoded, + ) except UnicodeDecodeError: return f"{self._format_policy(asset.policy_id)}@{self._truncate(asset_hex_name)}" @@ -493,12 +492,7 @@ def _transaction_balance( ) return result - def make_balance_frame( - self, - with_total: bool, - raw_values: bool, - text_cleaner: Callable = lambda x: x, - ): + def make_balance_frame(self, with_total: bool, raw_values: bool, detail_level: int): """Make DataFrame with transaction balances.""" balance = pd.DataFrame( data=[self._transaction_balance(x, raw_values) for x in self.transactions], @@ -510,7 +504,7 @@ def make_balance_frame( if not self.unmute: self._drop_muted_assets(balance) - if self.detail_level == 1: + if detail_level == 1: group: Tuple = (0, 1) elif raw_values: group = (0, 1, 2) @@ -550,27 +544,23 @@ def make_balance_frame( if not raw_values: balance.columns = pd.MultiIndex.from_tuples( [ - (text_cleaner(self.asset_names.get(c[0], c[0])),) - + cast(tuple, c)[1:] + (self.asset_names.get(c[0], c[0]),) + cast(tuple, c)[1:] for c in balance.columns ] ) - if self.detail_level == 1: + if detail_level == 1: return balance.xs(self.OWN_LABEL, level=1, axis=1) else: return balance def make_transaction_frame( - self, - raw_values: bool, - with_total: bool = True, - text_cleaner: Callable = lambda x: x, + self, detail_level: int, raw_values: bool, with_total: bool ) -> pd.DataFrame: """Build a transaction spreadsheet.""" msg_frame = pd.DataFrame( data=[ - {"hash": x.hash, "message": text_cleaner(self._format_message(x))} + {"hash": x.hash, "message": self._format_message(x)} for x in self.transactions ], index=self.transactions.index, @@ -590,11 +580,11 @@ def make_transaction_frame( ] ) balance_frame = self.make_balance_frame( - with_total=with_total, text_cleaner=text_cleaner, raw_values=raw_values + detail_level=detail_level, with_total=with_total, raw_values=raw_values ) if not raw_values: balance_frame.sort_index(axis=1, level=0, sort_remaining=True, inplace=True) - if self.detail_level > 1: + if detail_level > 1: msg_frame.columns = pd.MultiIndex.from_tuples( [ (c,) + (len(balance_frame.columns[0]) - 1) * ("",) @@ -614,11 +604,13 @@ def _draw_asset_legend(self, ax: Axes, asset_id: str): ticker = None name = str(self.asset_names.get(asset_id, asset_id)) image_data: Any = None + url = None if asset_id == self.ADA_ASSET: ticker = "ADA" image_data = os.path.join( os.path.dirname(os.path.abspath(__file__)), "ada_logo.webp" ) + url = "https://cardano.org/" else: asset_obj = self.data.assets[asset_id] if hasattr(asset_obj, "metadata"): @@ -626,6 +618,8 @@ def _draw_asset_legend(self, ax: Axes, asset_id: str): image_data = BytesIO(b64decode(asset_obj.metadata.logo)) if hasattr(asset_obj.metadata, "ticker"): ticker = asset_obj.metadata.ticker + if hasattr(asset_obj.metadata, "url"): + url = asset_obj.metadata.url if ticker: ax.text( 0.5, @@ -635,6 +629,8 @@ def _draw_asset_legend(self, ax: Axes, asset_id: str): transform=ax.transAxes, fontsize="large", fontweight="bold", + clip_on=True, + url=url, ) ax.text( 0.5, @@ -644,6 +640,7 @@ def _draw_asset_legend(self, ax: Axes, asset_id: str): transform=ax.transAxes, fontsize="xx-small", clip_on=True, + url=url, ) if image_data: ax.set_adjustable("datalim") @@ -651,13 +648,16 @@ def _draw_asset_legend(self, ax: Axes, asset_id: str): mpl.image.imread(image_data), aspect="equal", extent=(0.3, 0.7, 0.8, 0.4), + url=url, ) ax.set_xlim((0.0, 1.0)) ax.set_ylim((1.0, 0.0)) def plot_balance(self, order: str = "appearance"): """Create a Matplotlib plot with the asset balance over time.""" - balance = self.make_balance_frame(with_total=False, raw_values=True).cumsum() + balance = self.make_balance_frame( + detail_level=1, with_total=False, raw_values=True + ).cumsum() if order == "alpha": balance.sort_index( axis=1,