diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c09b796..a2db786e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,25 +5,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, windows-latest, macos-latest] exclude: - - python-version: "2.7" - os: ubuntu-latest - python-version: "3.6" os: ubuntu-latest include: - python-version: "3.6" os: ubuntu-20.04 - - python-version: "2.7" - os: ubuntu-20.04 - pylint-rcfile: "--rcfile=.pylintrc-27" - - python-version: "2.7" - os: macos-latest - pylint-rcfile: "--rcfile=.pylintrc-27" - - python-version: "2.7" - os: windows-latest - pylint-rcfile: "--rcfile=.pylintrc-27" runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v3 diff --git a/.pylintrc b/.pylintrc index ba1c9415..d8f97494 100644 --- a/.pylintrc +++ b/.pylintrc @@ -87,13 +87,6 @@ disable=missing-module-docstring, missing-class-docstring, missing-function-docstring, duplicate-code, - useless-object-inheritance, # Not 2.7 compatible - consider-using-f-string, # Not 2.7 compatible - consider-using-dict-items, # Not 2.7 compatible - super-with-arguments, # Not 2.7 compatible - raise-missing-from, # Not 2.7 compatible - redundant-u-string-prefix, # Not 2.7 compatible - unspecified-encoding, # Not 2.7 compatible invalid-name, # Workaround (https://github.com/PyCQA/pylint/issues/8319) # Enable the message, report, category or checker with the given id(s). You can @@ -158,7 +151,7 @@ max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it work, # install the 'python-enchant' package. -spelling-dict= +spelling-dict=en_GB # List of comma separated words that should be considered directives if they # appear and the beginning of a comment and should not be checked. diff --git a/.pylintrc-27 b/.pylintrc-27 deleted file mode 100644 index 73066b77..00000000 --- a/.pylintrc-27 +++ /dev/null @@ -1,490 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=missing-docstring, - duplicate-code, - superfluous-parens, # Not 3.x compatible - bad-continuation, # Black compatibility - ungrouped-imports, # isort compatiblity - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=7 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=optparse.Values,sys.exit - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: de_DE (myspell), fr_FR -# (myspell), en_GB (myspell), en_AU (myspell), en_US (myspell). -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file=.pylint_dictionary - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,pisaContext - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=b, - e, - h, - i, - j, - k, - s, - t, - v, - df, - dr, - ds, - ex, - ld, - te, - tp, - tr, - tz, - Run, - config, - _ - - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=14 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=30 - -# Maximum number of locals for function / method body -max-locals=24 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=92 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dae32af..175c4b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log ## [Unreleased] ### Added -- Conversion tool: identify data file types (.xls, .zip/.xlsx) using magic numbers +- Conversion tool: identify data file types (.xls, .zip/.xlsx) using magic numbers. - Conversion tool: identify duplicate data files using hashes. ### Changed - Accounting tool: use openpyxl instead of xlrd for reading .xlsx files. ([#260](https://github.com/BittyTax/BittyTax/issues/260)) @@ -9,6 +9,10 @@ - Conversion tool: error with non-zero exit status if no data files are processed. ([#253](https://github.com/BittyTax/BittyTax/issues/253)) - Conversion tool: report and continue any unexpected parser/merger exceptions. - Binance parser: add renamed Operation/Account. ([#300](https://github.com/BittyTax/BittyTax/issues/300)) +- Make code Python 3 compliant. + +### Removed +- Removed support for Python 2.7 as it is end of life. ## Version [0.5.1] (2023-04-04) Important:- diff --git a/setup.cfg b/setup.cfg index eb07e1da..2f720c7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,6 @@ classifiers = Operating System :: MacOS Operating System :: Microsoft :: Windows Operating System :: POSIX :: Linux - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -40,13 +39,13 @@ project_urls = package_dir= = src packages = find: -python_requires = >=2.7 +python_requires = >=3.6 install_requires = colorama defusedxml jinja2 openpyxl - python-dateutil >= 2.7.0 + python-dateutil pyyaml reportlab requests @@ -67,6 +66,3 @@ console_scripts = bittytax = bittytax.bittytax:main bittytax_conv = bittytax.conv.bittytax_conv:main bittytax_price = bittytax.price.bittytax_price:main - -[bdist_wheel] -universal = 1 diff --git a/src/bittytax/audit.py b/src/bittytax/audit.py index 0de387db..e940ea83 100644 --- a/src/bittytax/audit.py +++ b/src/bittytax/audit.py @@ -4,29 +4,30 @@ import sys from decimal import Decimal -from colorama import Back, Fore, Style +from colorama import Fore, Style from tqdm import tqdm from .config import config +from .constants import WARNING -class AuditRecords(object): +class AuditRecords: def __init__(self, transaction_records): self.wallets = {} self.totals = {} self.failures = [] if config.debug: - print("%saudit transaction records" % Fore.CYAN) + print(f"{Fore.CYAN}audit transaction records") for tr in tqdm( transaction_records, unit="tr", - desc="%saudit transaction records%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}audit transaction records{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if config.debug: - print("%saudit: TR %s" % (Fore.MAGENTA, tr)) + print(f"{Fore.MAGENTA}audit: TR {tr}") if tr.buy: self._add_tokens(tr.wallet, tr.buy.asset, tr.buy.quantity) @@ -37,32 +38,19 @@ def __init__(self, transaction_records): self._subtract_tokens(tr.wallet, tr.fee.asset, tr.fee.quantity) if config.debug: - print("%saudit: final balances by wallet" % Fore.CYAN) + print(f"{Fore.CYAN}audit: final balances by wallet") for wallet in sorted(self.wallets, key=str.lower): for asset in sorted(self.wallets[wallet]): print( - "%saudit: %s:%s=%s%s%s" - % ( - Fore.YELLOW, - wallet, - asset, - Style.BRIGHT, - "{:0,f}".format(self.wallets[wallet][asset].normalize()), - Style.NORMAL, - ) + f"{Fore.YELLOW}audit: {wallet}:{asset}={Style.BRIGHT}" + f"{self.wallets[wallet][asset].normalize():0,f}{Style.NORMAL}" ) - print("%saudit: final balances by asset" % Fore.CYAN) + print(f"{Fore.CYAN}audit: final balances by asset") for asset in sorted(self.totals): print( - "%saudit: %s=%s%s%s" - % ( - Fore.YELLOW, - asset, - Style.BRIGHT, - "{:0,f}".format(self.totals[asset].normalize()), - Style.NORMAL, - ) + f"{Fore.YELLOW}audit: {asset}={Style.BRIGHT}" + f"{self.totals[asset].normalize():0,f}{Style.NORMAL}" ) if config.audit_hide_empty: @@ -93,14 +81,8 @@ def _add_tokens(self, wallet, asset, quantity): if config.debug: print( - "%saudit: %s:%s=%s (+%s)" - % ( - Fore.GREEN, - wallet, - asset, - "{:0,f}".format(self.wallets[wallet][asset].normalize()), - "{:0,f}".format(quantity.normalize()), - ) + f"{Fore.GREEN}audit: {wallet}:{asset}=" + f"{self.wallets[wallet][asset].normalize():0,f} (+{quantity.normalize():0,f})" ) def _subtract_tokens(self, wallet, asset, quantity): @@ -119,26 +101,14 @@ def _subtract_tokens(self, wallet, asset, quantity): if config.debug: print( - "%saudit: %s:%s=%s (-%s)" - % ( - Fore.GREEN, - wallet, - asset, - "{:0,f}".format(self.wallets[wallet][asset].normalize()), - "{:0,f}".format(quantity.normalize()), - ) + f"{Fore.GREEN}audit: {wallet}:{asset}=" + f"{self.wallets[wallet][asset].normalize():0,f} (-{quantity.normalize():0,f})" ) if self.wallets[wallet][asset] < 0 and asset not in config.fiat_list: tqdm.write( - "%sWARNING%s Balance at %s:%s is negative %s" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - wallet, - asset, - "{:0,f}".format(self.wallets[wallet][asset].normalize()), - ) + f"{WARNING} Balance at {wallet}:{asset} " + f"is negative {self.wallets[wallet][asset].normalize():0,f}" ) def compare_pools(self, holdings): @@ -150,25 +120,20 @@ def compare_pools(self, holdings): if asset in holdings: if self.totals[asset] == holdings[asset].quantity: if config.debug: - print("%scheck pool: %s (ok)" % (Fore.GREEN, asset)) + print(f"{Fore.GREEN}check pool: {asset} (ok)") else: if config.debug: print( - "%scheck pool: %s %s (mismatch)" - % ( - Fore.RED, - asset, - "{:+0,f}".format( - (holdings[asset].quantity - self.totals[asset]).normalize() - ), - ) + f"{Fore.RED}check pool: {asset}" + f"{(holdings[asset].quantity - self.totals[asset]).normalize():+0,f} " + f"(mismatch)" ) self._log_failure(asset, self.totals[asset], holdings[asset].quantity) passed = False else: if config.debug: - print("%scheck pool: %s (missing)" % (Fore.RED, asset)) + print(f"{Fore.RED}check pool: {asset} (missing)") self._log_failure(asset, self.totals[asset], None) passed = False @@ -184,35 +149,20 @@ def _log_failure(self, asset, audit, s104): self.failures.append(failure) def report_failures(self): - header = "%-8s %25s %25s %25s" % ( - "Asset", - "Audit Balance", - "Section 104 Pool", - "Difference", + print( + f"\n{Fore.YELLOW}" + f'{"Asset":<8} {"Audit Balance":>25} {"Section 104 Pool":>25} {"Difference":>25}' ) - print("\n%s%s" % (Fore.YELLOW, header)) for failure in self.failures: if failure["s104"] is not None: print( - "%s%-8s %25s %25s %s%25s" - % ( - Fore.WHITE, - failure["asset"], - "{:0,f}".format(failure["audit"].normalize()), - "{:0,f}".format(failure["s104"].normalize()), - Fore.RED, - "{:+0,f}".format((failure["s104"] - failure["audit"]).normalize()), - ) + f'{Fore.WHITE}{failure["asset"]:<8} {failure["audit"].normalize():25,f} ' + f'{failure["s104"].normalize():25,f} ' + f'{Fore.RED}{(failure["s104"] - failure["audit"]).normalize():+25,f}' ) else: print( - "%s%-8s %25s %s%25s" - % ( - Fore.WHITE, - failure["asset"], - "{:0,f}".format(failure["audit"].normalize()), - Fore.RED, - "", - ) + f'{Fore.WHITE}{failure["asset"]<8} {failure["audit"].normalize():25,f} ' + f'{Fore.RED}{"":>25}' ) diff --git a/src/bittytax/bittytax.py b/src/bittytax/bittytax.py index 9b855720..279d67f7 100644 --- a/src/bittytax/bittytax.py +++ b/src/bittytax/bittytax.py @@ -10,10 +10,11 @@ import sys import colorama -from colorama import Back, Fore +from colorama import Fore from .audit import AuditRecords from .config import config +from .constants import ERROR, TAX_RULES_UK_COMPANY, TAX_RULES_UK_INDIVIDUAL, WARNING from .exceptions import ImportFailureError from .export_records import ExportRecords from .import_records import ImportRecords @@ -28,10 +29,8 @@ if sys.stdout.encoding != "UTF-8": if sys.version_info[:2] >= (3, 7): sys.stdout.reconfigure(encoding="utf-8") - elif sys.version_info[:2] >= (3, 1): - sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) else: - sys.stdout = codecs.getwriter("utf-8")(sys.stdout) + sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) def main(): @@ -47,22 +46,24 @@ def main(): "-v", "--version", action="version", - version="%s v%s" % (parser.prog, __version__), + version=f"{parser.prog} v{__version__}", ) parser.add_argument("-d", "--debug", action="store_true", help="enable debug logging") parser.add_argument( "-ty", "--taxyear", type=validate_year, - help="tax year must be in the range (%s-%s)" - % (min(CCG.CG_DATA_INDIVIDUAL), max(CCG.CG_DATA_INDIVIDUAL)), + help=( + f"tax year must be in the range " + f"({min(CCG.CG_DATA_INDIVIDUAL)}-{max(CCG.CG_DATA_INDIVIDUAL)})" + ), ) parser.add_argument( "--taxrules", - choices=[config.TAX_RULES_UK_INDIVIDUAL] + config.TAX_RULES_UK_COMPANY, + choices=[TAX_RULES_UK_INDIVIDUAL] + TAX_RULES_UK_COMPANY, metavar="{UK_INDIVIDUAL, UK_COMPANY_XXX} " "where XXX is the month which starts the financial year, i.e. JAN, FEB, etc.", - default=str(config.TAX_RULES_UK_INDIVIDUAL), + default=TAX_RULES_UK_INDIVIDUAL, type=str.upper, dest="tax_rules", help="specify tax rules to use, default: UK_INDIVIDUAL", @@ -99,22 +100,19 @@ def main(): config.debug = args.debug if config.debug: - print("%s%s v%s" % (Fore.YELLOW, parser.prog, __version__)) - print("%spython: v%s" % (Fore.GREEN, platform.python_version())) - print("%ssystem: %s, release: %s" % (Fore.GREEN, platform.system(), platform.release())) - config.output_config() + print(f"{Fore.YELLOW}{parser.prog} v{__version__}") + print(f"{Fore.GREEN}python: v{platform.python_version()}") + print(f"{Fore.GREEN}system: {platform.system()}, release: {platform.release()}") + config.output_config(sys.stdout) - if args.tax_rules in config.TAX_RULES_UK_COMPANY: - config.start_of_year_month = config.TAX_RULES_UK_COMPANY.index(args.tax_rules) + 1 + if args.tax_rules in TAX_RULES_UK_COMPANY: + config.start_of_year_month = TAX_RULES_UK_COMPANY.index(args.tax_rules) + 1 config.start_of_year_day = 1 try: transaction_records = do_import(args.filename) except IOError: - parser.exit( - "%sERROR%s File could not be read: %s" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, args.filename) - ) + parser.exit(f"{ERROR} File could not be read: {args.filename}") except ImportFailureError: parser.exit() @@ -137,7 +135,7 @@ def main(): do_each_tax_year(tax, args.taxyear, args.summary, value_asset) except DataSourceError as e: - parser.exit("%sERROR%s %s" % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, e)) + parser.exit(f"{ERROR} {e}") if args.nopdf: ReportLog(audit, tax.tax_report, value_asset.price_report, tax.holdings_report, args) @@ -156,8 +154,8 @@ def validate_year(value): year = int(value) if year not in CCG.CG_DATA_INDIVIDUAL: raise argparse.ArgumentTypeError( - "tax year %d is not supported, must be in the range (%s-%s)" - % (year, min(CCG.CG_DATA_INDIVIDUAL), max(CCG.CG_DATA_INDIVIDUAL)) + f"tax year {year} is not supported, must be in the range " + f"({min(CCG.CG_DATA_INDIVIDUAL)}-{max(CCG.CG_DATA_INDIVIDUAL)})", ) return year @@ -176,19 +174,11 @@ def do_import(filename): 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)) - else: - import_records.import_csv(sys.stdin) + import_records.import_csv(sys.stdin) print( - "%simport %s (success=%s, failure=%s)" - % ( - Fore.WHITE, - "successful" if import_records.failure_cnt <= 0 else "failure", - import_records.success_cnt, - import_records.failure_cnt, - ) + f"{Fore.WHITE}import {'successful' if import_records.failure_cnt <= 0 else 'failure'} " + f"(success={import_records.success_cnt}, failure={import_records.failure_cnt})" ) if import_records.failure_cnt > 0: @@ -205,9 +195,9 @@ def do_tax(transaction_records, tax_rules, skip_integrity_check): tax.pool_same_day() tax.match_sell(tax.DISPOSAL_SAME_DAY) - if tax_rules == config.TAX_RULES_UK_INDIVIDUAL: + if tax_rules == TAX_RULES_UK_INDIVIDUAL: tax.match_buyback(tax.DISPOSAL_BED_AND_BREAKFAST) - elif tax_rules in config.TAX_RULES_UK_COMPANY: + elif tax_rules in TAX_RULES_UK_COMPANY: tax.match_sell(tax.DISPOSAL_TEN_DAY) tax.process_section104(skip_integrity_check) @@ -227,28 +217,21 @@ def do_integrity_check(audit, holdings): if not pools_match or transfer_mismatch: int_passed = False - print( - "%sintegrity check: %s%s" % (Fore.CYAN, Fore.YELLOW, "passed" if int_passed else "failed") - ) + print(f"{Fore.CYAN}integrity check: {Fore.YELLOW}{'passed' if int_passed else 'failed'}") if transfer_mismatch: print( - "%sWARNING%s Integrity check failed: disposal(s) detected during transfer, " - "turn on logging [-d] to see transactions" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW) + f"{WARNING} Integrity check failed: disposal(s) detected during transfer, " + f"turn on logging [-d] to see transactions" ) elif not pools_match: if not config.transfers_include: print( - "%sWARNING%s Integrity check failed: audit does not match section 104 pools, " - "please check Withdrawals and Deposits for missing fees" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW) + f"{WARNING} Integrity check failed: audit does not match section 104 pools, " + f"please check Withdrawals and Deposits for missing fees" ) else: - print( - "%sERROR%s Integrity check failed: audit does not match section 104 pools" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED) - ) + print(f"{ERROR} Integrity check failed: audit does not match section 104 pools") audit.report_failures() return int_passed @@ -259,7 +242,7 @@ def transfer_mismatches(holdings): def do_each_tax_year(tax, tax_year, summary, value_asset): if tax_year: - print("%scalculating tax year %s" % (Fore.CYAN, config.format_tax_year(tax_year))) + print(f"{Fore.CYAN}calculating tax year {config.format_tax_year(tax_year)}") tax.calculate_capital_gains(tax_year) if not summary: @@ -267,17 +250,14 @@ def do_each_tax_year(tax, tax_year, summary, value_asset): else: # Calculate for all years for year in sorted(tax.tax_events): - print("%scalculating tax year %s" % (Fore.CYAN, config.format_tax_year(year))) + print(f"{Fore.CYAN}calculating tax year {config.format_tax_year(year)}") if year in CCG.CG_DATA_INDIVIDUAL: tax.calculate_capital_gains(year) if not summary: tax.calculate_income(year) else: - print( - "%sWARNING%s Tax year %s is not supported" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, year) - ) + print(f"{WARNING} Tax year {year} is not supported") if not summary: tax.calculate_holdings(value_asset) diff --git a/src/bittytax/config.py b/src/bittytax/config.py index 517cebbc..1e8010f2 100644 --- a/src/bittytax/config.py +++ b/src/bittytax/config.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # (c) Nano Nano Ltd 2019 -from __future__ import unicode_literals - import os import sys from datetime import datetime, timedelta @@ -10,40 +8,19 @@ import dateutil.tz import pkg_resources import yaml -from colorama import Back, Fore +from colorama import Fore +from .constants import BITTYTAX_PATH, ERROR -class Config(object): - TZ_LOCAL = dateutil.tz.gettz("Europe/London") - TZ_UTC = dateutil.tz.UTC - BITTYTAX_PATH = os.path.expanduser("~/.bittytax") +class Config: BITTYTAX_CONFIG = "bittytax.conf" - CACHE_DIR = os.path.join(BITTYTAX_PATH, "cache") + + TZ_LOCAL = dateutil.tz.gettz("Europe/London") FIAT_LIST = ["GBP", "EUR", "USD"] CRYPTO_LIST = ["BTC", "ETH", "XRP", "LTC", "BCH", "USDT"] - FORMAT_CSV = "CSV" - FORMAT_EXCEL = "EXCEL" - FORMAT_RECAP = "RECAP" - - TAX_RULES_UK_INDIVIDUAL = "UK_INDIVIDUAL" - TAX_RULES_UK_COMPANY = [ - "UK_COMPANY_JAN", - "UK_COMPANY_FEB", - "UK_COMPANY_MAR", - "UK_COMPANY_APR", - "UK_COMPANY_MAY", - "UK_COMPANY_JUN", - "UK_COMPANY_JUL", - "UK_COMPANY_AUG", - "UK_COMPANY_SEP", - "UK_COMPANY_OCT", - "UK_COMPANY_NOV", - "UK_COMPANY_DEC", - ] - TRADE_ASSET_TYPE_BUY = 0 TRADE_ASSET_TYPE_SELL = 1 TRADE_ASSET_TYPE_PRIORITY = 2 @@ -78,42 +55,28 @@ class Config(object): def __init__(self): self.debug = False - self.output = sys.stdout self.start_of_year_month = 4 self.start_of_year_day = 6 - if not os.path.exists(Config.BITTYTAX_PATH): - os.mkdir(Config.BITTYTAX_PATH) + if not os.path.exists(BITTYTAX_PATH): + os.mkdir(BITTYTAX_PATH) - if not os.path.exists(os.path.join(Config.BITTYTAX_PATH, Config.BITTYTAX_CONFIG)): - default_conf = pkg_resources.resource_string( - __name__, "config/" + Config.BITTYTAX_CONFIG - ) - with open( - os.path.join(Config.BITTYTAX_PATH, Config.BITTYTAX_CONFIG), "wb" - ) as config_file: + if not os.path.exists(os.path.join(BITTYTAX_PATH, self.BITTYTAX_CONFIG)): + default_conf = pkg_resources.resource_string(__name__, "config/" + self.BITTYTAX_CONFIG) + with open(os.path.join(BITTYTAX_PATH, self.BITTYTAX_CONFIG), "wb") as config_file: config_file.write(default_conf) try: - with open( - os.path.join(Config.BITTYTAX_PATH, Config.BITTYTAX_CONFIG), "rb" - ) as config_file: + with open(os.path.join(BITTYTAX_PATH, self.BITTYTAX_CONFIG), "rb") as config_file: self.config = yaml.safe_load(config_file) except IOError: sys.stderr.write( - "%sERROR%s Config file cannot be loaded: %s\n" - % ( - Back.RED + Fore.BLACK, - Back.RESET + Fore.RED, - os.path.join(Config.BITTYTAX_PATH, Config.BITTYTAX_CONFIG), - ) + f"{ERROR}Config file cannot be loaded: " + f"{os.path.join(BITTYTAX_PATH, self.BITTYTAX_CONFIG)}\n" ) sys.exit(1) except yaml.scanner.ScannerError as e: - sys.stderr.write( - "%sERROR%s Config file contains an error:\n%s\n" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, e) - ) + sys.stderr.write(f"{ERROR}Config file contains an error:\n{e}\n") sys.exit(1) for name, default in self.DEFAULT_CONFIG.items(): @@ -126,17 +89,13 @@ def __init__(self): def __getattr__(self, name): return self.config[name] - def output_config(self): - self.output.write( - '%sconfig: "%s"\n' - % ( - Fore.GREEN, - os.path.join(Config.BITTYTAX_PATH, Config.BITTYTAX_CONFIG), - ) + def output_config(self, sys_out): + sys_out.write( + f'{Fore.GREEN}config: "{os.path.join(BITTYTAX_PATH, self.BITTYTAX_CONFIG)}"\n' ) for name in self.DEFAULT_CONFIG: - self.output.write("%sconfig: %s: %s\n" % (Fore.GREEN, name, self.config[name])) + sys_out.write(f"{Fore.GREEN}config: {name}: {self.config[name]}\n") def sym(self): if self.ccy == "GBP": @@ -185,8 +144,8 @@ def format_tax_year(self, tax_year): end = self.get_tax_year_end(tax_year) if start.year == end.year: - return start.strftime("%Y") - return "{}/{}".format(start.strftime("%Y"), end.strftime("%y")) + return f"{start:%Y}" + return f"{start:%Y}/{end:%y}" config = Config() diff --git a/src/bittytax/constants.py b/src/bittytax/constants.py new file mode 100644 index 00000000..6eaf253b --- /dev/null +++ b/src/bittytax/constants.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# (c) Nano Nano Ltd 2023 + +import os + +import dateutil.tz +from colorama import Back, Fore, Style + +TZ_UTC = dateutil.tz.UTC + +BITTYTAX_PATH = os.path.expanduser("~/.bittytax") +CACHE_DIR = os.path.join(BITTYTAX_PATH, "cache") + +FORMAT_CSV = "CSV" +FORMAT_EXCEL = "EXCEL" +FORMAT_RECAP = "RECAP" + +TAX_RULES_UK_INDIVIDUAL = "UK_INDIVIDUAL" +TAX_RULES_UK_COMPANY = [ + "UK_COMPANY_JAN", + "UK_COMPANY_FEB", + "UK_COMPANY_MAR", + "UK_COMPANY_APR", + "UK_COMPANY_MAY", + "UK_COMPANY_JUN", + "UK_COMPANY_JUL", + "UK_COMPANY_AUG", + "UK_COMPANY_SEP", + "UK_COMPANY_OCT", + "UK_COMPANY_NOV", + "UK_COMPANY_DEC", +] + +WARNING = f"{Back.YELLOW}{Fore.BLACK}WARNING{Back.RESET}{Fore.YELLOW}" +ERROR = f"{Back.RED}{Fore.BLACK}ERROR{Back.RESET}{Fore.RED}" + +H1 = f"\n{Fore.CYAN}{Style.BRIGHT}" +_H1 = f"{Style.NORMAL}" diff --git a/src/bittytax/conv/bittytax_conv.py b/src/bittytax/conv/bittytax_conv.py index 010003fe..a4c149e6 100755 --- a/src/bittytax/conv/bittytax_conv.py +++ b/src/bittytax/conv/bittytax_conv.py @@ -13,6 +13,7 @@ from colorama import Fore from ..config import config +from ..constants import FORMAT_CSV, FORMAT_EXCEL, FORMAT_RECAP from ..version import __version__ from .datafile import DataFile from .datamerge import DataMerge @@ -29,16 +30,14 @@ if sys.stderr.encoding != "UTF-8": if sys.version_info[:2] >= (3, 7): sys.stderr.reconfigure(encoding="utf-8") - elif sys.version_info[:2] >= (3, 1): - sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach()) else: - sys.stderr = codecs.getwriter("utf-8")(sys.stderr) + sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach()) def main(): colorama.init() parser = argparse.ArgumentParser( - epilog="supported data file formats:\n" + DataParser.format_parsers(), + epilog=f"supported data file formats:\n{DataParser.format_parsers()}", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("filename", type=str, nargs="+", help="filename of data file") @@ -46,7 +45,7 @@ def main(): "-v", "--version", action="version", - version="%s v%s" % (parser.prog, __version__), + version=f"{parser.prog} v{__version__}", ) parser.add_argument("-d", "--debug", action="store_true", help="enable debug logging") parser.add_argument( @@ -68,8 +67,8 @@ def main(): ) parser.add_argument( "--format", - choices=[str(config.FORMAT_EXCEL), str(config.FORMAT_CSV), str(config.FORMAT_RECAP)], - default=str(config.FORMAT_EXCEL), + choices=[FORMAT_EXCEL, FORMAT_CSV, FORMAT_RECAP], + default=FORMAT_EXCEL, type=str.upper, help="specify the output format, default: EXCEL", ) @@ -87,20 +86,19 @@ def main(): args = parser.parse_args() config.debug = args.debug - config.output = sys.stderr DataFile.remove_duplicates = args.duplicates if config.debug: - sys.stderr.write("%s%s v%s\n" % (Fore.YELLOW, parser.prog, __version__)) - sys.stderr.write("%spython: v%s\n" % (Fore.GREEN, platform.python_version())) + sys.stderr.write(f"{Fore.YELLOW}{parser.prog} v{__version__}\n") + sys.stderr.write(f"{Fore.GREEN}python: v{platform.python_version()}\n") sys.stderr.write( - "%ssystem: %s, release: %s\n" % (Fore.GREEN, platform.system(), platform.release()) + f"{Fore.GREEN}system: {platform.system()}, release: {platform.release()}\n" ) - config.output_config() + config.output_config(sys.stderr) file_hashes = set() for filename in args.filename: - pathnames = glob.glob(filename) + pathnames = glob.glob(filename, recursive=True) if not pathnames: pathnames = [filename] @@ -115,16 +113,16 @@ def main(): except UnknownCryptoassetError as e: sys.stderr.write(Fore.RESET) - parser.error("%s, please specify using the [-ca CRYPTOASSET] option" % e) + parser.error(f"{e}, please specify using the [-ca CRYPTOASSET] option") except UnknownUsernameError as e: sys.stderr.write(Fore.RESET) parser.exit( - "%s: error: %s, please specify usernames in the %s file" - % (parser.prog, e, config.BITTYTAX_CONFIG), + f"{parser.prog}: error: {e}, please specify usernames in the " + f"{config.BITTYTAX_CONFIG} file" ) except DataFilenameError as e: sys.stderr.write(Fore.RESET) - parser.exit("%s: error: %s" % (parser.prog, e)) + parser.exit(f"{parser.prog}: error: {e}") except DataFormatUnrecognised: sys.stderr.write(_file_msg(pathname, None, msg="unrecognised")) except IOError as e: @@ -138,7 +136,7 @@ def main(): if DataFile.data_files: DataMerge.match_merge(DataFile.data_files) - if args.format == config.FORMAT_EXCEL: + if args.format == FORMAT_EXCEL: output = OutputExcel(parser.prog, DataFile.data_files_ordered, args) output.write_excel() else: @@ -148,7 +146,7 @@ def main(): output.write_csv() else: sys.stderr.write(Fore.RESET) - parser.exit(3, "%s: error: no data files could be processed\n" % parser.prog) + parser.exit(3, f"{parser.prog}: error: no data file(s) could be processed\n") def _do_read_file(file_type, pathname, args): @@ -189,18 +187,11 @@ def _get_file_info(filename): def _file_msg(filename, worksheet_name, msg): if worksheet_name: - worksheet_str = " '%s'" % worksheet_name + worksheet_str = f" '{worksheet_name}'" else: worksheet_str = "" - return "%sfile: %s%s%s %s%s\n" % ( - Fore.WHITE, - Fore.YELLOW, - filename, - worksheet_str, - Fore.WHITE, - msg, - ) + return f"{Fore.WHITE}file: {Fore.YELLOW}{filename}{worksheet_str} {Fore.WHITE}{msg}\n" if __name__ == "__main__": diff --git a/src/bittytax/conv/datafile.py b/src/bittytax/conv/datafile.py index 10572cc1..c397570e 100644 --- a/src/bittytax/conv/datafile.py +++ b/src/bittytax/conv/datafile.py @@ -7,16 +7,17 @@ import warnings import xlrd -from colorama import Back, Fore +from colorama import Fore from openpyxl import load_workbook from ..config import config +from ..constants import ERROR, WARNING from .dataparser import DataParser from .datarow import DataRow from .exceptions import DataFormatUnrecognised, DataRowError -class DataFile(object): +class DataFile: CSV_DELIMITERS = (",", ";") remove_duplicates = False @@ -48,13 +49,8 @@ def __iadd__(self, other): else: if [dr for dr in other.data_rows if dr in self.data_rows]: sys.stderr.write( - '%sWARNING%s Duplicate rows detected for "%s", ' - "use the [--duplicates] option to remove them (use with care!)\n" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - self.parser.name, - ) + f'{WARNING} Duplicate rows detected for "{self.parser.name}", ' + f"use the [--duplicates] option to remove them (use with care)\n" ) self.data_rows += other.data_rows @@ -65,12 +61,8 @@ def parse(self, **kwargs): for data_row in self.data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % ( - Fore.YELLOW, - self.parser.in_header_row_num + data_row.line_num, - data_row, - ) + f"{Fore.YELLOW}conv: " + f"row[{self.parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) data_row.parse(self.parser, **kwargs) @@ -81,33 +73,17 @@ def parse(self, **kwargs): self.failures = [dr for dr in self.data_rows if dr.failure is not None] if self.failures: - sys.stderr.write( - '%sWARNING%s Parser failure for "%s"\n' - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - self.parser.name, - ) - ) + sys.stderr.write(f'{WARNING} Parser failure for "{self.parser.name}"\n') + for data_row in self.failures: sys.stderr.write( - "%srow[%s] %s\n" - % ( - Fore.YELLOW, - self.parser.in_header_row_num + data_row.line_num, - data_row, - ) + f"{Fore.YELLOW}" + f"row[{self.parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if isinstance(data_row.failure, DataRowError): - sys.stderr.write( - "%sERROR%s %s\n" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, data_row.failure) - ) + sys.stderr.write(f"{ERROR} {data_row.failure}\n") else: - sys.stderr.write( - '%sERROR%s Unexpected error: "%s"\n' - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, data_row.failure) - ) + sys.stderr.write(f'{ERROR} Unexpected error: "{data_row.failure}"\n') @classmethod def read_excel_xlsx(cls, filename): @@ -117,15 +93,15 @@ def read_excel_xlsx(cls, filename): workbook = load_workbook(df, read_only=False, data_only=True) if config.debug: - sys.stderr.write("%sconv: EXCEL\n" % Fore.CYAN) + sys.stderr.write(f"{Fore.CYAN}conv: EXCEL\n") for sheet_name in workbook.sheetnames: yield workbook[sheet_name] workbook.close() del workbook - except (IOError, KeyError): - raise DataFormatUnrecognised(filename) + except (IOError, KeyError) as e: + raise DataFormatUnrecognised(filename) from e @classmethod def read_worksheet_xlsx(cls, worksheet, filename, args): @@ -136,26 +112,13 @@ def read_worksheet_xlsx(cls, worksheet, filename, args): raise DataFormatUnrecognised(filename, worksheet.title) sys.stderr.write( - "%sfile: %s%s '%s' %smatched as %s\"%s\"\n" - % ( - Fore.WHITE, - Fore.YELLOW, - filename, - worksheet.title, - Fore.WHITE, - Fore.CYAN, - parser.name, - ) + f"{Fore.WHITE}file: {Fore.YELLOW}{filename} '{worksheet.title}' " + f'{Fore.WHITE}matched as {Fore.CYAN}"{parser.name}"\n' ) if parser.deprecated: sys.stderr.write( - '%sWARNING%s This parser is deprecated, please use "%s"\n' - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - parser.deprecated.name, - ) + f'{WARNING} This parser is deprecated, please use "{parser.deprecated.name}"\n' ) data_file = DataFile(parser, reader) @@ -173,12 +136,12 @@ def read_excel_xls(cls, filename): try: with xlrd.open_workbook(filename) as workbook: if config.debug: - sys.stderr.write("%sconv: EXCEL\n" % Fore.CYAN) + sys.stderr.write(f"{Fore.CYAN}conv: EXCEL\n") for worksheet in workbook.sheets(): yield worksheet, workbook.datemode - except (xlrd.XLRDError, xlrd.compdoc.CompDocError): - raise DataFormatUnrecognised(filename) + except (xlrd.XLRDError, xlrd.compdoc.CompDocError) as e: + raise DataFormatUnrecognised(filename) from e @classmethod def read_worksheet_xls(cls, worksheet, datemode, filename, args): @@ -189,26 +152,13 @@ def read_worksheet_xls(cls, worksheet, datemode, filename, args): raise DataFormatUnrecognised(filename, worksheet.name) sys.stderr.write( - "%sfile: %s%s '%s' %smatched as %s\"%s\"\n" - % ( - Fore.WHITE, - Fore.YELLOW, - filename, - worksheet.name, - Fore.WHITE, - Fore.CYAN, - parser.name, - ) + f"{Fore.WHITE}file: {Fore.YELLOW}{filename} '{worksheet.name}' " + f'{Fore.WHITE}matched as {Fore.CYAN}"{parser.name}"\n' ) if parser.deprecated: sys.stderr.write( - '%sWARNING%s This parser is deprecated, please use "%s"\n' - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - parser.deprecated.name, - ) + f'{WARNING} This parser is deprecated, please use "{parser.deprecated.name}"\n' ) data_file = DataFile(parser, reader) @@ -231,8 +181,6 @@ def convert_cell_xlsx(cell): if cell.value is None: return "" - if sys.version_info[0] < 3 and cell.data_type == "s": - return cell.value.encode("utf-8") return str(cell.value) @staticmethod @@ -243,8 +191,8 @@ def get_cell_values_xls(rows, datemode): @staticmethod def convert_cell_xls(cell, datemode): if cell.ctype == xlrd.XL_CELL_DATE: - value = xlrd.xldate.xldate_as_datetime(cell.value, datemode).strftime( - "%Y-%m-%dT%H:%M:%S.%f %Z" + value = ( + f"{xlrd.xldate.xldate_as_datetime(cell.value, datemode):%Y-%m-%dT%H:%M:%S.%f %Z}" ) elif cell.ctype in ( xlrd.XL_CELL_NUMBER, @@ -254,10 +202,7 @@ def convert_cell_xls(cell, datemode): # repr is required to ensure no precision is lost value = repr(cell.value) else: - if sys.version_info[0] >= 3: - value = str(cell.value) - else: - value = cell.value.encode("utf-8") + value = str(cell.value) return value @@ -268,25 +213,14 @@ def read_csv(cls, filename, args): if parser is not None: sys.stderr.write( - '%sfile: %s%s %smatched as %s"%s"\n' - % ( - Fore.WHITE, - Fore.YELLOW, - filename, - Fore.WHITE, - Fore.CYAN, - parser.name, - ) + f"{Fore.WHITE}file: {Fore.YELLOW}{filename} " + f'{Fore.WHITE}matched as {Fore.CYAN}"{parser.name}"\n' ) if parser.deprecated: sys.stderr.write( - '%sWARNING%s This parser is deprecated, please use "%s"\n' - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - parser.deprecated.name, - ) + f"{WARNING} This parser is deprecated, please use " + f'"{parser.deprecated.name}"\n' ) data_file = DataFile(parser, reader) @@ -307,15 +241,9 @@ def read_csv_with_delimiter(cls, filename): with io.open(filename, newline="", encoding="utf-8-sig") as csv_file: for delimiter in cls.CSV_DELIMITERS: if config.debug: - sys.stderr.write("%sconv: CSV delimiter='%s'\n" % (Fore.CYAN, delimiter)) - - if sys.version_info[0] < 3: - # Special handling required for utf-8 encoded CSV files - reader = csv.reader(cls.utf_8_encoder(csv_file), delimiter=delimiter) - else: - reader = csv.reader(csv_file, delimiter=delimiter) + sys.stderr.write(f"{Fore.CYAN}conv: CSV delimiter='{delimiter}'\n") - yield reader + yield csv.reader(csv_file, delimiter=delimiter) csv_file.seek(0) @classmethod @@ -326,11 +254,6 @@ def consolidate_datafiles(cls, data_file): cls.data_files[data_file] = data_file cls.data_files_ordered.append(data_file) - @staticmethod - def utf_8_encoder(unicode_csv_data): - for line in unicode_csv_data: - yield line.encode("utf-8") - @staticmethod def get_parser(reader): parser = None diff --git a/src/bittytax/conv/datamerge.py b/src/bittytax/conv/datamerge.py index 906c5064..9f5f709e 100644 --- a/src/bittytax/conv/datamerge.py +++ b/src/bittytax/conv/datamerge.py @@ -4,16 +4,17 @@ import sys from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore from ..config import config +from ..constants import ERROR -class DataMerge(object): # pylint: disable=too-few-public-methods +class DataMerge: # pylint: disable=too-few-public-methods OPT = "Optional" MAN = "Mandatory" - SEPARATOR_AND = '"' + Fore.WHITE + " & " + Fore.CYAN + '"' + SEPARATOR_AND = f'"{Fore.WHITE} & {Fore.CYAN}"' mergers = [] @@ -46,7 +47,7 @@ def match_merge(cls, data_files): opt_cnt += 1 if man_cnt == 1 and opt_cnt > 0 or man_cnt > 1 and man_cnt == man_tot: - sys.stderr.write('%smerge: "%s"\n' % (Fore.WHITE, data_merge.name)) + sys.stderr.write(f'{Fore.WHITE}merge: "{data_merge.name}"\n') try: merge = data_merge.merge_handler(matched_data_files) @@ -54,22 +55,19 @@ def match_merge(cls, data_files): if config.debug: raise - sys.stderr.write( - '%sERROR%s Unexpected error: "%s"\n' - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, e) - ) + sys.stderr.write(f'{ERROR} Unexpected error: "{e}"\n') else: if merge: - parsers = [matched_data_files[df].parser.name for df in matched_data_files] + parsers = [df.parser.name for _, df in matched_data_files.items()] sys.stderr.write( - '%smerge: successfully merged %s"%s"\n' - % (Fore.WHITE, Fore.CYAN, cls.SEPARATOR_AND.join(parsers)) + f"{Fore.WHITE}merge: successfully merged " + f'{Fore.CYAN}"{cls.SEPARATOR_AND.join(parsers)}"\n' ) - for match in matched_data_files: - del data_files[matched_data_files[match]] + for _, df in matched_data_files.items(): + del data_files[df] else: - sys.stderr.write("%smerge: nothing to merge\n" % Fore.YELLOW) + sys.stderr.write(f"{Fore.YELLOW}merge: nothing to merge\n") @classmethod def _match_datafile(cls, data_files, parser): @@ -82,7 +80,7 @@ def _match_datafile(cls, data_files, parser): return None -class MergeDataRow(object): # pylint: disable=too-few-public-methods +class MergeDataRow: # pylint: disable=too-few-public-methods def __init__(self, data_row, data_file, data_file_id): self.data_row = data_row self.data_file = data_file diff --git a/src/bittytax/conv/dataparser.py b/src/bittytax/conv/dataparser.py index 034282f2..d6e8df78 100644 --- a/src/bittytax/conv/dataparser.py +++ b/src/bittytax/conv/dataparser.py @@ -10,12 +10,13 @@ from colorama import Fore, Style from ..config import config +from ..constants import TZ_UTC from ..price.pricedata import PriceData TERM_WIDTH = 69 -class DataParser(object): # pylint: disable=too-many-instance-attributes +class DataParser: # pylint: disable=too-many-instance-attributes TYPE_WALLET = "Wallets" TYPE_EXCHANGE = "Exchanges" TYPE_SAVINGS = "Savings, Loans & Investments" @@ -78,9 +79,9 @@ def format_header(self): else: header.append(col) - header_str = "'" + str(self.delimiter).join(header) + "'" + header_str = f"'{self.delimiter.join(header)}'" - return header_str[:TERM_WIDTH] + "..." if len(header_str) > TERM_WIDTH else header_str + return f"{header_str[:TERM_WIDTH]}..." if len(header_str) > TERM_WIDTH else header_str @classmethod def parse_timestamp(cls, timestamp_str, tzinfos=None, tz=None, dayfirst=False, fuzzy=False): @@ -93,12 +94,12 @@ def parse_timestamp(cls, timestamp_str, tzinfos=None, tz=None, dayfirst=False, f if tz: timestamp = timestamp.replace(tzinfo=dateutil.tz.gettz(tz)) - timestamp = timestamp.astimezone(config.TZ_UTC) + timestamp = timestamp.astimezone(TZ_UTC) elif timestamp.tzinfo is None: # Default to UTC if no timezone is specified - timestamp = timestamp.replace(tzinfo=config.TZ_UTC) + timestamp = timestamp.replace(tzinfo=TZ_UTC) else: - timestamp = timestamp.astimezone(config.TZ_UTC) + timestamp = timestamp.astimezone(TZ_UTC) return timestamp @@ -125,20 +126,10 @@ def convert_currency(cls, value, from_currency, timestamp): if config.debug: print( - "%sprice: %s, 1 %s=%s %s, %s %s=%s%s %s%s" - % ( - Fore.YELLOW, - timestamp.strftime("%Y-%m-%d"), - from_currency, - config.sym() + "{:0,.2f}".format(rate_ccy), - config.ccy, - "{:0,f}".format(Decimal(value).normalize()), - from_currency, - Style.BRIGHT, - config.sym() + "{:0,.2f}".format(value_in_ccy), - config.ccy, - Style.NORMAL, - ) + f"{Fore.YELLOW}price: {timestamp:%Y-%m-%d}, 1 {from_currency}=" + f"{config.sym()}{rate_ccy:0,.2f} {config.ccy}, {Decimal(value).normalize():0,f}" + f"{from_currency}{Style.BRIGHT}{config.sym()}{value_in_ccy:0,.2f} " + f"{config.ccy}{Style.NORMAL}" ) return value_in_ccy @@ -148,7 +139,7 @@ def match_header(cls, row, row_num): row = [col.strip() for col in row] if config.debug: sys.stderr.write( - "%sheader: row[%s] TRY: %s\n" % (Fore.YELLOW, row_num + 1, cls.format_row(row)) + f"{Fore.YELLOW}header: row[{row_num + 1}] TRY: {cls.format_row(row)}\n" ) parsers_reduced = [p for p in cls.parsers if len(p.header) == len(row)] @@ -167,13 +158,8 @@ def match_header(cls, row, row_num): if match: if config.debug: sys.stderr.write( - "%sheader: row[%s] MATCHED: %s as '%s'\n" - % ( - Fore.CYAN, - row_num + 1, - cls.format_row(parser.header), - parser.name, - ) + f"{Fore.CYAN}header: row[{row_num + 1}] " + f"MATCHED: {cls.format_row(parser.header)} as '{parser.name}'\n" ) parser.in_header = row parser.in_header_row_num = row_num + 1 @@ -181,13 +167,8 @@ def match_header(cls, row, row_num): if config.debug: sys.stderr.write( - "%sheader: row[%s] NO MATCH: %s '%s'\n" - % ( - Fore.BLUE, - row_num + 1, - cls.format_row(parser.header), - parser.name, - ) + f"{Fore.BLUE}header: row[{row_num + 1}] " + f"NO MATCH: {cls.format_row(parser.header)} '{parser.name}'\n" ) raise KeyError @@ -196,12 +177,12 @@ def match_header(cls, row, row_num): def format_parsers(cls): txt = "" for p_type in cls.LIST_ORDER: - txt += " " * 2 + p_type.upper() + ":\n" + txt += f" {p_type.upper()}:\n" prev_name = None for parser in sorted([parser for parser in cls.parsers if parser.p_type == p_type]): if parser.name != prev_name: - txt += " " * 4 + parser.name + "\n" - txt += " " * 6 + parser.format_header() + "\n" + txt += f" {parser.name}\n" + txt += f" {parser.format_header()}\n" prev_name = parser.name @@ -216,6 +197,6 @@ def format_row(row): elif col is None: row_out.append("*") else: - row_out.append("'%s'" % col) + row_out.append(f"'{col}'") - return "[" + ", ".join(row_out) + "]" + return f"[{', '.join(row_out)}]" diff --git a/src/bittytax/conv/datarow.py b/src/bittytax/conv/datarow.py index bba7f13e..f5bff748 100644 --- a/src/bittytax/conv/datarow.py +++ b/src/bittytax/conv/datarow.py @@ -6,14 +6,15 @@ from colorama import Back, Fore from ..config import config +from ..constants import TZ_UTC from .exceptions import DataRowError from .mergers import * # pylint: disable=wildcard-import, unused-wildcard-import from .parsers import * # pylint: disable=wildcard-import, unused-wildcard-import -DEFAULT_TIMESTAMP = datetime.datetime(datetime.MINYEAR, 1, 1, tzinfo=config.TZ_UTC) +DEFAULT_TIMESTAMP = datetime.datetime(datetime.MINYEAR, 1, 1, tzinfo=TZ_UTC) -class DataRow(object): +class DataRow: def __init__(self, line_num, row, in_header): self.line_num = line_num self.row = row @@ -46,18 +47,17 @@ def parse_all(data_rows, parser, **kwargs): def __str__(self): if self.failure and isinstance(self.failure, DataRowError): - return ( - "[" - + ", ".join( - [ - "%s'%s'%s" % (Back.RED, data, Back.RESET) - if self.failure.col_num == num - else "'%s'" % data - for num, data in enumerate(self.row) - ] - ) - + "]" + row_str = ", ".join( + [ + f"{Back.RED}'{data}'{Back.RESET}" + if self.failure.col_num == num + else f"'{data}'" + for num, data in enumerate(self.row) + ] ) + return f"[{row_str}]" + + row_str = "', '".join(self.row) if self.failure: - return Fore.RED + "[" + "'%s'" % "', '".join(self.row) + "]" - return "[" + "'%s'" % "', '".join(self.row) + "]" + return f"{Fore.RED}['{row_str}']" + return f"['{row_str}']" diff --git a/src/bittytax/conv/exceptions.py b/src/bittytax/conv/exceptions.py index ba9f6b18..41345244 100644 --- a/src/bittytax/conv/exceptions.py +++ b/src/bittytax/conv/exceptions.py @@ -4,7 +4,7 @@ class DataRowError(Exception): def __init__(self, col_num, col_name, value=None): - super(DataRowError, self).__init__() + super().__init__() self.col_num = col_num self.col_name = col_name self.value = value @@ -12,63 +12,60 @@ def __init__(self, col_num, col_name, value=None): class UnexpectedTypeError(DataRowError): def __str__(self): - return "Unrecognised %s: '%s'" % (self.col_name, self.value) + return f"Unrecognised {self.col_name}: '{self.value}'" class UnexpectedContentError(DataRowError): def __str__(self): - return "Unexpected %s content: '%s'" % (self.col_name, self.value) + return f"Unexpected {self.col_name} content: '{self.value}'" class MissingValueError(DataRowError): def __str__(self): - return "Missing value for '%s'" % self.col_name + return f"Missing value for '{self.col_name}'" class MissingComponentError(DataRowError): def __str__(self): - return "Missing component data for %s: '%s'" % (self.col_name, self.value) + return f"Missing component data for {self.col_name}: '{self.value}'" class UnexpectedTradingPairError(DataRowError): def __str__(self): - return "Unrecognised trading pair for %s: '%s'" % (self.col_name, self.value) + return f"Unrecognised trading pair for {self.col_name}: '{self.value}'" class DataParserError(Exception): def __init__(self, filename, worksheet=None): - super(DataParserError, self).__init__() + super().__init__() self.filename = filename self.worksheet = worksheet def format_filename(self): if self.worksheet: - return "%s '%s'" % (self.filename, self.worksheet) + return f"{self.filename} '{self.worksheet}'" return self.filename class UnknownCryptoassetError(DataParserError): def __str__(self): - return "Cryptoasset cannot be identified for data file: %s" % self.format_filename() + return f"Cryptoasset cannot be identified for data file: {self.format_filename()}" class UnknownUsernameError(DataParserError): def __str__(self): - return "Username cannot be identified in data file: %s" % self.format_filename() + return f"Username cannot be identified in data file: {self.format_filename()}" class DataFormatUnrecognised(DataParserError): def __str__(self): - return "Data file format is unrecognised: %s" % self.format_filename() + return f"Data file format is unrecognised: {self.format_filename()}" class DataFilenameError(DataParserError): def __init__(self, filename, component): - super(DataFilenameError, self).__init__(filename) + super().__init__(filename) self.component = component def __str__(self): - return "%s cannot be identified from filename: %s" % ( - self.component, - self.filename, - ) + return f"{self.component} cannot be identified from filename: {self.filename}" diff --git a/src/bittytax/conv/mergers/bscscan.py b/src/bittytax/conv/mergers/bscscan.py index d65604b3..dd57a318 100644 --- a/src/bittytax/conv/mergers/bscscan.py +++ b/src/bittytax/conv/mergers/bscscan.py @@ -21,14 +21,14 @@ def merge_bscscan(data_files): for data_row in data_files[TOKENS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" if NFTS in data_files: data_files[NFTS].parser.worksheet_name = WORKSHEET_NAME for data_row in data_files[NFTS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" return merge diff --git a/src/bittytax/conv/mergers/etherscan.py b/src/bittytax/conv/mergers/etherscan.py index d4d91fcf..cd5a9e8a 100644 --- a/src/bittytax/conv/mergers/etherscan.py +++ b/src/bittytax/conv/mergers/etherscan.py @@ -5,9 +5,10 @@ import sys from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore from ...config import config +from ...constants import WARNING from ..datamerge import DataMerge, MergeDataRow from ..exceptions import UnexpectedContentError from ..out_record import TransactionOutRecord @@ -51,39 +52,33 @@ def _do_merge_etherscan(data_files, staking_addresses): # pylint: disable=too-m MergeDataRow(dr, data_files[file_id], file_id) ) - for wallet in tx_ids: - for txn in tx_ids[wallet]: - if len(tx_ids[wallet][txn]) == 1: + for _, wallet_tx_ids in tx_ids.items(): + for txn in wallet_tx_ids: + if len(wallet_tx_ids[txn]) == 1: if config.debug: sys.stderr.write( - "%smerge: %s:%s\n" - % ( - Fore.BLUE, - tx_ids[wallet][txn][0].data_file_id.ljust(5), - tx_ids[wallet][txn][0].data_row, - ) + f"{Fore.BLUE}merge: {wallet_tx_ids[txn][0].data_file_id:<5}:" + f"{wallet_tx_ids[txn][0].data_row}\n" ) continue - for t in tx_ids[wallet][txn]: + for t in wallet_tx_ids[txn]: if config.debug: - sys.stderr.write( - "%smerge: %s:%s\n" % (Fore.GREEN, t.data_file_id.ljust(5), t.data_row) - ) + sys.stderr.write(f"{Fore.GREEN}merge: {t.data_file_id:<5}:{t.data_row}\n") - t_ins, t_outs, t_fee = _get_ins_outs(tx_ids[wallet][txn]) + t_ins, t_outs, t_fee = _get_ins_outs(wallet_tx_ids[txn]) if config.debug: _output_records(t_ins, t_outs, t_fee) - sys.stderr.write("%smerge: consolidate:\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: consolidate:\n") - _consolidate(tx_ids[wallet][txn], [TXNS, INTERNAL_TXNS]) + _consolidate(wallet_tx_ids[txn], [TXNS, INTERNAL_TXNS]) - t_ins, t_outs, t_fee = _get_ins_outs(tx_ids[wallet][txn]) + t_ins, t_outs, t_fee = _get_ins_outs(wallet_tx_ids[txn]) if config.debug: _output_records(t_ins, t_outs, t_fee) - sys.stderr.write("%smerge: merge:\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: merge:\n") if t_fee: fee_quantity = t_fee.t_record.fee_quantity @@ -100,24 +95,18 @@ def _do_merge_etherscan(data_files, staking_addresses): # pylint: disable=too-m _do_etherscan_multi_buy(t_ins, t_outs, t_fee) elif len(t_ins) > 1 and len(t_outs) > 1: # Multi-sell to multi-buy trade not supported - sys.stderr.write( - "%sWARNING%s Merge failure for Txhash: %s\n" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, txn) - ) + sys.stderr.write(f"{WARNING} Merge failure for Txhash: {txn}\n") - for mdr in tx_ids[wallet][txn]: + for mdr in wallet_tx_ids[txn]: mdr.data_row.failure = UnexpectedContentError( mdr.data_file.parser.in_header.index("Txhash"), "Txhash", mdr.data_row.row_dict["Txhash"], ) sys.stderr.write( - "%srow[%s] %s\n" - % ( - Fore.YELLOW, - mdr.data_file.parser.in_header_row_num + mdr.data_row.line_num, - mdr.data_row, - ) + f"{Fore.YELLOW}" + f"row[{mdr.data_file.parser.in_header_row_num + mdr.data_row.line_num}] " + f"{mdr.data_row}\n" ) continue @@ -176,8 +165,7 @@ def _consolidate(tx_ids, file_ids): txn.data_row.t_record = None tx_ids.remove(txn) - for asset in tx_assets: - txn = tx_assets[asset] + for _, txn in tx_assets.items(): if txn.quantity > 0: txn.data_row.t_record.t_type = TransactionOutRecord.TYPE_DEPOSIT txn.data_row.t_record.buy_asset = asset @@ -205,17 +193,15 @@ def _output_records(t_ins, t_outs, t_fee): dup = bool(t_fee and t_fee in t_ins + t_outs) if t_fee: - sys.stderr.write( - "%smerge: TR-F%s: %s\n" % (Fore.YELLOW, "*" if dup else "", t_fee.t_record) - ) + sys.stderr.write(f"{Fore.YELLOW}merge: TR-F{'*' if dup else ''}: {t_fee.t_record}\n") for t_in in t_ins: sys.stderr.write( - "%smerge: TR-I%s: %s\n" % (Fore.YELLOW, "*" if t_fee is t_in else "", t_in.t_record) + f"{Fore.YELLOW}merge: TR-I{'*' if t_fee is t_in else ''}: {t_in.t_record}\n" ) for t_out in t_outs: sys.stderr.write( - "%smerge: TR-O%s: %s\n" % (Fore.YELLOW, "*" if t_fee is t_out else "", t_out.t_record) + f"{Fore.YELLOW}merge: TR-O{'*' if t_fee is t_out else ''}: {t_out.t_record}\n" ) @@ -239,14 +225,14 @@ def _method_handling(t_ins, t_fee, staking_addresses): t_ins.remove(staking[0]) if config.debug: - sys.stderr.write("%smerge: staking:\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: staking:\n") else: raise ValueError("Multiple transactions") def _do_etherscan_multi_sell(t_ins, t_outs, t_fee): if config.debug: - sys.stderr.write("%smerge: trade sell(s):\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: trade sell(s):\n") tot_buy_quantity = 0 @@ -255,12 +241,8 @@ def _do_etherscan_multi_sell(t_ins, t_outs, t_fee): if config.debug: sys.stderr.write( - "%smerge: buy_quantity=%s buy_asset=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(buy_quantity), - buy_asset, - ) + f"{Fore.YELLOW}merge: buy_quantity=" + f"{TransactionOutRecord.format_quantity(buy_quantity)} buy_asset={buy_asset}\n" ) for cnt, t_out in enumerate(t_outs): @@ -273,11 +255,8 @@ def _do_etherscan_multi_sell(t_ins, t_outs, t_fee): if config.debug: sys.stderr.write( - "%smerge: split_buy_quantity=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(split_buy_quantity), - ) + f"{Fore.YELLOW}merge: split_buy_quantity=" + f"{TransactionOutRecord.format_quantity(split_buy_quantity)}\n" ) t_out.t_record.t_type = TransactionOutRecord.TYPE_TRADE @@ -292,7 +271,7 @@ def _do_etherscan_multi_sell(t_ins, t_outs, t_fee): def _do_etherscan_multi_buy(t_ins, t_outs, t_fee): if config.debug: - sys.stderr.write("%smerge: trade buy(s):\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: trade buy(s):\n") tot_sell_quantity = 0 @@ -301,12 +280,8 @@ def _do_etherscan_multi_buy(t_ins, t_outs, t_fee): if config.debug: sys.stderr.write( - "%smerge: sell_quantity=%s sell_asset=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(sell_quantity), - sell_asset, - ) + f"{Fore.YELLOW}merge: sell_quantity=" + f"{TransactionOutRecord.format_quantity(sell_quantity)} sell_asset={sell_asset}\n" ) for cnt, t_in in enumerate(t_ins): @@ -319,11 +294,8 @@ def _do_etherscan_multi_buy(t_ins, t_outs, t_fee): if config.debug: sys.stderr.write( - "%smerge: split_sell_quantity=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(split_sell_quantity), - ) + f"{Fore.YELLOW}merge: split_sell_quantity=" + f"{TransactionOutRecord.format_quantity(split_sell_quantity)}\n" ) t_in.t_record.t_type = TransactionOutRecord.TYPE_TRADE @@ -338,14 +310,10 @@ def _do_etherscan_multi_buy(t_ins, t_outs, t_fee): def _do_fee_split(t_all, t_fee, fee_quantity, fee_asset): if config.debug: - sys.stderr.write("%smerge: split fees:\n" % (Fore.YELLOW)) + sys.stderr.write(f"{Fore.YELLOW}merge: split fees:\n") sys.stderr.write( - "%smerge: fee_quantity=%s fee_asset=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(fee_quantity), - fee_asset, - ) + f"{Fore.YELLOW}merge: fee_quantity=" + f"{TransactionOutRecord.format_quantity(fee_quantity)} fee_asset={fee_asset}\n" ) tot_fee_quantity = 0 @@ -360,11 +328,8 @@ def _do_fee_split(t_all, t_fee, fee_quantity, fee_asset): if config.debug: sys.stderr.write( - "%smerge: split_fee_quantity=%s\n" - % ( - Fore.YELLOW, - TransactionOutRecord.format_quantity(split_fee_quantity), - ) + f"{Fore.YELLOW}merge: split_fee_quantity=" + f"{TransactionOutRecord.format_quantity(split_fee_quantity)}\n" ) t.t_record.fee_quantity = split_fee_quantity diff --git a/src/bittytax/conv/mergers/hecoinfo.py b/src/bittytax/conv/mergers/hecoinfo.py index fe959e63..430684ea 100644 --- a/src/bittytax/conv/mergers/hecoinfo.py +++ b/src/bittytax/conv/mergers/hecoinfo.py @@ -21,14 +21,14 @@ def merge_hecoinfo(data_files): for data_row in data_files[TOKENS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" if NFTS in data_files: data_files[NFTS].parser.worksheet_name = WORKSHEET_NAME for data_row in data_files[NFTS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" return merge diff --git a/src/bittytax/conv/mergers/polygonscan.py b/src/bittytax/conv/mergers/polygonscan.py index 5cd7f494..ee79120b 100644 --- a/src/bittytax/conv/mergers/polygonscan.py +++ b/src/bittytax/conv/mergers/polygonscan.py @@ -21,14 +21,14 @@ def merge_polygonscan(data_files): for data_row in data_files[TOKENS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" if NFTS in data_files: data_files[NFTS].parser.worksheet_name = WORKSHEET_NAME for data_row in data_files[NFTS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" return merge diff --git a/src/bittytax/conv/mergers/snowtrace.py b/src/bittytax/conv/mergers/snowtrace.py index bebe9213..2ac0cb54 100644 --- a/src/bittytax/conv/mergers/snowtrace.py +++ b/src/bittytax/conv/mergers/snowtrace.py @@ -21,14 +21,14 @@ def merge_snowtrace(data_files): for data_row in data_files[TOKENS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" if NFTS in data_files: data_files[NFTS].parser.worksheet_name = WORKSHEET_NAME for data_row in data_files[NFTS].data_rows: if data_row.t_record: address = data_row.t_record.wallet[-abs(TransactionOutRecord.WALLET_ADDR_LEN) :] - data_row.t_record.wallet = "%s-%s" % (WALLET, address) + data_row.t_record.wallet = f"{WALLET}-{address}" return merge diff --git a/src/bittytax/conv/out_record.py b/src/bittytax/conv/out_record.py index 3cdf63c6..814b3e34 100644 --- a/src/bittytax/conv/out_record.py +++ b/src/bittytax/conv/out_record.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- # (c) Nano Nano Ltd 2019 -import sys from decimal import Decimal from ..config import config from ..record import TransactionRecord -class TransactionOutRecord(object): # pylint: disable=too-many-instance-attributes +class TransactionOutRecord: # pylint: disable=too-many-instance-attributes TYPE_DEPOSIT = TransactionRecord.TYPE_DEPOSIT TYPE_MINING = TransactionRecord.TYPE_MINING TYPE_STAKING = TransactionRecord.TYPE_STAKING @@ -81,40 +80,40 @@ def __init__( def __str__(self): if self.t_type == self.TYPE_TRADE: - return "%s %s %s%s <- %s %s%s%s '%s' %s %s" % ( - self.t_type, - self.format_quantity(self.buy_quantity), - self.format_str(self.buy_asset), - self.format_value(self.buy_value), - self.format_quantity(self.sell_quantity), - self.format_str(self.sell_asset), - self.format_value(self.sell_value), - self.format_fee(), - self.format_str(self.wallet), - self.format_timestamp(self.timestamp), - self.format_note(self.note), + return ( + f"{self.t_type} " + f"{self.format_quantity(self.buy_quantity)} " + f"{self.buy_asset}" + f"{self.format_value(self.buy_value)} <- " + f"{self.format_quantity(self.sell_quantity)} " + f"{self.sell_asset}" + f"{self.format_value(self.sell_value)}" + f"{self.format_fee()} " + f"'{self.wallet}' " + f"{self.format_timestamp(self.timestamp)} " + f"{self.format_note(self.note)}" ) if self.t_type in self.BUY_TYPES: - return "%s %s %s%s%s '%s' %s %s" % ( - self.t_type, - self.format_quantity(self.buy_quantity), - self.format_str(self.buy_asset), - self.format_value(self.buy_value), - self.format_fee(), - self.format_str(self.wallet), - self.format_timestamp(self.timestamp), - self.format_note(self.note), + return ( + f"{self.t_type} " + f"{self.format_quantity(self.buy_quantity)} " + f"{self.buy_asset}" + f"{self.format_value(self.buy_value)}" + f"{self.format_fee()} " + f"'{self.wallet}' " + f"{self.format_timestamp(self.timestamp)} " + f"{self.format_note(self.note)}" ) if self.t_type in self.SELL_TYPES: - return "%s %s %s%s%s '%s' %s %s" % ( - self.t_type, - self.format_quantity(self.sell_quantity), - self.format_str(self.sell_asset), - self.format_value(self.sell_value), - self.format_fee(), - self.format_str(self.wallet), - self.format_timestamp(self.timestamp), - self.format_note(self.note), + return ( + f"{self.t_type} " + f"{self.format_quantity(self.sell_quantity)} " + f"{self.sell_asset}" + f"{self.format_value(self.sell_value)}" + f"{self.format_fee()} " + f"'{self.wallet}' " + f"{self.format_timestamp(self.timestamp)} " + f"{self.format_note(self.note)}" ) return [] @@ -137,39 +136,30 @@ def get_quantity(self): def format_quantity(quantity): if quantity is None: return "" - return "{:0,f}".format(quantity.normalize()) + return f"{quantity.normalize():0,f}" def format_fee(self): if self.fee_quantity: - return " + fee=%s %s%s" % ( - self.format_quantity(self.fee_quantity), - self.format_str(self.fee_asset), - self.format_value(self.fee_value), + return ( + f" + fee={self.format_quantity(self.fee_quantity)} " + f"{self.fee_asset}{self.format_value(self.fee_value)}" ) return "" @staticmethod def format_value(value): if value is not None: - return " (%s %s)" % (config.sym() + "{:0,.2f}".format(value), config.ccy) + return f" ({config.sym()}{value:0,.2f} {config.ccy})" return "" @staticmethod def format_note(note): if note: - if sys.version_info[0] < 3: - return "'%s' " % note.decode("utf8") - return "'%s' " % note + return f"'{note}' " return "" @staticmethod def format_timestamp(timestamp): if timestamp.microsecond: - return timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f %Z") - return timestamp.strftime("%Y-%m-%dT%H:%M:%S %Z") - - @staticmethod - def format_str(string): - if sys.version_info[0] < 3: - return string.decode("utf8") - return string + return f"{timestamp:%Y-%m-%dT%H:%M:%S.%f %Z}" + return f"{timestamp:%Y-%m-%dT%H:%M:%S %Z}" diff --git a/src/bittytax/conv/output_csv.py b/src/bittytax/conv/output_csv.py index 2288cbec..9d060223 100644 --- a/src/bittytax/conv/output_csv.py +++ b/src/bittytax/conv/output_csv.py @@ -5,13 +5,14 @@ import os import sys -from colorama import Back, Fore +from colorama import Fore from ..config import config +from ..constants import FORMAT_RECAP, WARNING from .out_record import TransactionOutRecord -class OutputBase(object): # pylint: disable=too-few-public-methods +class OutputBase: # pylint: disable=too-few-public-methods DEFAULT_FILENAME = "BittyTax_Records" EXCEL_PRECISION = 15 BITTYTAX_OUT_HEADER = [ @@ -38,19 +39,19 @@ def get_output_filename(filename, extension_type): if filename: filepath, file_extension = os.path.splitext(filename) if file_extension != extension_type: - filepath = filepath + "." + extension_type + filepath = f"{filepath}.{extension_type}" else: - filepath = OutputBase.DEFAULT_FILENAME + "." + extension_type + filepath = f"{OutputBase.DEFAULT_FILENAME}.{extension_type}" if not os.path.exists(filepath): return filepath filepath, file_extension = os.path.splitext(filepath) i = 2 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" while os.path.exists(new_fname): i += 1 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" return new_fname @@ -87,7 +88,7 @@ class OutputCsv(OutputBase): } def __init__(self, data_files, args): - super(OutputCsv, self).__init__(data_files) + super().__init__(data_files) if args.output_filename: self.filename = self.get_output_filename(args.output_filename, self.FILE_EXTENSION) else: @@ -99,35 +100,26 @@ def __init__(self, data_files, args): self.append_raw_data = args.append def out_header(self): - if self.csv_format == config.FORMAT_RECAP: + if self.csv_format == FORMAT_RECAP: return self.RECAP_OUT_HEADER return self.BITTYTAX_OUT_HEADER def in_header(self, in_header): - if self.csv_format == config.FORMAT_RECAP: + if self.csv_format == FORMAT_RECAP: return [name if name not in self.out_header() else name + "_" for name in in_header] return in_header def write_csv(self): if self.filename: - if sys.version_info[0] >= 3: - with open(self.filename, "w", newline="", encoding="utf-8") as csv_file: - writer = csv.writer(csv_file, lineterminator="\n") - self.write_rows(writer) - else: - with open(self.filename, "wb") as csv_file: - writer = csv.writer(csv_file, lineterminator="\n") - self.write_rows(writer) + with open(self.filename, "w", newline="", encoding="utf-8") as csv_file: + writer = csv.writer(csv_file, lineterminator="\n") + self.write_rows(writer) - sys.stderr.write( - "%soutput CSV file created: %s%s\n" % (Fore.WHITE, Fore.YELLOW, self.filename) - ) + sys.stderr.write(f"{Fore.WHITE}output CSV file created: {Fore.YELLOW}{self.filename}\n") else: - if sys.version_info[0] > 3: - sys.stdout.reconfigure(encoding="utf-8") - + sys.stdout.reconfigure(encoding="utf-8") writer = csv.writer(sys.stdout, lineterminator="\n") self.write_rows(writer) @@ -158,16 +150,22 @@ def write_rows(self, writer): writer.writerow(self._to_csv(data_row.t_record)) def _to_csv(self, t_record): - if self.csv_format == config.FORMAT_RECAP: + if self.csv_format == FORMAT_RECAP: return self._to_recap_csv(t_record) return self._to_bittytax_csv(t_record) + @staticmethod + def _format_decimal(decimal): + if decimal is None: + return "" + return f"{decimal.normalize():0f}" + @staticmethod def _format_timestamp(timestamp): if timestamp.microsecond: - return timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f %Z") - return timestamp.strftime("%Y-%m-%dT%H:%M:%S %Z") + return f"{timestamp:%Y-%m-%dT%H:%M:%S.%f %Z}" + return f"{timestamp:%Y-%m-%dT%H:%M:%S %Z}" @staticmethod def _to_bittytax_csv(tr): @@ -176,14 +174,8 @@ def _to_bittytax_csv(tr): and len(tr.buy_quantity.normalize().as_tuple().digits) > OutputBase.EXCEL_PRECISION ): sys.stderr.write( - "%sWARNING%s %d-digit precision exceeded for Buy Quantity: %s%s\n" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - OutputBase.EXCEL_PRECISION, - tr.format_quantity(tr.buy_quantity), - Fore.RESET, - ) + f"{WARNING} {OutputBase.EXCEL_PRECISION}-digit precision exceeded for " + f"Buy Quantity: {tr.format_quantity(tr.buy_quantity)}{Fore.RESET}\n" ) if ( @@ -191,14 +183,8 @@ def _to_bittytax_csv(tr): and len(tr.sell_quantity.normalize().as_tuple().digits) > OutputBase.EXCEL_PRECISION ): sys.stderr.write( - "%sWARNING%s %d-digit precision exceeded for Sell Quantity: %s%s\n" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - OutputBase.EXCEL_PRECISION, - tr.format_quantity(tr.sell_quantity), - Fore.RESET, - ) + f"{WARNING} {OutputBase.EXCEL_PRECISION}-digit precision exceeded for " + f"Sell Quantity: {tr.format_quantity(tr.sell_quantity)}{Fore.RESET}\n" ) if ( @@ -206,26 +192,20 @@ def _to_bittytax_csv(tr): and len(tr.fee_quantity.normalize().as_tuple().digits) > OutputBase.EXCEL_PRECISION ): sys.stderr.write( - "%sWARNING%s %d-digit precision exceeded for Fee Quantity: %s%s\n" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - OutputBase.EXCEL_PRECISION, - tr.format_quantity(tr.fee_quantity), - Fore.RESET, - ) + f"{WARNING} {OutputBase.EXCEL_PRECISION}-digit precision exceeded for" + f"Fee Quantity: {tr.format_quantity(tr.fee_quantity)}{Fore.RESET}\n" ) return [ tr.t_type, - "{0:f}".format(tr.buy_quantity.normalize()) if tr.buy_quantity is not None else None, + OutputCsv._format_decimal(tr.buy_quantity), tr.buy_asset, - "{0:f}".format(tr.buy_value.normalize()) if tr.buy_value is not None else None, - "{0:f}".format(tr.sell_quantity.normalize()) if tr.sell_quantity is not None else None, + OutputCsv._format_decimal(tr.buy_value), + OutputCsv._format_decimal(tr.sell_quantity), tr.sell_asset, - "{0:f}".format(tr.sell_value.normalize()) if tr.sell_value is not None else None, - "{0:f}".format(tr.fee_quantity.normalize()) if tr.fee_quantity is not None else None, + OutputCsv._format_decimal(tr.sell_value), + OutputCsv._format_decimal(tr.fee_quantity), tr.fee_asset, - "{0:f}".format(tr.fee_value.normalize()) if tr.fee_value is not None else None, + OutputCsv._format_decimal(tr.fee_value), tr.wallet, OutputCsv._format_timestamp(tr.timestamp), tr.note, @@ -235,11 +215,11 @@ def _to_bittytax_csv(tr): def _to_recap_csv(tr): return [ OutputCsv.RECAP_TYPE_MAPPING[tr.t_type], - tr.timestamp.strftime("%Y-%m-%d %H:%M:%S"), - "{0:f}".format(tr.buy_quantity.normalize()) if tr.buy_quantity is not None else None, + f"{tr.timestamp:%Y-%m-%d %H:%M:%S}", + OutputCsv._format_decimal(tr.buy_quantity), tr.buy_asset, - "{0:f}".format(tr.sell_quantity.normalize()) if tr.sell_quantity is not None else None, + OutputCsv._format_decimal(tr.sell_quantity), tr.sell_asset, - "{0:f}".format(tr.fee_quantity.normalize()) if tr.fee_quantity is not None else None, + OutputCsv._format_decimal(tr.fee_quantity), tr.fee_asset, ] diff --git a/src/bittytax/conv/output_excel.py b/src/bittytax/conv/output_excel.py index 73e3fae4..82e351a1 100644 --- a/src/bittytax/conv/output_excel.py +++ b/src/bittytax/conv/output_excel.py @@ -10,6 +10,7 @@ from xlsxwriter.utility import xl_rowcol_to_cell from ..config import config +from ..constants import TZ_UTC from ..version import __version__ from .exceptions import DataRowError from .out_record import TransactionOutRecord @@ -32,7 +33,7 @@ class OutputExcel(OutputBase): # pylint: disable=too-many-instance-attributes PROJECT_URL = "https://github.com/BittyTax/BittyTax" def __init__(self, progname, data_files, args): - super(OutputExcel, self).__init__(data_files) + super().__init__(data_files) self.filename = self.get_output_filename(args.output_filename, self.FILE_EXTENSION) self.workbook = xlsxwriter.Workbook(self.filename) self.workbook.set_size(1800, 1200) @@ -40,7 +41,7 @@ def __init__(self, progname, data_files, args): self.workbook.set_properties( { "title": self.TITLE, - "author": "%s v%s" % (progname, __version__), + "author": f"{progname} v{__version__}", "comments": self.PROJECT_URL, } ) @@ -134,12 +135,10 @@ def write_excel(self): worksheet.autofit() self.workbook.close() - sys.stderr.write( - "%soutput EXCEL file created: %s%s\n" % (Fore.WHITE, Fore.YELLOW, self.filename) - ) + sys.stderr.write(f"{Fore.WHITE}output EXCEL file created: {Fore.YELLOW}{self.filename}\n") -class Worksheet(object): +class Worksheet: SHEETNAME_MAX_LEN = 31 MAX_COL_WIDTH = 30 @@ -167,11 +166,11 @@ def _sheet_name(self, parser_name): sheet_name = name else: self.sheet_names[name.lower()] += 1 - sheet_name = "%s(%s)" % (name, self.sheet_names[name.lower()]) + sheet_name = f"{name}({self.sheet_names[name.lower()]})" if len(sheet_name) > self.SHEETNAME_MAX_LEN: - sheet_name = "%s(%s)" % ( - name[: len(name) - (len(sheet_name) - self.SHEETNAME_MAX_LEN)], - self.sheet_names[name.lower()], + sheet_name = ( + f"{name[: len(name) - (len(sheet_name) - self.SHEETNAME_MAX_LEN)]}" + f"({self.sheet_names[name.lower()]})" ) return sheet_name @@ -190,9 +189,6 @@ def _table_name(self, parser_name): return name def _make_columns(self, in_header): - if sys.version_info[0] < 3: - in_header = [h.decode("utf8") for h in in_header] - col_names = {} columns = [] @@ -250,13 +246,8 @@ def add_row(self, data_row, row_num): self._xl_timestamp(data_row.t_record.timestamp, row_num, 11) self._xl_note(data_row.t_record.note, row_num, 12) - if sys.version_info[0] < 3: - in_row = [r.decode("utf8") for r in data_row.row] - else: - in_row = data_row.row - # Add original data - for col_num, col_data in enumerate(in_row): + for col_num, col_data in enumerate(data_row.row): if ( data_row.failure and isinstance(data_row.failure, DataRowError) @@ -333,7 +324,7 @@ def _xl_quantity(self, quantity, row_num, col_num): self.worksheet.write_string( row_num, col_num, - "{0:f}".format(quantity.normalize()), + f"{quantity.normalize():0f}", self.output.format_num_string, ) else: @@ -348,18 +339,13 @@ def _xl_quantity(self, quantity, row_num, col_num): col_num, { "type": "formula", - "criteria": "=INT(" + cell + ")=" + cell, + "criteria": f"=INT({cell})={cell}", "format": self.output.format_num_int, }, ) - self._autofit_calc(col_num, len("{:0,f}".format(quantity.normalize()))) + self._autofit_calc(col_num, len(f"{quantity.normalize():0,f}")) def _xl_asset(self, asset, row_num, col_num): - if sys.version_info[0] < 3: - try: - asset = asset.decode("utf8") - except UnicodeEncodeError: - pass self.worksheet.write_string(row_num, col_num, asset) self._autofit_calc(col_num, len(asset)) @@ -368,18 +354,16 @@ def _xl_value(self, value, row_num, col_num): self.worksheet.write_number( row_num, col_num, value.normalize(), self.output.format_currency ) - self._autofit_calc(col_num, len("£{:0,.2f}".format(value))) + self._autofit_calc(col_num, len(f"£{value:0,.2f}")) else: self.worksheet.write_blank(row_num, col_num, None, self.output.format_currency) def _xl_wallet(self, wallet, row_num, col_num): - if sys.version_info[0] < 3: - wallet = wallet.decode("utf8") self.worksheet.write_string(row_num, col_num, wallet) self._autofit_calc(col_num, len(wallet)) def _xl_timestamp(self, timestamp, row_num, col_num): - utc_timestamp = timestamp.astimezone(config.TZ_UTC) + utc_timestamp = timestamp.astimezone(TZ_UTC) utc_timestamp = timestamp.replace(tzinfo=None) if self.microseconds: @@ -387,10 +371,10 @@ def _xl_timestamp(self, timestamp, row_num, col_num): self.worksheet.write_string( row_num, col_num, - utc_timestamp.strftime(self.output.STR_FORMAT_MS), + f"{utc_timestamp:{self.output.STR_FORMAT_MS}}", self.output.format_num_string, ) - self._autofit_calc(col_num, len(utc_timestamp.strftime(self.output.STR_FORMAT_MS))) + self._autofit_calc(col_num, len(f"{utc_timestamp:{self.output.STR_FORMAT_MS}}")) elif self.milliseconds: self.worksheet.write_datetime( row_num, col_num, utc_timestamp, self.output.format_timestamp_ms @@ -403,8 +387,6 @@ def _xl_timestamp(self, timestamp, row_num, col_num): self._autofit_calc(col_num, len(self.output.DATE_FORMAT)) def _xl_note(self, note, row_num, col_num): - if sys.version_info[0] < 3: - note = note.decode("utf8") self.worksheet.write_string(row_num, col_num, note) self._autofit_calc(col_num, len(note) if note else self.MAX_COL_WIDTH) @@ -419,8 +401,8 @@ def _autofit_calc(self, col_num, width): self.col_width[col_num] = width def autofit(self): - for col_num in self.col_width: - self.worksheet.set_column(col_num, col_num, self.col_width[col_num]) + for col_num, col_width in self.col_width.items(): + self.worksheet.set_column(col_num, col_num, col_width) def make_table(self, rows, parser_name): self.worksheet.add_table( diff --git a/src/bittytax/conv/parsers/binance.py b/src/bittytax/conv/parsers/binance.py index 13506c2d..3423f44d 100644 --- a/src/bittytax/conv/parsers/binance.py +++ b/src/bittytax/conv/parsers/binance.py @@ -251,8 +251,8 @@ def parse_binance_statements(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: @@ -412,9 +412,7 @@ def _make_trade(operation, tx_times, default_asset=""): split_buy_quantity = buy_quantity - tot_buy_quantity if config.debug: - sys.stderr.write( - "%sconv: split_buy_quantity=%s\n" % (Fore.GREEN, split_buy_quantity) - ) + sys.stderr.write(f"{Fore.GREEN}conv: split_buy_quantity={split_buy_quantity}\n") else: split_buy_quantity = buy_quantity diff --git a/src/bittytax/conv/parsers/blockfi.py b/src/bittytax/conv/parsers/blockfi.py index d5207826..605449eb 100644 --- a/src/bittytax/conv/parsers/blockfi.py +++ b/src/bittytax/conv/parsers/blockfi.py @@ -4,8 +4,9 @@ import sys from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore +from ...constants import WARNING from ..dataparser import DataParser from ..exceptions import UnexpectedTypeError from ..out_record import TransactionOutRecord @@ -18,11 +19,10 @@ def parse_blockfi(data_row, parser, **kwargs): if row_dict["Confirmed At"] == "" and not kwargs["unconfirmed"]: sys.stderr.write( - "%srow[%s] %s\n" % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) sys.stderr.write( - "%sWARNING%s Skipping unconfirmed transaction, use the [-uc] option to include it\n" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW) + f"{WARNING} Skipping unconfirmed transaction, use the [-uc] option to include it\n" ) return diff --git a/src/bittytax/conv/parsers/bscscan.py b/src/bittytax/conv/parsers/bscscan.py index d631a141..cc25d607 100644 --- a/src/bittytax/conv/parsers/bscscan.py +++ b/src/bittytax/conv/parsers/bscscan.py @@ -54,7 +54,7 @@ def parse_bscscan(data_row, _parser, **_kwargs): def _get_wallet(address): - return "%s-%s" % (WALLET, address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]) + return f"{WALLET}-{address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}" def parse_bscscan_internal(data_row, _parser, **_kwargs): diff --git a/src/bittytax/conv/parsers/celsius.py b/src/bittytax/conv/parsers/celsius.py index 6402f397..a3bcdbc7 100644 --- a/src/bittytax/conv/parsers/celsius.py +++ b/src/bittytax/conv/parsers/celsius.py @@ -4,8 +4,9 @@ import sys from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore +from ...constants import WARNING from ..dataparser import DataParser from ..exceptions import UnexpectedTypeError from ..out_record import TransactionOutRecord @@ -19,11 +20,10 @@ def parse_celsius(data_row, parser, **kwargs): if row_dict["Confirmed"] != "Yes" and not kwargs["unconfirmed"]: sys.stderr.write( - "%srow[%s] %s\n" % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) sys.stderr.write( - "%sWARNING%s Skipping unconfirmed transaction, use the [-uc] option to include it\n" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW) + f"{WARNING} Skipping unconfirmed transaction, use the [-uc] option to include it\n" ) return diff --git a/src/bittytax/conv/parsers/cexio.py b/src/bittytax/conv/parsers/cexio.py index 73ccb49d..d66c3028 100644 --- a/src/bittytax/conv/parsers/cexio.py +++ b/src/bittytax/conv/parsers/cexio.py @@ -27,8 +27,8 @@ def parse_cexio(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/cgtcalculator.py b/src/bittytax/conv/parsers/cgtcalculator.py index b7928389..fa0f3cf0 100644 --- a/src/bittytax/conv/parsers/cgtcalculator.py +++ b/src/bittytax/conv/parsers/cgtcalculator.py @@ -5,6 +5,7 @@ from decimal import Decimal from ...config import config +from ...constants import TZ_UTC from ..dataparser import DataParser from ..exceptions import UnexpectedContentError, UnexpectedTypeError from ..out_record import TransactionOutRecord @@ -29,7 +30,7 @@ def parse_cgtcalculator(data_row, parser, **_kwargs): wallet=WALLET, ) elif row_dict["B/S"] == "S": - if data_row.timestamp >= datetime(2008, 4, 6, tzinfo=config.TZ_UTC): + if data_row.timestamp >= datetime(2008, 4, 6, tzinfo=TZ_UTC): data_row.t_record = TransactionOutRecord( TransactionOutRecord.TYPE_TRADE, data_row.timestamp, diff --git a/src/bittytax/conv/parsers/coinbase.py b/src/bittytax/conv/parsers/coinbase.py index a2c04259..776e5039 100644 --- a/src/bittytax/conv/parsers/coinbase.py +++ b/src/bittytax/conv/parsers/coinbase.py @@ -2,7 +2,6 @@ # (c) Nano Nano Ltd 2019 import re -import sys from decimal import Decimal from ...config import config @@ -85,10 +84,10 @@ def parse_coinbase_usd(data_row, parser, **_kwargs): def _get_fiat_values(row_dict, currency, timestamp): - sp_header = "%s Spot Price at Transaction" % currency - st_header = "%s Subtotal" % currency - t_header = "%s Total (inclusive of fees)" % currency - f_header = "%s Fees" % currency + sp_header = f"{currency} Spot Price at Transaction" + st_header = f"{currency} Subtotal" + t_header = f"{currency} Total (inclusive of fees)" + f_header = f"{currency} Fees" spot_price_ccy = DataParser.convert_currency(row_dict[sp_header], currency, timestamp) total_ccy = DataParser.convert_currency(row_dict[t_header], currency, timestamp) @@ -215,9 +214,6 @@ def _do_parse_coinbase(data_row, parser, fiat_values): def _get_convert_info(notes): - if sys.version_info[0] < 3: - notes = notes.decode("utf8") - match = re.match(r"^Converted ([\d|,]*\.\d+) (\w+) to ([\d|,]*\.\d+) (\w+) *$", notes) if match: @@ -226,9 +222,6 @@ def _get_convert_info(notes): def _get_currency(notes): - if sys.version_info[0] < 3: - notes = notes.decode("utf8") - match = re.match(r".+for .{1}[\d|,]+\.\d{2} (\w{3}).*$", notes) if match: diff --git a/src/bittytax/conv/parsers/coinbasepro.py b/src/bittytax/conv/parsers/coinbasepro.py index 8fad9a8b..7fef54f5 100644 --- a/src/bittytax/conv/parsers/coinbasepro.py +++ b/src/bittytax/conv/parsers/coinbasepro.py @@ -25,8 +25,8 @@ def parse_coinbase_pro_account_v2(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/coinlist.py b/src/bittytax/conv/parsers/coinlist.py index 4ebb176d..85136268 100644 --- a/src/bittytax/conv/parsers/coinlist.py +++ b/src/bittytax/conv/parsers/coinlist.py @@ -106,8 +106,8 @@ def parse_coinlist_pro(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) try: diff --git a/src/bittytax/conv/parsers/coinmetro.py b/src/bittytax/conv/parsers/coinmetro.py index dfdfbc4f..0ab02023 100644 --- a/src/bittytax/conv/parsers/coinmetro.py +++ b/src/bittytax/conv/parsers/coinmetro.py @@ -18,8 +18,8 @@ def parse_coinmetro(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/etherscan.py b/src/bittytax/conv/parsers/etherscan.py index 9e2408a6..31700e8d 100644 --- a/src/bittytax/conv/parsers/etherscan.py +++ b/src/bittytax/conv/parsers/etherscan.py @@ -53,13 +53,13 @@ def parse_etherscan(data_row, _parser, **_kwargs): def _get_wallet(address): - return "%s-%s" % (WALLET, address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]) + return f"{WALLET}-{address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}" def _get_note(row_dict): if row_dict["Status"] != "": if row_dict.get("Method"): - return "Failure (%s)" % row_dict["Method"] + return f'Failure ({row_dict["Method"]})' return "Failure" if row_dict.get("Method"): @@ -137,7 +137,7 @@ def parse_etherscan_nfts(data_row, _parser, **kwargs): TransactionOutRecord.TYPE_DEPOSIT, data_row.timestamp, buy_quantity=1, - buy_asset="{} #{}".format(row_dict["TokenName"], row_dict["TokenId"]), + buy_asset=f'{row_dict["TokenName"]} #{row_dict["TokenId"]}', wallet=_get_wallet(row_dict["To"]), ) elif row_dict["From"].lower() in kwargs["filename"].lower(): @@ -145,7 +145,7 @@ def parse_etherscan_nfts(data_row, _parser, **kwargs): TransactionOutRecord.TYPE_WITHDRAWAL, data_row.timestamp, sell_quantity=1, - sell_asset="{} #{}".format(row_dict["TokenName"], row_dict["TokenId"]), + sell_asset=f'{row_dict["TokenName"]} #{row_dict["TokenId"]}', wallet=_get_wallet(row_dict["From"]), ) else: diff --git a/src/bittytax/conv/parsers/gatehub.py b/src/bittytax/conv/parsers/gatehub.py index d79af551..cd665394 100644 --- a/src/bittytax/conv/parsers/gatehub.py +++ b/src/bittytax/conv/parsers/gatehub.py @@ -25,8 +25,8 @@ def parse_gatehub(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/gravity.py b/src/bittytax/conv/parsers/gravity.py index 3b0ffa84..8d6ed11e 100644 --- a/src/bittytax/conv/parsers/gravity.py +++ b/src/bittytax/conv/parsers/gravity.py @@ -35,8 +35,8 @@ def parse_gravity_v1(data_rows, parser, **kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/hecoinfo.py b/src/bittytax/conv/parsers/hecoinfo.py index ad073989..58bfa5a5 100644 --- a/src/bittytax/conv/parsers/hecoinfo.py +++ b/src/bittytax/conv/parsers/hecoinfo.py @@ -54,7 +54,7 @@ def parse_hecoinfo(data_row, _parser, **_kwargs): def _get_wallet(address): - return "%s-%s" % (WALLET, address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]) + return f"{WALLET}-{address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}" def parse_hecoinfo_internal(data_row, _parser, **_kwargs): diff --git a/src/bittytax/conv/parsers/hitbtc.py b/src/bittytax/conv/parsers/hitbtc.py index 67a38454..04db39ec 100644 --- a/src/bittytax/conv/parsers/hitbtc.py +++ b/src/bittytax/conv/parsers/hitbtc.py @@ -19,8 +19,8 @@ def parse_hitbtc_trades_v2(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/hotbit.py b/src/bittytax/conv/parsers/hotbit.py index 0e47f6a3..c46fe73c 100644 --- a/src/bittytax/conv/parsers/hotbit.py +++ b/src/bittytax/conv/parsers/hotbit.py @@ -87,8 +87,8 @@ def parse_hotbit_orders_v1(data_rows, parser, **kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: @@ -169,8 +169,8 @@ def parse_hotbit_trades(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f" row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/koinly.py b/src/bittytax/conv/parsers/koinly.py index 1dd57da4..e94a83d7 100644 --- a/src/bittytax/conv/parsers/koinly.py +++ b/src/bittytax/conv/parsers/koinly.py @@ -40,8 +40,8 @@ def parse_koinly(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/kraken.py b/src/bittytax/conv/parsers/kraken.py index e7a8bc39..10282b58 100644 --- a/src/bittytax/conv/parsers/kraken.py +++ b/src/bittytax/conv/parsers/kraken.py @@ -80,8 +80,8 @@ def parse_kraken_ledgers(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: diff --git a/src/bittytax/conv/parsers/nexo.py b/src/bittytax/conv/parsers/nexo.py index d8143ca7..4772bc04 100644 --- a/src/bittytax/conv/parsers/nexo.py +++ b/src/bittytax/conv/parsers/nexo.py @@ -38,9 +38,9 @@ def parse_nexo(data_row, parser, **_kwargs): buy_asset = row_dict["Output Currency"] sell_asset = row_dict["Input Currency"] - for local_asset in ASSET_NORMALISE: - buy_asset = buy_asset.replace(local_asset, ASSET_NORMALISE[local_asset]) - sell_asset = sell_asset.replace(local_asset, ASSET_NORMALISE[local_asset]) + for nexo_asset, asset in ASSET_NORMALISE.items(): + buy_asset = buy_asset.replace(nexo_asset, asset) + sell_asset = sell_asset.replace(nexo_asset, asset) if "Amount" in row_dict: if row_dict["Type"] != "Exchange": diff --git a/src/bittytax/conv/parsers/okx.py b/src/bittytax/conv/parsers/okx.py index 94c4ea4c..1894308b 100644 --- a/src/bittytax/conv/parsers/okx.py +++ b/src/bittytax/conv/parsers/okx.py @@ -14,11 +14,7 @@ WALLET = "OKX" TZ_INFOS = {"CST": dateutil.tz.gettz("Asia/Shanghai")} - -if sys.version_info[0] < 3: - BOM = "\xef\xbb\xbf" -else: - BOM = "\ufeff" # pylint: disable=anomalous-unicode-escape-in-string +BOM = "\ufeff" # pylint: disable=anomalous-unicode-escape-in-string def parse_okx_trades_v2(data_rows, parser, **_kwargs): @@ -42,8 +38,8 @@ def parse_okx_trades_v2(data_rows, parser, **_kwargs): for data_row in data_rows: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: @@ -115,20 +111,12 @@ def parse_okx_trades_v1(data_rows, parser, **_kwargs): try: if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % ( - Fore.YELLOW, - parser.in_header_row_num + buy_row.line_num, - buy_row, - ) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + buy_row.line_num}] {buy_row}\n" ) sys.stderr.write( - "%sconv: row[%s] %s\n" - % ( - Fore.YELLOW, - parser.in_header_row_num + sell_row.line_num, - sell_row, - ) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + sell_row.line_num}] {sell_row}\n" ) _parse_okx_trades_v1_row(buy_row, sell_row, parser) diff --git a/src/bittytax/conv/parsers/polygonscan.py b/src/bittytax/conv/parsers/polygonscan.py index 96a33a8e..0a760232 100644 --- a/src/bittytax/conv/parsers/polygonscan.py +++ b/src/bittytax/conv/parsers/polygonscan.py @@ -54,7 +54,7 @@ def parse_polygonscan(data_row, _parser, **_kwargs): def _get_wallet(address): - return "%s-%s" % (WALLET, address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]) + return f"{WALLET}-{address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}" def parse_polygonscan_internal(data_row, _parser, **_kwargs): diff --git a/src/bittytax/conv/parsers/qtwallet.py b/src/bittytax/conv/parsers/qtwallet.py index b7930a7c..92f2ba79 100644 --- a/src/bittytax/conv/parsers/qtwallet.py +++ b/src/bittytax/conv/parsers/qtwallet.py @@ -5,8 +5,9 @@ import sys from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore +from ...constants import WARNING from ..dataparser import DataParser from ..exceptions import UnexpectedTypeError, UnknownCryptoassetError from ..out_record import TransactionOutRecord @@ -30,12 +31,10 @@ def parse_qt_wallet(data_row, parser, **kwargs): if row_dict["Confirmed"] == "false" and not kwargs["unconfirmed"]: sys.stderr.write( - "%srow[%s] %s\n" % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) sys.stderr.write( - "%sWARNING%s Skipping unconfirmed transaction, " - "use the [-uc] option to include it\n" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW) + f"{WARNING} Skipping unconfirmed transaction, use the [-uc] option to include it\n" ) return diff --git a/src/bittytax/conv/parsers/scripts/binance_assets.py b/src/bittytax/conv/parsers/scripts/binance_assets.py index 68912543..17b96d92 100644 --- a/src/bittytax/conv/parsers/scripts/binance_assets.py +++ b/src/bittytax/conv/parsers/scripts/binance_assets.py @@ -24,14 +24,15 @@ def get_assets(): print("\nQUOTE_ASSETS = [") for i in sorted(quote_assets): - print(' "%s",' % i) + print(f' "{i}",') print("]") rows = [] for i in range(0, len(base_assets), 10): - rows.append(", ".join("'{}'".format(v) for v in sorted(base_assets)[i : i + 10])) + rows.append(", ".join(f"'{v}'" for v in sorted(base_assets)[i : i + 10])) - print("\nBASE_ASSETS = [%s]\n" % (",\n ".join(rows))) + rows_str = ",/\n ".join(rows) + print(f"\nBASE_ASSETS = [{rows_str}]\n") if __name__ == "__main__": diff --git a/src/bittytax/conv/parsers/scripts/hotbit_assets.py b/src/bittytax/conv/parsers/scripts/hotbit_assets.py index ea8e4f63..ff258bc2 100644 --- a/src/bittytax/conv/parsers/scripts/hotbit_assets.py +++ b/src/bittytax/conv/parsers/scripts/hotbit_assets.py @@ -24,14 +24,15 @@ def get_assets(): print("\nQUOTE_ASSETS = [") for i in sorted(quote_assets): - print(' "%s",' % i) + print(f' "{i}",') print("]") rows = [] for i in range(0, len(base_assets), 10): - rows.append(", ".join("'{}'".format(v) for v in sorted(base_assets)[i : i + 10])) + rows.append(", ".join(f"'{v}'" for v in sorted(base_assets)[i : i + 10])) - print("\nBASE_ASSETS = [%s]\n" % (",\n ".join(rows))) + rows_str = ",\n ".join(rows) + print(f"\nBASE_ASSETS = [{rows_str}]\n") if __name__ == "__main__": diff --git a/src/bittytax/conv/parsers/scripts/kraken_assets.py b/src/bittytax/conv/parsers/scripts/kraken_assets.py index aaa4bec9..2723400c 100644 --- a/src/bittytax/conv/parsers/scripts/kraken_assets.py +++ b/src/bittytax/conv/parsers/scripts/kraken_assets.py @@ -44,15 +44,12 @@ def get_quote_assets(): quote = response.json()["result"][pair]["quote"] if bt_base and bt_quote and bt_base + "/" + bt_quote == wsname: - print("%s = %s/%s [OK]" % (pair, bt_base, bt_quote)) + print(f"{pair} = {bt_base}/{bt_quote} [OK]") elif bt_base == base and bt_quote == quote: - print("%s = %s/%s [OK]" % (pair, bt_base, bt_quote)) + print(f"{pair} = {bt_base}/{bt_quote} [OK]") else: passed = False - print( - "%s = %s/%s [Failure] %s (%s & %s)" - % (pair, bt_base, bt_quote, wsname, base, quote) - ) + print(f"{pair} = {bt_base}/{bt_quote} [Failure] {wsname} ({base} & {quote})") if passed: print("===Split trading pairs PASSED===") @@ -65,12 +62,12 @@ def get_quote_assets(): def output_constants(alt_assets, quote_assets): print("\nQUOTE_ASSETS = [") for i in sorted(quote_assets): - print(' "%s",' % i) + print(f' "{i}"') print("]") print("\nALT_ASSETS = {") for k, v in sorted(alt_assets.items()): - print(' "{}": "{}",'.format(k, v)) + print(f' "{k}": "{v}",') print("}") diff --git a/src/bittytax/conv/parsers/snowtrace.py b/src/bittytax/conv/parsers/snowtrace.py index 25e47a21..10906b88 100644 --- a/src/bittytax/conv/parsers/snowtrace.py +++ b/src/bittytax/conv/parsers/snowtrace.py @@ -54,7 +54,7 @@ def parse_snowtrace(data_row, _parser, **_kwargs): def _get_wallet(address): - return "%s-%s" % (WALLET, address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]) + return f"{WALLET}-{address.lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}" def parse_snowtrace_internal(data_row, _parser, **_kwargs): diff --git a/src/bittytax/conv/parsers/staketax.py b/src/bittytax/conv/parsers/staketax.py index fa9b4136..9ad9f4f5 100644 --- a/src/bittytax/conv/parsers/staketax.py +++ b/src/bittytax/conv/parsers/staketax.py @@ -82,10 +82,7 @@ def parse_staketax_default(data_row, parser, **_kwargs): def _get_wallet(exchange, wallet_address): - return "%s-%s" % ( - exchange.replace("_blockchain", "").capitalize(), - wallet_address[0:16], - ) + return f'{exchange.replace("_blockchain", "").capitalize()}-{wallet_address[0:16]}' def parse_staketax_bittytax(data_row, parser, **_kwargs): diff --git a/src/bittytax/conv/parsers/trezorsuite.py b/src/bittytax/conv/parsers/trezorsuite.py index 380538db..eb499ddf 100644 --- a/src/bittytax/conv/parsers/trezorsuite.py +++ b/src/bittytax/conv/parsers/trezorsuite.py @@ -48,7 +48,7 @@ def parse_trezor_suite_v2(data_row, parser, **_kwargs): ) elif row_dict["Type"] == "FAILED": if row_dict["Label"]: - note = "Failure (%s)" % row_dict["Label"] + note = f'Failure ({row_dict["Label"]})' else: note = "Failure" diff --git a/src/bittytax/conv/parsers/zelcore.py b/src/bittytax/conv/parsers/zelcore.py index 2d375d79..e4fbd83c 100644 --- a/src/bittytax/conv/parsers/zelcore.py +++ b/src/bittytax/conv/parsers/zelcore.py @@ -44,15 +44,13 @@ def _get_wallet(filename, chain_id): if match: if match.group(1) is None: - return "%s-%s:%s" % ( - WALLET, - match.group(2).lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN], - chain_id, + return ( + f"{WALLET}-{match.group(2).lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}:" + f"{chain_id}" ) - return "%s-k:%s:%s" % ( - WALLET, - match.group(2).lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN], - chain_id, + return ( + f"{WALLET}-k:{match.group(2).lower()[0 : TransactionOutRecord.WALLET_ADDR_LEN]}:" + f"{chain_id}" ) return WALLET diff --git a/src/bittytax/conv/parsers/zerion.py b/src/bittytax/conv/parsers/zerion.py index a1c36cd1..dccd709e 100644 --- a/src/bittytax/conv/parsers/zerion.py +++ b/src/bittytax/conv/parsers/zerion.py @@ -22,8 +22,8 @@ def parse_zerion(data_rows, parser, **_kwargs): for row_index, data_row in enumerate(data_rows): if config.debug: sys.stderr.write( - "%sconv: row[%s] %s\n" - % (Fore.YELLOW, parser.in_header_row_num + data_row.line_num, data_row) + f"{Fore.YELLOW}conv: " + f"row[{parser.in_header_row_num + data_row.line_num}] {data_row}\n" ) if data_row.parsed: @@ -187,8 +187,8 @@ def _do_zerion_multi_withdrawal(data_row, data_rows, row_index, t_outs): if config.debug: sys.stderr.write( - "%sconv: split_fee_quantity=%s split_fee_value=%s\n" - % (Fore.GREEN, split_fee_quantity, split_fee_value) + f"{Fore.GREEN}conv: " + f"split_fee_quantity={split_fee_quantity} split_fee_value={split_fee_value}\n" ) t_record = TransactionOutRecord( @@ -223,12 +223,12 @@ def _do_zerion_multi_sell(data_row, data_rows, row_index, t_ins, t_outs): if config.debug: sys.stderr.write( - "%sconv: buy_quantity=%s buy_asset=%s buy_value=%s\n" - % (Fore.GREEN, buy_quantity, buy_asset, buy_value) + f"{Fore.GREEN}conv: " + f"buy_quantity={buy_quantity} buy_asset={buy_asset} buy_value={buy_value}\n" ) sys.stderr.write( - "%sconv: fee_quantity=%s fee_asset=%s fee_value=%s\n" - % (Fore.GREEN, fee_quantity, fee_asset, fee_value) + f"{Fore.GREEN}conv: " + f"fee_quantity={fee_quantity} fee_asset={fee_asset} fee_value={fee_value}\n" ) for cnt, t_out in enumerate(t_outs): @@ -247,12 +247,12 @@ def _do_zerion_multi_sell(data_row, data_rows, row_index, t_ins, t_outs): if config.debug: sys.stderr.write( - "%sconv: split_buy_quantity=%s split_buy_value=%s\n" - % (Fore.GREEN, split_buy_quantity, split_buy_value) + f"{Fore.GREEN}conv: " + f"split_buy_quantity={split_buy_quantity} split_buy_value={split_buy_value}\n" ) sys.stderr.write( - "%sconv: split_fee_quantity=%s split_fee_value=%s\n" - % (Fore.GREEN, split_fee_quantity, split_fee_value) + f"{Fore.GREEN}conv: " + f"split_fee_quantity={split_fee_quantity} split_fee_value={split_fee_value}\n" ) sell_quantity, sell_asset, sell_value = _get_data_json(data_row, t_out) @@ -290,12 +290,12 @@ def _do_zerion_multi_buy(data_row, data_rows, row_index, t_ins, t_outs): sell_quantity, sell_asset, sell_value = _get_data_json(data_row, t_outs[0]) if config.debug: sys.stderr.write( - "%sconv: sell_quantity=%s sell_asset=%s sell_value=%s\n" - % (Fore.GREEN, sell_quantity, sell_asset, sell_value) + f"{Fore.GREEN}conv: " + f"sell_quantity={sell_quantity} sell_asset={sell_asset} sell_value={sell_value}\n" ) sys.stderr.write( - "%sconv: fee_quantity=%s fee_asset=%s fee_value=%s\n" - % (Fore.GREEN, fee_quantity, fee_asset, fee_value) + f"{Fore.GREEN}conv: " + f"fee_quantity={fee_quantity} fee_asset={fee_asset} fee_value={fee_value}\n" ) for cnt, t_in in enumerate(t_ins): @@ -314,12 +314,12 @@ def _do_zerion_multi_buy(data_row, data_rows, row_index, t_ins, t_outs): if config.debug: sys.stderr.write( - "%sconv: split_sell_quantity=%s split_sell_value=%s\n" - % (Fore.GREEN, split_sell_quantity, split_sell_value) + f"{Fore.GREEN}conv: " + f"split_sell_quantity={split_sell_quantity} split_sell_value={split_sell_value}\n" ) sys.stderr.write( - "%sconv: split_fee_quantity=%s split_fee_value=%s\n" - % (Fore.GREEN, split_fee_quantity, split_fee_value) + f"{Fore.GREEN}conv: " + f"split_fee_quantity={split_fee_quantity} split_fee_value={split_fee_value}\n" ) buy_quantity, buy_asset, buy_value = _get_data_json(data_row, t_in) diff --git a/src/bittytax/exceptions.py b/src/bittytax/exceptions.py index 8cf70b88..561d6a61 100644 --- a/src/bittytax/exceptions.py +++ b/src/bittytax/exceptions.py @@ -6,7 +6,7 @@ class TransactionParserError(Exception): def __init__(self, col_num, col_name, value=None): - super(TransactionParserError, self).__init__() + super().__init__() self.col_num = col_num self.col_name = col_name self.value = value @@ -14,30 +14,30 @@ def __init__(self, col_num, col_name, value=None): class UnexpectedTransactionTypeError(TransactionParserError): def __str__(self): - return "Invalid Transaction Type: '%s', use {%s}" % ( - self.value, - ",".join(TransactionRecord.ALL_TYPES), + return ( + f"Invalid Transaction Type: '{self.value}', use " + f"{{{','.join(TransactionRecord.ALL_TYPES)}}}" ) class TimestampParserError(TransactionParserError): def __str__(self): - return "Invalid Timestamp: '%s', use format YYYY-MM-DDTHH:MM:SS ZZZ" % self.value + return f"Invalid Timestamp: '{self.value}', use format YYYY-MM-DDTHH:MM:SS ZZZ" class DataValueError(TransactionParserError): def __str__(self): - return "Invalid data for %s: '%s'" % (self.col_name, self.value) + return f"Invalid data for {self.col_name}: '{self.value}'" class UnexpectedDataError(TransactionParserError): def __str__(self): - return "Unexpected data in %s: '%s'" % (self.col_name, self.value) + return f"Unexpected data in {self.col_name}: '{self.value}'" class MissingDataError(TransactionParserError): def __str__(self): - return "Missing data for %s" % self.col_name + return f"Missing data for {self.col_name}" class ImportFailureError(Exception): diff --git a/src/bittytax/export_records.py b/src/bittytax/export_records.py index 190aa665..50ff2a93 100644 --- a/src/bittytax/export_records.py +++ b/src/bittytax/export_records.py @@ -8,7 +8,7 @@ from colorama import Fore -class ExportRecords(object): +class ExportRecords: DEFAULT_FILENAME = "BittyTax_Export" FILE_EXTENSION = "csv" OUT_HEADER = [ @@ -39,26 +39,21 @@ def get_output_filename(): filepath, file_extension = os.path.splitext(filepath) i = 2 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" while os.path.exists(new_fname): i += 1 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" return new_fname def write_csv(self): filename = self.get_output_filename() - if sys.version_info[0] >= 3: - with open(filename, "w", newline="", encoding="utf-8") as csv_file: - writer = csv.writer(csv_file, lineterminator="\n") - self.write_rows(writer) - else: - with open(filename, "wb") as csv_file: - writer = csv.writer(csv_file, lineterminator="\n") - self.write_rows(writer) + with open(filename, "w", newline="", encoding="utf-8") as csv_file: + writer = csv.writer(csv_file, lineterminator="\n") + self.write_rows(writer) - sys.stderr.write("%sexport file created: %s%s\n" % (Fore.WHITE, Fore.YELLOW, filename)) + sys.stderr.write(f"{Fore.WHITE}export file created: {Fore.YELLOW}{filename}\n") def write_rows(self, writer): writer.writerow(self.OUT_HEADER) diff --git a/src/bittytax/holdings.py b/src/bittytax/holdings.py index 9ff405cc..df7f39ec 100644 --- a/src/bittytax/holdings.py +++ b/src/bittytax/holdings.py @@ -3,13 +3,14 @@ from decimal import Decimal -from colorama import Back, Fore +from colorama import Fore from tqdm import tqdm from .config import config +from .constants import WARNING -class Holdings(object): +class Holdings: def __init__(self, asset): self.asset = asset self.quantity = Decimal(0) @@ -29,21 +30,12 @@ def add_tokens(self, quantity, cost, fees, is_deposit): if config.debug: print( - "%ssection104: %s=%s (+%s) cost=%s %s (+%s %s) fees=%s %s (+%s %s)" - % ( - Fore.YELLOW, - self.asset, - "{:0,f}".format(self.quantity.normalize()), - "{:0,f}".format(quantity.normalize()), - config.sym() + "{:0,.2f}".format(self.cost), - config.ccy, - config.sym() + "{:0,.2f}".format(cost), - config.ccy, - config.sym() + "{:0,.2f}".format(self.fees), - config.ccy, - config.sym() + "{:0,.2f}".format(fees), - config.ccy, - ) + f"{Fore.YELLOW}section104: " + f"{self.asset}={self.quantity.normalize():0,f} (+{quantity.normalize():0,f}) " + f"cost={config.sym()}{self.cost:0,.2f} {config.ccy} " + f"(+{config.sym()}{cost:0,.2f} {config.ccy}) " + f"fees={config.sym()}{self.fees:0,.2f} {config.ccy} " + f"(+{config.sym()}{fees:0,.2f} {config.ccy})" ) def subtract_tokens(self, quantity, cost, fees, is_withdrawal): @@ -56,34 +48,18 @@ def subtract_tokens(self, quantity, cost, fees, is_withdrawal): if config.debug: print( - "%ssection104: %s=%s (-%s) cost=%s %s (-%s %s) fees=%s %s (-%s %s)" - % ( - Fore.YELLOW, - self.asset, - "{:0,f}".format(self.quantity.normalize()), - "{:0,f}".format(quantity.normalize()), - config.sym() + "{:0,.2f}".format(self.cost), - config.ccy, - config.sym() + "{:0,.2f}".format(cost), - config.ccy, - config.sym() + "{:0,.2f}".format(self.fees), - config.ccy, - config.sym() + "{:0,.2f}".format(fees), - config.ccy, - ) + f"{Fore.YELLOW}section104: " + f"{self.asset}={self.quantity.normalize():0,f} (-{quantity.normalize():0,f}) " + f"cost={config.sym()}{self.cost:0,.2f} {config.ccy} " + f"(-{config.sym()}{cost:0,.2f} {config.ccy}) " + f"fees={config.sym()}{self.fees:0,.2f} {config.ccy} " + f"(-{config.sym()}{fees:0,.2f} {config.ccy})" ) def check_transfer_mismatch(self): if self.withdrawals > 0 and self.withdrawals != self.deposits: tqdm.write( - "%sWARNING%s Disposal detected between a Withdrawal and a Deposit " - "(%s:%s) for %s, cost basis will be wrong" - % ( - Back.RED + Fore.BLACK, - Back.RESET + Fore.RED, - self.withdrawals, - self.deposits, - self.asset, - ) + f"{WARNING} Disposal detected between a Withdrawal and a Deposit " + f"({self.withdrawals}:{self.deposits}) for {self.asset}, cost basis will be wrong" ) self.mismatches += 1 diff --git a/src/bittytax/import_records.py b/src/bittytax/import_records.py index ddb1fca6..fa2352b5 100644 --- a/src/bittytax/import_records.py +++ b/src/bittytax/import_records.py @@ -13,6 +13,7 @@ from tqdm import tqdm, trange from .config import config +from .constants import ERROR, TZ_UTC from .exceptions import ( DataValueError, MissingDataError, @@ -25,7 +26,7 @@ from .transactions import Buy, Sell -class ImportRecords(object): +class ImportRecords: def __init__(self): self.t_rows = [] self.success_cnt = 0 @@ -34,24 +35,24 @@ def __init__(self): def import_excel_xlsx(self, filename): warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl") workbook = load_workbook(filename=filename, read_only=False, data_only=True) - print("%sExcel file: %s%s" % (Fore.WHITE, Fore.YELLOW, filename)) + print(f"{Fore.WHITE}Excel file: {Fore.YELLOW}{filename}") for sheet_name in workbook.sheetnames: worksheet = workbook[sheet_name] if worksheet.title.startswith("--"): - print("%sskipping '%s' worksheet" % (Fore.GREEN, worksheet.title)) + print(f"{Fore.GREEN}skipping '{worksheet.title}' worksheet") continue if config.debug: - print("%simporting '%s' rows" % (Fore.CYAN, worksheet.title)) + print(f"{Fore.CYAN}importing '{worksheet.title}' rows") for row_num, row in enumerate( tqdm( worksheet.rows, total=worksheet.max_row, unit=" row", - desc="%simporting '%s' rows%s" % (Fore.CYAN, worksheet.title, Fore.GREEN), + desc=f"{Fore.CYAN}importing '{worksheet.title}' rows{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ) ): @@ -71,13 +72,10 @@ def import_excel_xlsx(self, filename): t_row.failure = e if config.debug or t_row.failure: - tqdm.write("%simport: %s" % (Fore.YELLOW, t_row)) + tqdm.write(f"{Fore.YELLOW}import: {t_row}") if t_row.failure: - tqdm.write( - "%sERROR%s %s" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, t_row.failure) - ) + tqdm.write(f"{ERROR} {t_row.failure}") self.t_rows.append(t_row) self.update_cnts(t_row) @@ -87,21 +85,21 @@ def import_excel_xlsx(self, filename): def import_excel_xls(self, filename): workbook = xlrd.open_workbook(filename) - print("%sExcel file: %s%s" % (Fore.WHITE, Fore.YELLOW, filename)) + print(f"{Fore.WHITE}Excel file: {Fore.YELLOW}{filename}") for worksheet in workbook.sheets(): if worksheet.name.startswith("--"): - print("%sskipping '%s' worksheet" % (Fore.GREEN, worksheet.name)) + print(f"{Fore.GREEN}skipping '{worksheet.name}' worksheet") continue if config.debug: - print("%simporting '%s' rows" % (Fore.CYAN, worksheet.name)) + print(f"{Fore.CYAN}importing '{worksheet.name}' rows") for row_num in trange( 0, worksheet.nrows, unit=" row", - desc="%simporting '%s' rows%s" % (Fore.CYAN, worksheet.name, Fore.GREEN), + desc=f"{Fore.CYAN}importing '{worksheet.name}' rows{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if row_num == 0: @@ -123,13 +121,10 @@ def import_excel_xls(self, filename): t_row.failure = e if config.debug or t_row.failure: - tqdm.write("%simport: %s" % (Fore.YELLOW, t_row)) + tqdm.write(f"{Fore.YELLOW}import: {t_row}") if t_row.failure: - tqdm.write( - "%sERROR%s %s" - % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, t_row.failure) - ) + tqdm.write(f"{ERROR} {t_row.failure}") self.t_rows.append(t_row) self.update_cnts(t_row) @@ -141,9 +136,6 @@ def import_excel_xls(self, filename): def convert_cell_xlsx(cell): if cell.value is None: return "" - - if sys.version_info[0] < 3 and cell.data_type == "s": - return cell.value.encode("utf-8") return str(cell.value) @staticmethod @@ -151,9 +143,9 @@ def convert_cell_xls(cell, workbook): if cell.ctype == xlrd.XL_CELL_DATE: datetime = xlrd.xldate.xldate_as_datetime(cell.value, workbook.datemode) if datetime.microsecond: - value = datetime.strftime("%Y-%m-%dT%H:%M:%S.%f") + value = f"{datetime:%Y-%m-%dT%H:%M:%S.%f}" else: - value = datetime.strftime("%Y-%m-%d %H:%M:%S") + value = f"{datetime:%Y-%m-%dT%H:%M:%S}" elif cell.ctype in ( xlrd.XL_CELL_NUMBER, xlrd.XL_CELL_BOOLEAN, @@ -162,28 +154,21 @@ def convert_cell_xls(cell, workbook): # repr is required to ensure no precision is lost value = repr(cell.value) else: - if sys.version_info[0] >= 3: - value = str(cell.value) - else: - value = cell.value.encode("utf-8") + value = str(cell.value) return value def import_csv(self, import_file): - print("%sCSV file: %s%s" % (Fore.WHITE, Fore.YELLOW, import_file.name)) + print(f"{Fore.WHITE}CSV file: {Fore.YELLOW}{import_file.name}") if config.debug: - print("%simporting rows" % Fore.CYAN) + print(f"{Fore.CYAN}importing rows") - if sys.version_info[0] < 3: - # Special handling required for utf-8 encoded CSV files - reader = csv.reader(self.utf_8_encoder(import_file)) - else: - reader = csv.reader(import_file) + reader = csv.reader(import_file) for row in tqdm( reader, unit=" row", - desc="%simporting%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}importing{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if reader.line_num == 1: @@ -197,21 +182,14 @@ def import_csv(self, import_file): t_row.failure = e if config.debug or t_row.failure: - tqdm.write("%simport: %s" % (Fore.YELLOW, t_row)) + tqdm.write(f"{Fore.YELLOW}import: {t_row}") if t_row.failure: - tqdm.write( - "%sERROR%s %s" % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, t_row.failure) - ) + tqdm.write(f"{ERROR} {t_row.failure}") self.t_rows.append(t_row) self.update_cnts(t_row) - @staticmethod - def utf_8_encoder(unicode_csv_data): - for line in unicode_csv_data: - yield line.encode("utf-8") - def update_cnts(self, t_row): if t_row.failure is not None: self.failure_cnt += 1 @@ -227,12 +205,12 @@ def get_records(self): if config.debug: for t_row in self.t_rows: - print("%simport: %s" % (Fore.YELLOW, t_row)) + print(f"{Fore.YELLOW}import: {t_row}") return transaction_records -class TransactionRow(object): +class TransactionRow: HEADER = [ "Type", "Buy Quantity", @@ -371,14 +349,14 @@ def parse(self): def parse_timestamp(self): try: timestamp = dateutil.parser.parse(self.row_dict["Timestamp"]) - except ValueError: + except ValueError as e: raise TimestampParserError( self.HEADER.index("Timestamp"), "Timestamp", self.row_dict["Timestamp"] - ) + ) from e if timestamp.tzinfo is None: # Default to UTC if no timezone is specified - timestamp = timestamp.replace(tzinfo=config.TZ_UTC) + timestamp = timestamp.replace(tzinfo=TZ_UTC) return timestamp @@ -387,12 +365,12 @@ def validate_quantity(self, quantity_hdr, required): if required: try: quantity = Decimal(self.strip_non_digits(self.row_dict[quantity_hdr])) - except InvalidOperation: + except InvalidOperation as e: raise DataValueError( self.HEADER.index(quantity_hdr), quantity_hdr, self.row_dict[quantity_hdr], - ) + ) from e if quantity < 0: raise DataValueError(self.HEADER.index(quantity_hdr), quantity_hdr, quantity) @@ -426,12 +404,12 @@ def validate_value(self, value_hdr, required): if required: try: value = Decimal(self.strip_non_digits(self.row_dict[value_hdr])) - except InvalidOperation: + except InvalidOperation as e: raise DataValueError( self.HEADER.index(value_hdr), value_hdr, self.row_dict[value_hdr], - ) + ) from e if value < 0: raise DataValueError(self.HEADER.index(value_hdr), value_hdr, value) @@ -453,30 +431,22 @@ def strip_non_digits(string): def __str__(self): if self.t_record and self.t_record.tid: - tid_str = " %s[TID:%s]" % (Fore.MAGENTA, self.t_record.tid[0]) + tid_str = f" {Fore.MAGENTA}[TID:{self.t_record.tid[0]}]" else: tid_str = "" if self.worksheet_name: - worksheet_str = "'%s' " % self.worksheet_name + worksheet_str = f"'{self.worksheet_name}' " else: worksheet_str = "" - if sys.version_info[0] < 3: - row = [r.decode("utf8") for r in self.row] - else: - row = self.row - - if self.failure is not None: - row_str = ", ".join( - [ - "%s'%s'%s" % (Back.RED, data, Back.RESET) - if self.failure.col_num == num - else "'%s'" % data - for num, data in enumerate(row) - ] - ) - else: - row_str = "'%s'" % "', '".join(row) + row_str = ", ".join( + [ + f"{Back.RED}'{data}'{Back.RESET}" + if self.failure and self.failure.col_num == num + else f"'{data}'" + for num, data in enumerate(self.row) + ] + ) - return "%srow[%s] [%s]%s" % (worksheet_str, self.row_num, row_str, tid_str) + return f"{worksheet_str}row[{self.row_num}] [{row_str}]{tid_str}" diff --git a/src/bittytax/price/assetdata.py b/src/bittytax/price/assetdata.py index 05a0881c..a4a73a18 100644 --- a/src/bittytax/price/assetdata.py +++ b/src/bittytax/price/assetdata.py @@ -4,18 +4,19 @@ import os from ..config import config +from ..constants import CACHE_DIR from .datasource import BittyTaxAPI, DataSourceBase, Frankfurter from .exceptions import UnexpectedDataSourceError -class AssetData(object): +class AssetData: FIAT_DATASOURCES = (BittyTaxAPI.__name__, Frankfurter.__name__) def __init__(self): self.data_sources = {} - if not os.path.exists(config.CACHE_DIR): - os.mkdir(config.CACHE_DIR) + if not os.path.exists(CACHE_DIR): + os.mkdir(CACHE_DIR) for data_source_class in DataSourceBase.__subclasses__(): self.data_sources[data_source_class.__name__.upper()] = data_source_class() @@ -128,7 +129,7 @@ def get_historic_price_ds(self, req_symbol, req_date, req_data_source, no_cache= else: asset_id["quote"] = "BTC" - date = req_date.strftime("%Y-%m-%d") + date = f"{req_date:%Y-%m-%d}" pair = req_symbol + "/" + asset_id["quote"] if not no_cache: diff --git a/src/bittytax/price/bittytax_price.py b/src/bittytax/price/bittytax_price.py index 4b4f09f0..070408c7 100644 --- a/src/bittytax/price/bittytax_price.py +++ b/src/bittytax/price/bittytax_price.py @@ -10,9 +10,10 @@ import colorama import dateutil.parser -from colorama import Back, Fore +from colorama import Fore from ..config import config +from ..constants import ERROR, WARNING from ..version import __version__ from .assetdata import AssetData from .datasource import DataSourceBase @@ -26,10 +27,8 @@ if sys.stdout.encoding != "UTF-8": if sys.version_info[:2] >= (3, 7): sys.stdout.reconfigure(encoding="utf-8") - elif sys.version_info[:2] >= (3, 1): - sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) else: - sys.stdout = codecs.getwriter("utf-8")(sys.stdout) + sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) def main(): @@ -39,7 +38,7 @@ def main(): "-v", "--version", action="version", - version="%s v%s" % (parser.prog, __version__), + version=f"{parser.prog} v{__version__}", ) if sys.version_info[:2] >= (3, 7): @@ -50,8 +49,8 @@ def main(): parser_latest = subparsers.add_parser( CMD_LATEST, help="get the latest price of an asset", - description="Get the latest [asset] price (in %s). If no data source [-ds] is given, " - "the same data source(s) as 'bittytax' are used." % config.ccy, + description=f"Get the latest [asset] price (in {config.ccy}). " + "If no data source [-ds] is given, the same data source(s) as 'bittytax' are used.", ) parser_latest.add_argument( "asset", @@ -78,9 +77,8 @@ def main(): parser_history = subparsers.add_parser( CMD_HISTORY, help="get the historical price of an asset", - description="Get the historic [asset] price (in %s) for the [date] specified. " - "If no data source [-ds] is given, the same data source(s) as 'bittytax' are used." - % config.ccy, + description=f"Get the historic [asset] price (in {config.ccy}) for the [date] specified. " + "If no data source [-ds] is given, the same data source(s) as 'bittytax' are used.", ) parser_history.add_argument( "asset", @@ -147,10 +145,10 @@ def main(): config.debug = args.debug if config.debug: - print("%s%s v%s" % (Fore.YELLOW, parser.prog, __version__)) - print("%spython: v%s" % (Fore.GREEN, platform.python_version())) - print("%ssystem: %s, release: %s" % (Fore.GREEN, platform.system(), platform.release())) - config.output_config() + print(f"{Fore.YELLOW}{parser.prog} v{__version__}") + print(f"{Fore.GREEN}python: v{platform.python_version()}") + print(f"{Fore.GREEN}system: {platform.system()}, release: {platform.release()}") + config.output_config(sys.stdout) if args.command in (CMD_LATEST, CMD_HISTORY): symbol = args.asset[0] @@ -206,42 +204,27 @@ def main(): asset = True except DataSourceError as e: - parser.exit("%sERROR%s %s" % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, e)) + parser.exit(f"{ERROR} {e}") if not asset: - parser.exit( - "%sWARNING%s Prices for %s are not supported" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, symbol) - ) + parser.exit(f"{WARNING} Prices for {symbol} are not supported") if not price: if args.command == CMD_HISTORY: parser.exit( - "%sWARNING%s Price for %s on %s is not available" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - symbol, - args.date[0].strftime("%Y-%m-%d"), - ) + f"{WARNING} Price for {symbol} on {args.date[0]:%Y-%m-%d} is not available" ) else: - parser.exit( - "%sWARNING%s Current price for %s is not available" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, symbol) - ) + parser.exit(f"{WARNING} Current price for {symbol} is not available") elif args.command == CMD_LIST: symbol = args.asset try: assets = AssetData().get_assets(symbol, args.datasource, args.search_terms) except DataSourceError as e: - parser.exit("%sERROR%s %s" % (Back.RED + Fore.BLACK, Back.RESET + Fore.RED, e)) + parser.exit(f"{ERROR} {e}") if symbol and not assets: - parser.exit( - "%sWARNING%s Asset %s not found" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, symbol) - ) + parser.exit(f"{WARNING} Asset {symbol} not found") if args.search_terms and not assets: parser.exit("No results found") @@ -268,54 +251,34 @@ def get_historic_btc_price(date): def output_price(symbol, price_ccy, quantity): - print( - "%s1 %s=%s %s" - % (Fore.WHITE, symbol, config.sym() + "{:0,.2f}".format(price_ccy), config.ccy) - ) + print(f"{Fore.WHITE}1 {symbol}={config.sym()}{price_ccy:0,.2f} {config.ccy}") if quantity: quantity = Decimal(quantity) print( - "%s%s %s=%s %s" - % ( - Fore.WHITE, - "{:0,f}".format(quantity.normalize()), - symbol, - config.sym() + "{:0,.2f}".format(quantity * price_ccy), - config.ccy, - ) + f"{Fore.WHITE}{quantity.normalize():0,f} {symbol}=" + f"{config.sym()}{quantity * price_ccy:0,.2f} {config.ccy}" ) def output_ds_price(asset): print( - "%s1 %s=%s %s %svia %s (%s)%s" - % ( - Fore.YELLOW, - asset["symbol"], - "{:0,f}".format(asset["price"].normalize()), - asset["quote"], - Fore.CYAN, - asset["data_source"], - asset["name"], - Fore.YELLOW + " <-" if asset.get("priority") else "", - ) + f'{Fore.YELLOW}1 {asset["symbol"]}={asset["price"].normalize():0,f} {asset["quote"]} ' + f'{Fore.CYAN} via {asset["data_source"]} ({asset["name"]})' + f'{Fore.YELLOW + " <-" if asset.get("priority") else ""}' ) def output_assets(assets): for asset in assets: + if asset["id"]: + id_str = f' [ID:{asset["id"]}]' + else: + id_str = "" + print( - "%s%s (%s) %svia %s%s%s" - % ( - Fore.WHITE, - # Fore.YELLOW if asset['priority'] else Fore.WHITE, - asset["symbol"], - asset["name"], - Fore.CYAN, - asset["data_source"], - " [ID:{}]".format(asset["id"]) if asset["id"] else "", - Fore.YELLOW + " <-" if asset["priority"] else "", - ) + f'{Fore.WHITE}{asset["symbol"]} ({asset["name"]})' + f'{Fore.CYAN} via {asset["data_source"]}{id_str}' + f'{Fore.YELLOW + " <-" if asset["priority"] else ""}' ) @@ -332,8 +295,8 @@ def validate_date(value): try: date = dateutil.parser.parse(value, dayfirst=dayfirst) - except ValueError: - raise argparse.ArgumentTypeError("date is not valid") + except ValueError as e: + raise argparse.ArgumentTypeError("date is not valid") from e return date.replace(tzinfo=config.TZ_LOCAL) @@ -341,8 +304,8 @@ def validate_date(value): def validate_quantity(value): try: quantity = Decimal(value.replace(",", "")) - except InvalidOperation: - raise argparse.ArgumentTypeError("quantity is not valid") + except InvalidOperation as e: + raise argparse.ArgumentTypeError("quantity is not valid") from e return quantity diff --git a/src/bittytax/price/datasource.py b/src/bittytax/price/datasource.py index 5b2afa0d..ca375c20 100644 --- a/src/bittytax/price/datasource.py +++ b/src/bittytax/price/datasource.py @@ -10,9 +10,10 @@ import dateutil.parser import requests -from colorama import Back, Fore +from colorama import Fore from ..config import config +from ..constants import CACHE_DIR, TZ_UTC, WARNING from ..version import __version__ from .exceptions import UnexpectedDataSourceAssetIdError @@ -20,13 +21,12 @@ COINPAPRIKA_MAX_DAYS = 5000 -class DataSourceBase(object): - USER_AGENT = "BittyTax/%s Python/%s %s/%s" % ( - __version__, - platform.python_version(), - platform.system(), - platform.release(), +class DataSourceBase: + USER_AGENT = ( + f"BittyTax/{__version__} Python/{platform.python_version()} " + f"{platform.system()}/{platform.release()}" ) + TIME_OUT = 30 def __init__(self): @@ -36,7 +36,7 @@ def __init__(self): for pair in sorted(self.prices): if config.debug: - print("%sprice: %s (%s) data cache loaded" % (Fore.YELLOW, self.name(), pair)) + print(f"{Fore.YELLOW}price: {self.name()} ({pair}) data cache loaded") atexit.register(self.dump_prices) @@ -45,7 +45,7 @@ def name(self): def get_json(self, url): if config.debug: - print("%sprice: GET %s" % (Fore.YELLOW, url)) + print(f"{Fore.YELLOW}price: GET {url}") response = requests.get(url, headers={"User-Agent": self.USER_AGENT}, timeout=self.TIME_OUT) @@ -70,19 +70,19 @@ def update_prices(self, pair, prices, timestamp): # We might not receive data for the date requested, if so set to None to prevent repeat # lookups, assuming date is in the past - date = timestamp.strftime("%Y-%m-%d") + date = f"{timestamp:%Y-%m-%d}" if date not in prices and timestamp.date() < datetime.now().date(): prices[date] = {"price": None, "url": None} self.prices[pair].update(prices) def load_prices(self): - filename = os.path.join(config.CACHE_DIR, self.name() + ".json") + filename = os.path.join(CACHE_DIR, self.name() + ".json") if not os.path.exists(filename): return {} try: - with open(filename, "r") as price_cache: + with open(filename, "r", encoding="utf-8") as price_cache: json_prices = json.load(price_cache) return { pair: { @@ -95,14 +95,13 @@ def load_prices(self): for pair in json_prices } except (IOError, ValueError): - print( - "%sWARNING%s Data cached for %s could not be loaded" - % (Back.YELLOW + Fore.BLACK, Back.RESET + Fore.YELLOW, self.name()) - ) + print(f"{WARNING} Data cached for {self.name()} could not be loaded") return {} def dump_prices(self): - with open(os.path.join(config.CACHE_DIR, self.name() + ".json"), "w") as price_cache: + with open( + os.path.join(CACHE_DIR, self.name() + ".json"), "w", encoding="utf-8" + ) as price_cache: json_prices = { pair: { date: { @@ -132,14 +131,9 @@ def _update_asset(self, symbol, data_source): if config.debug: print( - "%sprice: %s updated as %s [ID:%s] (%s)" - % ( - Fore.YELLOW, - symbol, - self.name(), - asset_id, - self.ids[asset_id]["name"], - ) + f"{Fore.YELLOW}price: " + f"{symbol} updated as {self.name()} [ID:{asset_id}] " + f'({self.ids[asset_id]["name"]})' ) else: raise UnexpectedDataSourceAssetIdError(data_source, symbol) @@ -152,14 +146,9 @@ def _add_asset(self, symbol, data_source): if config.debug: print( - "%sprice: %s added as %s [ID:%s] (%s)" - % ( - Fore.YELLOW, - symbol, - self.name(), - asset_id, - self.ids[asset_id]["name"], - ) + f"{Fore.YELLOW}price: " + f"{symbol} added as {self.name()} [ID:{asset_id}] " + f'({self.ids[asset_id]["name"]})' ) else: raise UnexpectedDataSourceAssetIdError(data_source, symbol) @@ -167,17 +156,17 @@ def _add_asset(self, symbol, data_source): def get_list(self): if self.ids: asset_list = {} - for t in self.ids: - symbol = self.ids[t]["symbol"] + for t, ids in self.ids.items(): + symbol = ids["symbol"] if symbol not in asset_list: asset_list[symbol] = [] - asset_list[symbol].append({"id": t, "name": self.ids[t]["name"]}) + asset_list[symbol].append({"id": t, "name": ids["name"]}) # Include any custom symbols as well - for symbol in asset_list: - if self.assets[symbol] not in asset_list[symbol]: - asset_list[symbol].append(self.assets[symbol]) + for symbol, assets in asset_list.items(): + if self.assets[symbol] not in assets: + assets.append(self.assets[symbol]) return asset_list return {k: [{"id": None, "name": v["name"]}] for k, v in self.assets.items()} @@ -196,26 +185,24 @@ def str_to_decimal(price): @staticmethod def decimal_to_str(price): if price: - return "{0:f}".format(price) + return f"{price:f}" return None @staticmethod def epoch_time(timestamp): - epoch = (timestamp - datetime(1970, 1, 1, tzinfo=config.TZ_UTC)).total_seconds() + epoch = (timestamp - datetime(1970, 1, 1, tzinfo=TZ_UTC)).total_seconds() return int(epoch) class BittyTaxAPI(DataSourceBase): def __init__(self): - super(BittyTaxAPI, self).__init__() + super().__init__() json_resp = self.get_json("https://api.bitty.tax/v1/symbols") self.assets = {k: {"name": v} for k, v in json_resp["symbols"].items()} def get_latest(self, asset, quote, _asset_id=None): - json_resp = self.get_json( - "https://api.bitty.tax/v1/latest?base=%s&symbols=%s" % (asset, quote) - ) + json_resp = self.get_json(f"https://api.bitty.tax/v1/latest?base={asset}&symbols={quote}") return ( Decimal(repr(json_resp["rates"][quote])) if "rates" in json_resp and quote in json_resp["rates"] @@ -223,18 +210,14 @@ def get_latest(self, asset, quote, _asset_id=None): ) def get_historical(self, asset, quote, timestamp, _asset_id=None): - url = "https://api.bitty.tax/v1/%s?base=%s&symbols=%s" % ( - timestamp.strftime("%Y-%m-%d"), - asset, - quote, - ) + url = f"https://api.bitty.tax/v1/{timestamp:%Y-%m-%d}?base={asset}&symbols={quote}" json_resp = self.get_json(url) pair = self.pair(asset, quote) # Date returned in response might not be date requested due to weekends/holidays self.update_prices( pair, { - timestamp.strftime("%Y-%m-%d"): { + f"{timestamp:%Y-%m-%d}": { "price": Decimal(repr(json_resp["rates"][quote])) if "rates" in json_resp and quote in json_resp["rates"] else None, @@ -247,7 +230,7 @@ def get_historical(self, asset, quote, timestamp, _asset_id=None): class Frankfurter(DataSourceBase): def __init__(self): - super(Frankfurter, self).__init__() + super().__init__() currencies = [ "EUR", "USD", @@ -295,9 +278,7 @@ def __init__(self): self.assets = {c: {"name": "Fiat " + c} for c in currencies} def get_latest(self, asset, quote, _asset_id=None): - json_resp = self.get_json( - "https://api.frankfurter.app/latest?from=%s&to=%s" % (asset, quote) - ) + json_resp = self.get_json(f"https://api.frankfurter.app/latest?from={asset}&to={quote}") return ( Decimal(repr(json_resp["rates"][quote])) if "rates" in json_resp and quote in json_resp["rates"] @@ -305,18 +286,14 @@ def get_latest(self, asset, quote, _asset_id=None): ) def get_historical(self, asset, quote, timestamp, _asset_id=None): - url = "https://api.frankfurter.app/%s?from=%s&to=%s" % ( - timestamp.strftime("%Y-%m-%d"), - asset, - quote, - ) + url = f"https://api.frankfurter.app/{timestamp:%Y-%m-%d}?from={asset}&to={quote}" json_resp = self.get_json(url) pair = self.pair(asset, quote) # Date returned in response might not be date requested due to weekends/holidays self.update_prices( pair, { - timestamp.strftime("%Y-%m-%d"): { + f"{timestamp:%Y-%m-%d}": { "price": Decimal(repr(json_resp["rates"][quote])) if "rates" in json_resp and quote in json_resp["rates"] else None, @@ -329,7 +306,7 @@ def get_historical(self, asset, quote, timestamp, _asset_id=None): class CoinDesk(DataSourceBase): def __init__(self): - super(CoinDesk, self).__init__() + super().__init__() self.assets = {"BTC": {"name": "Bitcoin"}} def get_latest(self, _asset, quote, _asset_id=None): @@ -342,13 +319,8 @@ def get_latest(self, _asset, quote, _asset_id=None): def get_historical(self, asset, quote, timestamp, _asset_id=None): url = ( - "https://api.coindesk.com/v1/bpi/historical/close.json" - "?start=%s&end=%s¤cy=%s" - % ( - timestamp.strftime("%Y-%m-%d"), - datetime.now().strftime("%Y-%m-%d"), - quote, - ) + f"https://api.coindesk.com/v1/bpi/historical/close.json" + f"?start={timestamp:%Y-%m-%d}&end={datetime.now():%Y-%m-%d}¤cy={quote}" ) json_resp = self.get_json(url) pair = self.pair(asset, quote) @@ -365,7 +337,7 @@ def get_historical(self, asset, quote, timestamp, _asset_id=None): class CryptoCompare(DataSourceBase): def __init__(self): - super(CryptoCompare, self).__init__() + super().__init__() json_resp = self.get_json("https://min-api.cryptocompare.com/data/all/coinlist") self.assets = { c[1]["Symbol"].strip().upper(): {"name": c[1]["CoinName"].strip()} @@ -375,22 +347,17 @@ def __init__(self): def get_latest(self, asset, quote, _asset_id=None): json_resp = self.get_json( - "https://min-api.cryptocompare.com/data/price" - "?extraParams=%s&fsym=%s&tsyms=%s" % (self.USER_AGENT, asset, quote) + f"https://min-api.cryptocompare.com/data/price" + f"?extraParams={self.USER_AGENT}&fsym={asset}&tsyms={quote}" ) return Decimal(repr(json_resp[quote])) if quote in json_resp else None def get_historical(self, asset, quote, timestamp, _asset_id=None): url = ( - "https://min-api.cryptocompare.com/data/histoday?aggregate=1&extraParams=%s" - "&fsym=%s&tsym=%s&limit=%s&toTs=%d" - % ( - self.USER_AGENT, - asset, - quote, - CRYPTOCOMPARE_MAX_DAYS, - self.epoch_time(timestamp + timedelta(days=CRYPTOCOMPARE_MAX_DAYS)), - ) + f"https://min-api.cryptocompare.com/data/histoday?aggregate=1" + f"&extraParams={self.USER_AGENT}&fsym={asset}&tsym={quote}" + f"&limit={CRYPTOCOMPARE_MAX_DAYS}" + f"&toTs={self.epoch_time(timestamp + timedelta(days=CRYPTOCOMPARE_MAX_DAYS))}" ) json_resp = self.get_json(url) @@ -400,7 +367,7 @@ def get_historical(self, asset, quote, timestamp, _asset_id=None): self.update_prices( pair, { - datetime.fromtimestamp(d["time"]).strftime("%Y-%m-%d"): { + f'{datetime.fromtimestamp(d["time"]):%Y-%m-%d}': { "price": Decimal(repr(d["close"])) if "close" in d and d["close"] else None, "url": url, } @@ -412,7 +379,7 @@ def get_historical(self, asset, quote, timestamp, _asset_id=None): class CoinGecko(DataSourceBase): def __init__(self): - super(CoinGecko, self).__init__() + super().__init__() json_resp = self.get_json("https://api.coingecko.com/api/v3/coins/list") self.ids = { c["id"]: {"symbol": c["symbol"].strip().upper(), "name": c["name"].strip()} @@ -429,8 +396,8 @@ def get_latest(self, asset, quote, asset_id=None): asset_id = self.assets[asset]["id"] json_resp = self.get_json( - "https://api.coingecko.com/api/v3/coins/%s?localization=false" - "&community_data=false&developer_data=false" % asset_id + f"https://api.coingecko.com/api/v3/coins/{asset_id}?localization=false" + f"&community_data=false&developer_data=false" ) return ( Decimal(repr(json_resp["market_data"]["current_price"][quote.lower()])) @@ -444,9 +411,9 @@ def get_historical(self, asset, quote, timestamp, asset_id=None): if asset_id is None: asset_id = self.assets[asset]["id"] - url = "https://api.coingecko.com/api/v3/coins/%s/market_chart?vs_currency=%s&days=max" % ( - asset_id, - quote, + url = ( + f"https://api.coingecko.com/api/v3/coins/{asset_id}/market_chart" + f"?vs_currency={quote}&days=max" ) json_resp = self.get_json(url) pair = self.pair(asset, quote) @@ -454,7 +421,7 @@ def get_historical(self, asset, quote, timestamp, asset_id=None): self.update_prices( pair, { - datetime.utcfromtimestamp(p[0] / 1000).strftime("%Y-%m-%d"): { + f"{datetime.utcfromtimestamp(p[0] / 1000):%Y-%m-%d}": { "price": Decimal(repr(p[1])) if p[1] else None, "url": url, } @@ -466,7 +433,7 @@ def get_historical(self, asset, quote, timestamp, asset_id=None): class CoinPaprika(DataSourceBase): def __init__(self): - super(CoinPaprika, self).__init__() + super().__init__() json_resp = self.get_json("https://api.coinpaprika.com/v1/coins") self.ids = { c["id"]: {"symbol": c["symbol"].strip().upper(), "name": c["name"].strip()} @@ -483,7 +450,7 @@ def get_latest(self, asset, quote, asset_id=None): asset_id = self.assets[asset]["id"] json_resp = self.get_json( - "https://api.coinpaprika.com/v1/tickers/%s?quotes=%s" % ((asset_id, quote)) + f"https://api.coinpaprika.com/v1/tickers/{asset_id}?quotes={quote}" ) return ( Decimal(repr(json_resp["quotes"][quote]["price"])) @@ -500,9 +467,8 @@ def get_historical(self, asset, quote, timestamp, asset_id=None): asset_id = self.assets[asset]["id"] url = ( - "https://api.coinpaprika.com/v1/tickers/%s/historical" - "?start=%s&limit=%s"e=%s&interval=1d" - % (asset_id, timestamp.strftime("%Y-%m-%d"), COINPAPRIKA_MAX_DAYS, quote) + "https://api.coinpaprika.com/v1/tickers/{asset_id}/historical" + "?start={timestamp:%Y-%m-%d}&limit={COINPAPRIKA_MAX_DAYS}"e={quote}&interval=1d" ) json_resp = self.get_json(url) @@ -510,7 +476,7 @@ def get_historical(self, asset, quote, timestamp, asset_id=None): self.update_prices( pair, { - dateutil.parser.parse(p["timestamp"]).strftime("%Y-%m-%d"): { + f'{dateutil.parser.parse(p["timestamp"]):%Y-%m-%d}': { "price": Decimal(repr(p["price"])) if p["price"] else None, "url": url, } diff --git a/src/bittytax/price/exceptions.py b/src/bittytax/price/exceptions.py index c3f54dd0..01a47af2 100644 --- a/src/bittytax/price/exceptions.py +++ b/src/bittytax/price/exceptions.py @@ -4,28 +4,28 @@ import os from ..config import config +from ..constants import BITTYTAX_PATH class DataSourceError(Exception): def __init__(self, data_source, value=None): - super(DataSourceError, self).__init__() + super().__init__() self.data_source = data_source self.value = value class UnexpectedDataSourceError(DataSourceError): def __str__(self): - return "Invalid data source: '%s' in %s, use {%s}" % ( - self.data_source, - os.path.join(config.BITTYTAX_PATH, config.BITTYTAX_CONFIG), - ",".join([ds.__name__ for ds in self.value.__subclasses__()]), + return ( + f"Invalid data source: '{self.data_source}' in " + f"{os.path.join(BITTYTAX_PATH, config.BITTYTAX_CONFIG)}, use " + f"{{{','.join([ds.__name__ for ds in self.value.__subclasses__()])}}}" ) class UnexpectedDataSourceAssetIdError(DataSourceError): def __str__(self): - return "Invalid data source asset ID: '%s' for '%s' in %s" % ( - self.data_source, - self.value, - os.path.join(config.BITTYTAX_PATH, config.BITTYTAX_CONFIG), + return ( + f"Invalid data source asset ID: '{self.data_source}' for '{self.value}' in " + f"{os.path.join(BITTYTAX_PATH, config.BITTYTAX_CONFIG)}" ) diff --git a/src/bittytax/price/pricedata.py b/src/bittytax/price/pricedata.py index 9912fdd8..9e8aff1f 100644 --- a/src/bittytax/price/pricedata.py +++ b/src/bittytax/price/pricedata.py @@ -6,17 +6,18 @@ from colorama import Fore from ..config import config +from ..constants import CACHE_DIR from .datasource import DataSourceBase from .exceptions import UnexpectedDataSourceError -class PriceData(object): +class PriceData: def __init__(self, data_sources_required, price_tool=False): self.price_tool = price_tool self.data_sources = {} - if not os.path.exists(config.CACHE_DIR): - os.mkdir(config.CACHE_DIR) + if not os.path.exists(CACHE_DIR): + os.mkdir(CACHE_DIR) for data_source_class in DataSourceBase.__subclasses__(): if data_source_class.__name__.upper() in [ds.upper() for ds in data_sources_required]: @@ -44,7 +45,7 @@ def get_latest_ds(self, data_source, asset, quote): def get_historical_ds(self, data_source, asset, quote, timestamp, no_cache=False): if data_source.upper() in self.data_sources: if asset in self.data_sources[data_source.upper()].assets: - date = timestamp.strftime("%Y-%m-%d") + date = f"{timestamp:%Y-%m-%d}" pair = asset + "/" + quote if not no_cache: @@ -84,28 +85,14 @@ def get_latest(self, asset, quote): if price is not None: if config.debug: print( - "%sprice: , 1 %s=%s %s via %s (%s)" - % ( - Fore.YELLOW, - asset, - "{:0,f}".format(price.normalize()), - quote, - self.data_sources[data_source.upper()].name(), - name, - ) + f"{Fore.YELLOW}price: , 1 " + f"{asset}={price.normalize():0,f} {quote} via " + f"{self.data_sources[data_source.upper()].name()} ({name})" ) if self.price_tool: print( - "%s1 %s=%s %s %svia %s (%s)" - % ( - Fore.YELLOW, - asset, - "{:0,f}".format(price.normalize()), - quote, - Fore.CYAN, - self.data_sources[data_source.upper()].name(), - name, - ) + f"{Fore.YELLOW}1 {asset}={price.normalize():0,f} {quote} " + f"{Fore.CYAN}via {self.data_sources[data_source.upper()].name()} ({name})" ) return price, name, self.data_sources[data_source.upper()].name() return None, name, None @@ -119,29 +106,14 @@ def get_historical(self, asset, quote, timestamp, no_cache=False): if price is not None: if config.debug: print( - "%sprice: %s, 1 %s=%s %s via %s (%s)" - % ( - Fore.YELLOW, - timestamp.strftime("%Y-%m-%d"), - asset, - "{:0,f}".format(price.normalize()), - quote, - self.data_sources[data_source.upper()].name(), - name, - ) + f"{Fore.YELLOW}price: {timestamp:%Y-%m-%d}, 1 " + f"{asset}={price.normalize():0,f} {quote} via " + f"{self.data_sources[data_source.upper()].name()} ({name})" ) if self.price_tool: print( - "%s1 %s=%s %s %svia %s (%s)" - % ( - Fore.YELLOW, - asset, - "{:0,f}".format(price.normalize()), - quote, - Fore.CYAN, - self.data_sources[data_source.upper()].name(), - name, - ) + f"{Fore.YELLOW}1 {asset}={price.normalize():0,f} {quote} " + f"{Fore.CYAN}via {self.data_sources[data_source.upper()].name()} ({name})" ) return price, name, self.data_sources[data_source.upper()].name(), url return None, name, None, None diff --git a/src/bittytax/price/valueasset.py b/src/bittytax/price/valueasset.py index 64795ce2..a506cd16 100755 --- a/src/bittytax/price/valueasset.py +++ b/src/bittytax/price/valueasset.py @@ -4,14 +4,15 @@ from datetime import datetime from decimal import Decimal -from colorama import Back, Fore, Style +from colorama import Fore, Style from tqdm import tqdm from ..config import config +from ..constants import WARNING from .pricedata import PriceData -class ValueAsset(object): +class ValueAsset: def __init__(self, price_tool=False): self.price_tool = price_tool self.price_report = {} @@ -32,32 +33,16 @@ def get_value(self, asset, timestamp, quantity): value = asset_price_ccy * quantity if config.debug: print( - "%sprice: %s, 1 %s=%s %s, %s %s=%s%s %s%s" - % ( - Fore.YELLOW, - timestamp.strftime("%Y-%m-%d"), - asset, - config.sym() + "{:0,.2f}".format(asset_price_ccy), - config.ccy, - "{:0,f}".format(quantity.normalize()), - asset, - Style.BRIGHT, - config.sym() + "{:0,.2f}".format(value), - config.ccy, - Style.NORMAL, - ) + f"{Fore.YELLOW}price: {timestamp:%Y-%m-%d}, 1 " + f"{asset}={config.sym()}{asset_price_ccy:0,.2f} {config.ccy}, " + f"{quantity.normalize():0,f} {asset}=" + f"{Style.BRIGHT}{config.sym()}{value:0,.2f} {config.ccy}{Style.NORMAL}" ) return value, False tqdm.write( - "%sWARNING%s Price for %s on %s is not available, using price of %s" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - asset, - timestamp.strftime("%Y-%m-%d"), - config.sym() + "{:0,.2f}".format(0), - ) + f"{WARNING} Price for {asset} on {timestamp:%Y-%m-%d} is not available, " + f"using price of {config.sym()}{0:0,.2f}" ) return Decimal(0), False @@ -73,14 +58,8 @@ def get_historical_price(self, asset, timestamp, no_cache=False): if not self.price_tool and timestamp.date() >= datetime.now().date(): tqdm.write( - "%sWARNING%s Price for %s on %s, no historic price available, " - "using latest price" - % ( - Back.YELLOW + Fore.BLACK, - Back.RESET + Fore.YELLOW, - asset, - timestamp.strftime("%Y-%m-%d"), - ) + f"{WARNING} Price for {asset} on {timestamp:Y-%m-%d}, no historic price available, " + f"using latest price" ) return self.get_latest_price(asset) @@ -146,7 +125,7 @@ def price_report_cache( if asset not in self.price_report[tax_year]: self.price_report[tax_year][asset] = {} - date = timestamp.strftime("%Y-%m-%d") + date = f"{timestamp:%Y-%m-%d}" if date not in self.price_report[tax_year][asset]: self.price_report[tax_year][asset][date] = { "name": name, diff --git a/src/bittytax/record.py b/src/bittytax/record.py index 0e02a1e1..722ae978 100644 --- a/src/bittytax/record.py +++ b/src/bittytax/record.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # (c) Nano Nano Ltd 2019 -import sys - from .config import config # pylint: disable=too-few-public-methods, too-many-instance-attributes -class TransactionRecord(object): +class TransactionRecord: TYPE_DEPOSIT = "Deposit" TYPE_MINING = "Mining" TYPE_STAKING = "Staking" @@ -81,10 +79,9 @@ def set_tid(self): def _format_fee(self): if self.fee: - return " + fee=%s %s%s" % ( - self._format_quantity(self.fee.quantity), - self._format_str(self.fee.asset), - self._format_value(self.fee.proceeds), + return ( + f" + fee={self._format_quantity(self.fee.quantity)} " + f"{self.fee.asset}{self._format_value(self.fee.proceeds)}" ) return "" @@ -92,39 +89,31 @@ def _format_fee(self): def _format_quantity(quantity): if quantity is None: return "" - return "{:0,f}".format(quantity.normalize()) + return f"{quantity.normalize():0,f}" @staticmethod def _format_value(value): if value is not None: - return " (%s %s)" % (config.sym() + "{:0,.2f}".format(value), config.ccy) + return f" ({config.sym()}{value:0,.2f} {config.ccy})" return "" @staticmethod def _format_note(note): if note: - if sys.version_info[0] < 3: - return "'%s' " % note.decode("utf8") - return "'%s' " % note + return f"'{note}' " return "" @staticmethod def _format_timestamp(timestamp): if timestamp.microsecond: - return timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f %Z") - return timestamp.strftime("%Y-%m-%dT%H:%M:%S %Z") + return f"{timestamp:%Y-%m-%dT%H:%M:%S.%f %Z}" + return f"{timestamp:%Y-%m-%dT%H:%M:%S %Z}" @staticmethod def _format_decimal(decimal): if decimal is None: return "" - return "{:0f}".format(decimal.normalize()) - - @staticmethod - def _format_str(string): - if sys.version_info[0] < 3: - return string.decode("utf8") - return string + return f"{decimal.normalize():0f}" def __eq__(self, other): return self.timestamp == other.timestamp @@ -137,43 +126,43 @@ def __lt__(self, other): def __str__(self): if self.buy and self.sell: - return "%s %s %s%s <- %s %s%s%s '%s' %s %s[TID:%s]" % ( - self.t_type, - self._format_quantity(self.buy.quantity), - self._format_str(self.buy.asset), - self._format_value(self.buy.cost), - self._format_quantity(self.sell.quantity), - self._format_str(self.sell.asset), - self._format_value(self.sell.proceeds), - self._format_fee(), - self._format_str(self.wallet), - self._format_timestamp(self.timestamp), - self._format_note(self.note), - self.tid[0], + return ( + f"{self.t_type} " + f"{self._format_quantity(self.buy.quantity)} " + f"{self.buy.asset}" + f"{self._format_value(self.buy.cost)} <- " + f"{self._format_quantity(self.sell.quantity)} " + f"{self.sell.asset}" + f"{self._format_value(self.sell.proceeds)}" + f"{self._format_fee()} " + f"'{self.wallet}' " + f"{self._format_timestamp(self.timestamp)} " + f"{self._format_note(self.note)}" + f"[TID:{self.tid[0]}]" ) if self.buy: - return "%s %s %s%s%s '%s' %s %s[TID:%s]" % ( - self.t_type, - self._format_quantity(self.buy.quantity), - self._format_str(self.buy.asset), - self._format_value(self.buy.cost), - self._format_fee(), - self._format_str(self.wallet), - self._format_timestamp(self.timestamp), - self._format_note(self.note), - self.tid[0], + return ( + f"{self.t_type} " + f"{self._format_quantity(self.buy.quantity)} " + f"{self.buy.asset}" + f"{self._format_value(self.buy.cost)}" + f"{self._format_fee()} " + f"'{self.wallet}' " + f"{self._format_timestamp(self.timestamp)} " + f"{self._format_note(self.note)}" + f"[TID:{self.tid[0]}]" ) if self.sell: - return "%s %s %s%s%s '%s' %s %s[TID:%s]" % ( - self.t_type, - self._format_quantity(self.sell.quantity), - self._format_str(self.sell.asset), - self._format_value(self.sell.proceeds), - self._format_fee(), - self._format_str(self.wallet), - self._format_timestamp(self.timestamp), - self._format_note(self.note), - self.tid[0], + return ( + f"{self.t_type} " + f"{self._format_quantity(self.sell.quantity)} " + f"{self.sell.asset}" + f"{self._format_value(self.sell.proceeds)}" + f"{self._format_fee()} " + f"'{self.wallet}' " + f"{self._format_timestamp(self.timestamp)} " + f"{self._format_note(self.note)}" + f"[TID:{self.tid[0]}]" ) return [] @@ -182,33 +171,33 @@ def to_csv(self): return [ self.t_type, self._format_decimal(self.buy.quantity), - self._format_str(self.buy.asset), + self.buy.asset, self._format_decimal(self.buy.cost), self._format_decimal(self.sell.quantity), - self._format_str(self.sell.asset), + self.sell.asset, self._format_decimal(self.sell.proceeds), self._format_decimal(self.fee.quantity) if self.fee else "", - self._format_str(self.fee.asset) if self.fee else "", + self.fee.asset if self.fee else "", self._format_decimal(self.fee.proceeds) if self.fee else "", - self._format_str(self.wallet), + self.wallet, self._format_timestamp(self.timestamp), - self._format_str(self.note), + self.note, ] if self.buy: return [ self.t_type, self._format_decimal(self.buy.quantity), - self._format_str(self.buy.asset), + self.buy.asset, self._format_decimal(self.buy.cost), "", "", "", self._format_decimal(self.fee.quantity) if self.fee else "", - self._format_str(self.fee.asset) if self.fee else "", + self.fee.asset if self.fee else "", self._format_decimal(self.fee.proceeds) if self.fee else "", - self._format_str(self.wallet), + self.wallet, self._format_timestamp(self.timestamp), - self._format_str(self.note), + self.note, ] if self.sell: return [ @@ -217,13 +206,13 @@ def to_csv(self): "", "", self._format_decimal(self.sell.quantity), - self._format_str(self.sell.asset), + self.sell.asset, self._format_decimal(self.sell.proceeds), self._format_decimal(self.fee.quantity) if self.fee else "", - self._format_str(self.fee.asset) if self.fee else "", + self.fee.asset if self.fee else "", self._format_decimal(self.fee.proceeds) if self.fee else "", - self._format_str(self.wallet), + self.wallet, self._format_timestamp(self.timestamp), - self._format_str(self.note), + self.note, ] return [] diff --git a/src/bittytax/report.py b/src/bittytax/report.py index bd41e6c7..a49b44ad 100644 --- a/src/bittytax/report.py +++ b/src/bittytax/report.py @@ -10,14 +10,15 @@ import dateutil.parser import jinja2 -from colorama import Back, Fore, Style +from colorama import Fore, Style from xhtml2pdf import pisa from .config import config +from .constants import _H1, ERROR, H1, TAX_RULES_UK_COMPANY from .version import __version__ -class ReportPdf(object): +class ReportPdf: DEFAULT_FILENAME = "BittyTax_Report" FILE_EXTENSION = "pdf" TEMPLATE_FILE = "tax_report.html" @@ -34,12 +35,13 @@ def __init__(self, progname, audit, tax_report, price_report, holdings_report, a self.env.filters["ratesfilter"] = self.ratesfilter self.env.filters["nowrapfilter"] = self.nowrapfilter self.env.filters["lenfilter"] = self.lenfilter + self.env.globals["TAX_RULES_UK_COMPANY"] = TAX_RULES_UK_COMPANY template = self.env.get_template(self.TEMPLATE_FILE) html = template.render( { "date": datetime.now(), - "author": "{} v{}".format(progname, __version__), + "author": f"{progname} v{__version__}", "config": config, "audit": audit, "tax_report": tax_report, @@ -54,49 +56,41 @@ def __init__(self, progname, audit, tax_report, price_report, holdings_report, a status = pisa.CreatePDF(html, dest=pdf_file) if not status.err: - print("%sPDF tax report created: %s%s" % (Fore.WHITE, Fore.YELLOW, self.filename)) + print(f"{Fore.WHITE}PDF tax report created: {Fore.YELLOW}{self.filename}") else: - print( - "%sERROR%s Failed to create PDF tax report", - (Back.RED + Fore.BLACK, Back.RESET + Fore.RED), - ) + print(f"{ERROR} Failed to create PDF tax report") @staticmethod def datefilter(date): if isinstance(date, datetime): - return date.strftime("%d/%m/%Y") - return dateutil.parser.parse(date).strftime("%d/%m/%Y") + return f"{date:%d/%m/%Y}" + return f"{dateutil.parser.parse(date):%d/%m/%Y}" @staticmethod def datefilter2(date): - return "{} {}{} {}".format( - date.strftime("%b"), - date.day, - ReportLog.format_day(date.day), - date.strftime("%Y"), - ) + return f"{date:%b} {date.day}{ReportLog.format_day(date.day)} {date:%Y}" @staticmethod def quantityfilter(quantity): - return "{:0,f}".format(quantity.normalize()) + return f"{quantity.normalize():0,f}" @staticmethod def valuefilter(value): if config.ccy == "GBP": - return "£{:0,.2f}".format(value) + return f"£{value:0,.2f}" if config.ccy == "EUR": - return "€{:0,.2f}".format(value) + return f"€{value:0,.2f}" if config.ccy in ("USD", "AUD", "NZD"): - return "${:0,.2f}".format(value) + return f"${value:0,.2f}" if config.ccy in ("DKK", "NOK", "SEK"): - return "kr.{:0,.2f}".format(value) + return f"kr.{value:0,.2f}" raise ValueError("Currency not supported") @staticmethod def ratefilter(rate): if rate is None: return "*" - return "{}%".format(rate) + return f"{rate}%" @staticmethod def ratesfilter(rates): @@ -124,15 +118,15 @@ def get_output_filename(filename, extension_type): filepath, file_extension = os.path.splitext(filepath) i = 2 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" while os.path.exists(new_fname): i += 1 - new_fname = "%s-%s%s" % (filepath, i, file_extension) + new_fname = f"{filepath}-{i}{file_extension}" return new_fname -class ReportLog(object): +class ReportLog: MAX_SYMBOL_LEN = 8 MAX_NAME_LEN = 32 MAX_NOTE_LEN = 40 @@ -144,25 +138,20 @@ def __init__(self, audit, tax_report, price_report, holdings_report, args): self.price_report = price_report self.holdings_report = holdings_report - print("%stax report output:" % Fore.WHITE) + print(f"{Fore.WHITE}tax report output:") if args.taxyear: if not args.summary: self.audit() print( - "\n%sTax Year - %s (%s to %s)%s" - % ( - Fore.CYAN + Style.BRIGHT, - config.format_tax_year(args.taxyear), - self.format_date2(config.get_tax_year_start(args.taxyear)), - self.format_date2(config.get_tax_year_end(args.taxyear)), - Style.NORMAL, - ) + f"{H1}Tax Year - {config.format_tax_year(args.taxyear)} " + f"({self.format_date2(config.get_tax_year_start(args.taxyear))} to " + f"{self.format_date2(config.get_tax_year_end(args.taxyear))}){_H1}" ) self.capital_gains(args.taxyear, args.tax_rules, args.summary) if not args.summary: self.income(args.taxyear) - print("\n%sAppendix%s" % (Fore.CYAN + Style.BRIGHT, Style.NORMAL)) + print("{H1}Appendix{_H1}") self.price_data(args.taxyear) else: if not args.summary: @@ -170,67 +159,48 @@ def __init__(self, audit, tax_report, price_report, holdings_report, args): for tax_year in sorted(tax_report): print( - "\n%sTax Year - %s (%s to %s)%s" - % ( - Fore.CYAN + Style.BRIGHT, - config.format_tax_year(tax_year), - self.format_date2(config.get_tax_year_start(tax_year)), - self.format_date2(config.get_tax_year_end(tax_year)), - Style.NORMAL, - ) + f"{H1}Tax Year - {config.format_tax_year(tax_year)} " + f"({self.format_date2(config.get_tax_year_start(tax_year))} to " + f"{self.format_date2(config.get_tax_year_end(tax_year))}){_H1}" ) self.capital_gains(tax_year, args.tax_rules, args.summary) if not args.summary: self.income(tax_year) if not args.summary: - print("\n%sAppendix%s" % (Fore.CYAN + Style.BRIGHT, Style.NORMAL)) + print(f"{H1}Appendix{_H1}") for tax_year in sorted(tax_report): self.price_data(tax_year) print("") self.holdings() def audit(self): - print("\n%sAudit%s" % (Fore.CYAN + Style.BRIGHT, Style.NORMAL)) - print("%sFinal Balances" % Fore.CYAN) + print(f"{H1}Audit{_H1}") + print(f"{Fore.CYAN}Final Balances") for wallet in sorted(self.audit_report.wallets, key=str.lower): - print( - "\n%s%-30s %s %25s" - % (Fore.YELLOW, "Wallet", "Asset".ljust(self.MAX_SYMBOL_LEN), "Balance") - ) + print(f'\n{Fore.YELLOW}{"Wallet":<30} {"Asset":<{self.MAX_SYMBOL_LEN}} {"Balance":>25}') for asset in sorted(self.audit_report.wallets[wallet]): print( - "%s%-30s %s %25s" - % ( - Fore.WHITE, - wallet, - asset.ljust(self.MAX_SYMBOL_LEN), - self.format_quantity(self.audit_report.wallets[wallet][asset]), - ) + f"{Fore.WHITE}{wallet:<30} {asset:<{self.MAX_SYMBOL_LEN}} " + f"{self.format_quantity(self.audit_report.wallets[wallet][asset]):>25}" ) def capital_gains(self, tax_year, tax_rules, summary): cgains = self.tax_report[tax_year]["CapitalGains"] - if tax_rules in config.TAX_RULES_UK_COMPANY: - print("%sChargeable Gains" % Fore.CYAN) + if tax_rules in TAX_RULES_UK_COMPANY: + print(f"{Fore.CYAN}Chargeable Gains") else: - print("%sCapital Gains" % Fore.CYAN) - - header = "%s %-10s %-28s %25s %13s %13s %13s %13s" % ( - "Asset".ljust(self.MAX_SYMBOL_LEN), - "Date", - "Disposal Type", - "Quantity", - "Cost", - "Fees", - "Proceeds", - "Gain", + print(f"{Fore.CYAN}Capital Gains") + + header = ( + f'{"Asset":<{self.MAX_SYMBOL_LEN}} {"Date":<10} {"Disposal Type":<28} ' + f'{"Quantity":>25} {"Cost":>13} {"Fees":>13} {"Proceeds":>13} {"Gain":>13}' ) for asset in sorted(cgains.assets): disposals = quantity = cost = fees = proceeds = gain = 0 - print("\n%s%s" % (Fore.YELLOW, header)) + print(f"\n{Fore.YELLOW}{header}") for te in cgains.assets[asset]: disposals += 1 quantity += te.quantity @@ -239,228 +209,155 @@ def capital_gains(self, tax_year, tax_rules, summary): proceeds += te.proceeds gain += te.gain print( - "%s%s %-10s %-28s %25s %13s %13s %13s %s%13s" - % ( - Fore.WHITE, - te.asset.ljust(self.MAX_SYMBOL_LEN), - self.format_date(te.date), - te.format_disposal(), - self.format_quantity(te.quantity), - self.format_value(te.cost), - self.format_value(te.fees), - self.format_value(te.proceeds), - Fore.RED if te.gain < 0 else Fore.WHITE, - self.format_value(te.gain), - ) + f"{Fore.WHITE}{te.asset:<{self.MAX_SYMBOL_LEN}} " + f"{self.format_date(te.date):<10} " + f"{te.format_disposal():<28} {self.format_quantity(te.quantity):>25} " + f"{self.format_value(te.cost):>13} {self.format_value(te.fees):>13} " + f"{self.format_value(te.proceeds):>13} " + f"{Fore.RED if te.gain < 0 else Fore.WHITE}{self.format_value(te.gain):>13}" ) if disposals > 1: print( - "%s%s %-10s %-28s %25s %13s %13s %13s %s%13s" - % ( - Fore.YELLOW, - "Total".ljust(self.MAX_SYMBOL_LEN), - "", - "", - self.format_quantity(quantity), - self.format_value(cost), - self.format_value(fees), - self.format_value(proceeds), - Fore.RED if gain < 0 else Fore.YELLOW, - self.format_value(gain), - ) + f'{Fore.YELLOW}{"Total":<{self.MAX_SYMBOL_LEN}} {"":<10} ' + f'{"":<28} {self.format_quantity(quantity):>25} ' + f"{self.format_value(cost):>13} {self.format_value(fees):>13} " + f"{self.format_value(proceeds):>13} " + f"{Fore.RED if gain < 0 else Fore.WHITE}{self.format_value(gain):>13}" ) - print("%s%s" % (Fore.YELLOW, "_" * len(header))) + print(f'{Fore.YELLOW}{"_" * len(header)}') print( - "%s%s %-10s %-28s %25s %13s %13s %13s %s%13s%s" - % ( - Fore.YELLOW + Style.BRIGHT, - "Total".ljust(self.MAX_SYMBOL_LEN), - "", - "", - "", - self.format_value(cgains.totals["cost"]), - self.format_value(cgains.totals["fees"]), - self.format_value(cgains.totals["proceeds"]), - Fore.RED if cgains.totals["gain"] < 0 else Fore.YELLOW, - self.format_value(cgains.totals["gain"]), - Style.NORMAL, - ) + f'{Fore.YELLOW}{Style.BRIGHT}{"Total":<{self.MAX_SYMBOL_LEN}} {"":<10} ' + f'{"":<28} {"":>25} {self.format_value(cgains.totals["cost"]):>13} ' + f'{self.format_value(cgains.totals["fees"]):>13} ' + f'{self.format_value(cgains.totals["proceeds"]):>13} ' + f'{Fore.RED if cgains.totals["gain"] < 0 else Fore.YELLOW}' + f'{self.format_value(cgains.totals["gain"]):>13}{Style.NORMAL}' ) - print("\n%sSummary\n" % Fore.CYAN) - print("%s%-40s %13d" % (Fore.WHITE, "Number of disposals:", cgains.summary["disposals"])) + print(f"\n{Fore.CYAN}Summary\n") + print(f'{Fore.WHITE}{"Number of disposals:":<40} {cgains.summary["disposals"]:>13}') if cgains.estimate["proceeds_warning"]: print( - "%s%-40s %s" - % ( - Fore.WHITE, - "Disposal proceeds:", - ("*" + self.format_value(cgains.totals["proceeds"])) - .rjust(13) - .replace("*", Fore.YELLOW + "*" + Fore.WHITE), + f'{Fore.WHITE}{"Disposal proceeds:":<40} ' + f'{"*" + self.format_value(cgains.totals["proceeds"]):>13}'.replace( + "*", f"{Fore.YELLOW}*{Fore.WHITE}" ) ) + else: print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Disposal proceeds:", - self.format_value(cgains.totals["proceeds"]), - ) + f'{Fore.WHITE}{"Disposal proceeds:":<40} ' + f'{self.format_value(cgains.totals["proceeds"]):>13}' ) print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Allowable costs (including the", - self.format_value(cgains.totals["cost"] + cgains.totals["fees"]), - ) + f'{Fore.WHITE}{"Allowable costs (including the":<40} ' + f'{self.format_value(cgains.totals["cost"] + cgains.totals["fees"]):>13}' ) - print("%spurchase price):" % Fore.WHITE) + + print(f"{Fore.WHITE}purchase price):") print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Gains in the year, before losses:", - self.format_value(cgains.summary["total_gain"]), - ) + f'{Fore.WHITE}{"Gains in the year, before losses:":<40} ' + f'{self.format_value(cgains.summary["total_gain"]):>13}' ) print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Losses in the year:", - self.format_value(abs(cgains.summary["total_loss"])), - ) + f'{Fore.WHITE}{"Losses in the year:":<40} ' + f'{self.format_value(abs(cgains.summary["total_loss"])):>13}' ) if cgains.estimate["proceeds_warning"]: print( - "%s*Assets sold are more than %s, " + f"{Fore.YELLOW}*Assets sold are more than " + f'{self.format_value(cgains.estimate["proceeds_limit"])}, ' "this needs to be reported to HMRC if you already complete a Self Assessment" - % (Fore.YELLOW, self.format_value(cgains.estimate["proceeds_limit"])) ) if not summary: - if tax_rules in config.TAX_RULES_UK_COMPANY: + if tax_rules in TAX_RULES_UK_COMPANY: self.ct_estimate(tax_year) else: self.cgt_estimate(tax_year) def cgt_estimate(self, tax_year): cgains = self.tax_report[tax_year]["CapitalGains"] - print("\n%sTax Estimate\n" % Fore.CYAN) + print(f"\n{Fore.CYAN}Tax Estimate\n") print( - "%sThe figures below are only an estimate, they do not take into consideration " - "other gains and losses in the same tax year, always consult with a professional " - "accountant before filing.\n" % Fore.CYAN + f"{Fore.CYAN}The figures below are only an estimate, " + "they do not take into consideration other gains and losses in the same tax year, " + "always consult with a professional accountant before filing.\n" ) if cgains.totals["gain"] > 0: print( - "%s%s %13s" - % ( - Fore.WHITE, - "Taxable Gain*:".ljust(40).replace("*", Fore.YELLOW + "*" + Fore.WHITE), - self.format_value(cgains.estimate["taxable_gain"]), + f'{Fore.WHITE}{"Taxable Gain*:":<40} ' + f'{self.format_value(cgains.estimate["taxable_gain"]):>13}'.replace( + "*", f"{Fore.YELLOW}*{Fore.WHITE}" ) ) else: print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Taxable Gain:", - self.format_value(cgains.estimate["taxable_gain"]), - ) + f'{Fore.WHITE}{"Taxable Gain:":<40} ' + f'{self.format_value(cgains.estimate["taxable_gain"]):>13}' ) print( - "%s%-40s %13s (%s)" - % ( - Fore.WHITE, - "Capital Gains Tax (Basic rate):", - self.format_value(cgains.estimate["cgt_basic"]), - self.format_rate(cgains.estimate["cgt_basic_rate"]), - ) + f'{Fore.WHITE}{"Capital Gains Tax (Basic rate):":<40} ' + f'{self.format_value(cgains.estimate["cgt_basic"]):>13} ' + f'({self.format_rate(cgains.estimate["cgt_basic_rate"])})' ) + print( - "%s%-40s %13s (%s)" - % ( - Fore.WHITE, - "Capital Gains Tax (Higher rate):", - self.format_value(cgains.estimate["cgt_higher"]), - self.format_rate(cgains.estimate["cgt_higher_rate"]), - ) + f'{Fore.WHITE}{"Capital Gains Tax (Higher rate):":<40} ' + f'{self.format_value(cgains.estimate["cgt_higher"]):>13} ' + f'({self.format_rate(cgains.estimate["cgt_higher_rate"])})' ) if cgains.estimate["allowance_used"]: print( - "%s*%s of the tax-free allowance (%s) used" - % ( - Fore.YELLOW, - self.format_value(cgains.estimate["allowance_used"]), - self.format_value(cgains.estimate["allowance"]), - ) + f'{Fore.YELLOW}*{self.format_value(cgains.estimate["allowance_used"])} of the ' + f'tax-free allowance ({self.format_value(cgains.estimate["allowance"])}) used' ) def ct_estimate(self, tax_year): cgains = self.tax_report[tax_year]["CapitalGains"] - print("\n%sTax Estimate\n" % Fore.CYAN) + print(f"\n{Fore.CYAN}Tax Estimate\n") print( - "%sThe figures below are only an estimate, they do not take into consideration " - "other gains and losses in the same tax year, always consult with a professional " - "accountant before filing.\n" % Fore.CYAN + f"{Fore.CYAN}The figures below are only an estimate, they do not take into " + "consideration other gains and losses in the same tax year, always consult with a " + "professional accountant before filing.\n" ) print( - "%s%-40s %13s" - % ( - Fore.WHITE, - "Taxable Gain:", - self.format_value(cgains.estimate["taxable_gain"]), - ) + f'{Fore.WHITE}{"Taxable Gain:":<40} ' + f'{self.format_value(cgains.estimate["taxable_gain"]):>13}' ) + if "ct_small" in cgains.estimate: print( - "%s%-40s %13s (%s)" - % ( - Fore.WHITE, - "Corporation Tax (Small profits rate):", - self.format_value(cgains.estimate["ct_small"]), - "/".join(map(self.format_rate, cgains.estimate["ct_small_rates"])), - ) + f'{Fore.WHITE}{"Corporation Tax (Small profits rate):":<40} ' + f'{self.format_value(cgains.estimate["ct_small"]):>13} ' + f'({"/".join(map(self.format_rate, cgains.estimate["ct_small_rates"]))})' ) print( - "%s%-40s %13s (%s)" - % ( - Fore.WHITE, - "Corporation Tax (Main rate):", - self.format_value(cgains.estimate["ct_main"]), - "/".join(map(self.format_rate, cgains.estimate["ct_main_rates"])), - ) + f'{Fore.WHITE}{"Corporation Tax (Main rate):":<40} ' + f'{self.format_value(cgains.estimate["ct_main"]):>13} ' + f'({"/".join(map(self.format_rate, cgains.estimate["ct_main_rates"]))})' ) if None in cgains.estimate["ct_small_rates"]: - print("%s* Main rate used" % Fore.YELLOW) + print(f"{Fore.YELLOW}* Main rate used") def income(self, tax_year): income = self.tax_report[tax_year]["Income"] - print("\n%sIncome\n" % Fore.CYAN) - header = "%s %-10s %-10s %-40s %-25s %13s %13s" % ( - "Asset".ljust(self.MAX_SYMBOL_LEN), - "Date", - "Type", - "Description", - "Quantity", - "Amount", - "Fees", + print(f"\n{Fore.CYAN}Income\n") + header = ( + f'{"Asset":<{self.MAX_SYMBOL_LEN}} {"Date":<10} {"Type":<10} {"Description":<40} ' + f'{"Quantity":<25} {"Amount":>13} {"Fees":>13}' ) - print("%s%s" % (Fore.YELLOW, header)) + + print(f"{Fore.YELLOW}{header}") for asset in sorted(income.assets): events = quantity = amount = fees = 0 @@ -470,88 +367,42 @@ def income(self, tax_year): amount += te.amount fees += te.fees print( - "%s%s %-10s %-10s %-40s %-25s %13s %13s" - % ( - Fore.WHITE, - te.asset.ljust(self.MAX_SYMBOL_LEN), - self.format_date(te.date), - te.type, - self.format_note(te.note), - self.format_quantity(te.quantity), - self.format_value(te.amount), - self.format_value(te.fees), - ) + f"{Fore.WHITE}{te.asset:<{self.MAX_SYMBOL_LEN}} " + f"{self.format_date(te.date):<10} {te.type:<10} " + f"{self.format_note(te.note):<40} {self.format_quantity(te.quantity):<25} " + f"{self.format_value(te.amount):>13} {self.format_value(te.fees):>13}" ) - if events > 1: print( - "%s%s %-10s %-10s %-40s %-25s %13s %13s\n" - % ( - Fore.YELLOW, - "Total".ljust(self.MAX_SYMBOL_LEN), - "", - "", - "", - self.format_quantity(quantity), - self.format_value(amount), - self.format_value(fees), - ) + f'{Fore.YELLOW}{"Total":<{self.MAX_SYMBOL_LEN}} {"":<10} ' + f'{"":<10} {"":<40} {self.format_quantity(quantity):<25} ' + f"{self.format_value(amount):>13} {self.format_value(fees):>13}\n" ) print( - "%s%s %-10s %-40s %-25s %13s %13s" - % ( - Fore.YELLOW, - "Income Type".ljust(self.MAX_SYMBOL_LEN + 11), - "", - "", - "", - "Amount", - "Fees", - ) + f'{Fore.YELLOW}{"Income Type":<{self.MAX_SYMBOL_LEN + 11}} {"":<10} ' + f'{"":<40} {"":<25} {"Amount":>13} {"Fees":>13}' ) for i_type in sorted(income.type_totals): print( - "%s%s %-10s %-40s %-25s %13s %13s" - % ( - Fore.WHITE, - i_type.ljust(self.MAX_SYMBOL_LEN + 11), - "", - "", - "", - self.format_value(income.type_totals[i_type]["amount"]), - self.format_value(income.type_totals[i_type]["fees"]), - ) + f'{Fore.WHITE}{i_type:<{self.MAX_SYMBOL_LEN + 11}} {"":<10} ' + f'{"":<40} {"":<25} {self.format_value(income.type_totals[i_type]["amount"]):>13} ' + f'{self.format_value(income.type_totals[i_type]["fees"]):>13}' ) - print("%s%s" % (Fore.YELLOW, "_" * len(header))) + print(f'{Fore.YELLOW}{"_" * len(header)}') print( - "%s%s %-10s %-40s %-25s %13s %13s%s" - % ( - Fore.YELLOW + Style.BRIGHT, - "Total".ljust(self.MAX_SYMBOL_LEN + 11), - "", - "", - "", - self.format_value(income.totals["amount"]), - self.format_value(income.totals["fees"]), - Style.NORMAL, - ) + f'{Fore.YELLOW}{Style.BRIGHT}{"Total":<{self.MAX_SYMBOL_LEN + 11}} {"":<10} ' + f'{"":<40} {"":<25} {self.format_value(income.totals["amount"]):>13} ' + f'{self.format_value(income.totals["fees"]):>13}{Style.NORMAL}' ) def price_data(self, tax_year): - print("%sPrice Data - %s\n" % (Fore.CYAN, config.format_tax_year(tax_year))) + print(f"{Fore.CYAN}Price Data - {config.format_tax_year(tax_year)}\n") print( - "%s%s %-16s %-10s %13s %25s" - % ( - Fore.YELLOW, - "Asset".ljust(self.ASSET_WIDTH + 2), - "Data Source", - "Date", - "Price (%s)" % config.ccy, - "Price (BTC)", - ) + f'{Fore.YELLOW}{"Asset":<{self.ASSET_WIDTH + 2}} {"Data Source":<16} ' + f'{"Date":<10} {"Price (" + config.ccy + ")":>13} {"Price (BTC)":>25}' ) if tax_year not in self.price_report: @@ -563,105 +414,75 @@ def price_data(self, tax_year): price_data = self.price_report[tax_year][asset][date] if price_data["price_ccy"] is not None: print( - "%s1 %s %-16s %-10s %13s %25s" - % ( - Fore.WHITE, - self.format_asset(asset, price_data["name"]).ljust(self.ASSET_WIDTH), - price_data["data_source"], - self.format_date(date), - self.format_value(price_data["price_ccy"]), - self.format_quantity(price_data["price_btc"]), - ) + f"{Fore.WHITE}" + f'1 {self.format_asset(asset, price_data["name"]):<{self.ASSET_WIDTH}} ' + f'{price_data["data_source"]:<16} {self.format_date(date):<10} ' + f'{self.format_value(price_data["price_ccy"]):>13} ' + f'{self.format_quantity(price_data["price_btc"]):>25}' ) else: price_missing_flag = True print( - "%s1 %s %-16s %-10s %s%13s %25s" - % ( - Fore.WHITE, - self.format_asset(asset, price_data["name"]).ljust(self.ASSET_WIDTH), - "", - self.format_date(date), - Fore.BLUE, - "Not available*", - "", - ) + f"{Fore.WHITE}" + f'1 {self.format_asset(asset, price_data["name"]):<{self.ASSET_WIDTH}} ' + f'{"":<16} {self.format_date(date):<10} ' + f'{Fore.BLUE}{"Not available*":>13} ' + f'{"":>25}' ) if price_missing_flag: - print("%s*Price of %s used" % (Fore.BLUE, self.format_value(0))) + print(f"{Fore.BLUE}*Price of {self.format_value(0)} used") def holdings(self): - print("%sCurrent Holdings\n" % Fore.CYAN) - header = "%s %25s %16s %16s %16s" % ( - "Asset".ljust(self.ASSET_WIDTH), - "Quantity", - "Cost + Fees", - "Value", - "Gain", + print(f"{Fore.CYAN}Current Holdings\n") + + header = ( + f'{"Asset":<{self.ASSET_WIDTH}} {"Quantity":>25} {"Cost + Fees":>16} {"Value":>16} ' + f'{"Gain":>16}' ) - print("%s%s" % (Fore.YELLOW, header)) + + print(f"{Fore.YELLOW}{header}") for h in sorted(self.holdings_report["holdings"]): holding = self.holdings_report["holdings"][h] if holding["value"] is not None: print( - "%s%s %25s %16s %16s %s%16s" - % ( - Fore.WHITE, - self.format_asset(holding["asset"], holding["name"]).ljust( - self.ASSET_WIDTH - ), - self.format_quantity(holding["quantity"]), - self.format_value(holding["cost"]), - self.format_value(holding["value"]), - Fore.RED if holding["gain"] < 0 else Fore.WHITE, - self.format_value(holding["gain"]), - ) + f"{Fore.WHITE}" + f'{self.format_asset(holding["asset"], holding["name"]):<{self.ASSET_WIDTH}} ' + f'{self.format_quantity(holding["quantity"]):>25} ' + f'{self.format_value(holding["cost"]):>16} ' + f'{self.format_value(holding["value"]):>16} ' + f'{Fore.RED if holding["gain"] < 0 else Fore.WHITE}' + f'{self.format_value(holding["gain"]):>16}' ) else: print( - "%s%s %25s %16s %s%16s %16s" - % ( - Fore.WHITE, - self.format_asset(holding["asset"], holding["name"]).ljust( - self.ASSET_WIDTH - ), - self.format_quantity(holding["quantity"]), - self.format_value(holding["cost"]), - Fore.BLUE, - "Not available", - "", - ) + f"{Fore.WHITE}" + f'{self.format_asset(holding["asset"], holding["name"]):<{self.ASSET_WIDTH}} ' + f'{self.format_quantity(holding["quantity"]):>25} ' + f'{self.format_value(holding["cost"]):>16} ' + f'{Fore.BLUE}{"Not available":>16} ' + f'{"":>16}' ) - print("%s%s" % (Fore.YELLOW, "_" * len(header))) + print(f'{Fore.YELLOW}{"_" * len(header)}') print( - "%s%s %25s %16s %16s %s%16s" - % ( - Fore.YELLOW + Style.BRIGHT, - "Total".ljust(self.ASSET_WIDTH), - "", - self.format_value(self.holdings_report["totals"]["cost"]), - self.format_value(self.holdings_report["totals"]["value"]), - Fore.RED if self.holdings_report["totals"]["gain"] < 0 else Fore.YELLOW, - self.format_value(self.holdings_report["totals"]["gain"]), - ) + f"{Fore.YELLOW}{Style.BRIGHT}" + f'{"Total":<{self.ASSET_WIDTH}} {"":>25} ' + f'{self.format_value(self.holdings_report["totals"]["cost"]):>16} ' + f'{self.format_value(self.holdings_report["totals"]["value"]):>16} ' + f'{Fore.RED if self.holdings_report["totals"]["gain"] < 0 else Fore.YELLOW}' + f'{self.format_value(self.holdings_report["totals"]["gain"]):>16}' ) @staticmethod def format_date(date): if isinstance(date, datetime): - return date.strftime("%d/%m/%Y") - return dateutil.parser.parse(date).strftime("%d/%m/%Y") + return f"{date:%d/%m/%Y}" + return f"{dateutil.parser.parse(date):%d/%m/%Y}" @staticmethod def format_date2(date): - return "{} {}{} {}".format( - date.strftime("%b"), - date.day, - ReportLog.format_day(date.day), - date.strftime("%Y"), - ) + return f"{date:%b} {date.day}{ReportLog.format_day(date.day)} {date:%Y}" @staticmethod def format_day(day): @@ -670,24 +491,24 @@ def format_day(day): @staticmethod def format_quantity(quantity): if quantity is not None: - return "{:0,f}".format(quantity.normalize()) + return f"{quantity.normalize():0,f}" return "n/a" @staticmethod def format_value(value): - return config.sym() + "{:0,.2f}".format(value + 0) + return f"{config.sym()}{value + 0:0,.2f}" @staticmethod def format_asset(asset, name): if name is not None: - return "%s (%s)" % (asset, name) + return f"{asset} ({name})" return asset @staticmethod def format_rate(rate): if rate is None: - return Fore.YELLOW + "*" + Fore.WHITE - return "{}%".format(rate) + return f"{Fore.YELLOW}*{Fore.WHITE}" + return f"{rate}%" @staticmethod def format_note(note): @@ -698,7 +519,7 @@ def format_note(note): ) -class ProgressSpinner(object): +class ProgressSpinner: def __init__(self): self.spinner = itertools.cycle(["-", "\\", "|", "/"]) self.busy = False @@ -714,7 +535,7 @@ def do_spinner(self): def __enter__(self): if sys.stdout.isatty(): self.busy = True - sys.stdout.write("%sgenerating PDF report%s: " % (Fore.CYAN, Fore.GREEN)) + sys.stdout.write(f"{Fore.CYAN}generating PDF report{Fore.GREEN}: ") threading.Thread(target=self.do_spinner).start() def __exit__(self, exc_type, exc_val, exc_traceback): diff --git a/src/bittytax/tax.py b/src/bittytax/tax.py index ea0ffe6b..0bb8bde6 100644 --- a/src/bittytax/tax.py +++ b/src/bittytax/tax.py @@ -11,13 +11,14 @@ from tqdm import tqdm from .config import config +from .constants import TAX_RULES_UK_COMPANY from .holdings import Holdings from .transactions import Buy, Sell PRECISION = Decimal("0.00") -class TaxCalculator(object): # pylint: disable=too-many-instance-attributes +class TaxCalculator: # pylint: disable=too-many-instance-attributes DISPOSAL_SAME_DAY = "Same Day" DISPOSAL_TEN_DAY = "Ten Day" DISPOSAL_BED_AND_BREAKFAST = "Bed & Breakfast" @@ -58,12 +59,12 @@ def pool_same_day(self): sell_transactions = {} if config.debug: - print("%spool same day transactions" % Fore.CYAN) + print(f"{Fore.CYAN}pool same day transactions") for t in tqdm( transactions, unit="t", - desc="%spool same day%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}pool same day{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if ( @@ -95,12 +96,12 @@ def pool_same_day(self): if config.debug: for t in sorted(self.all_transactions()): if len(t.pooled) > 1: - print("%spool: %s" % (Fore.GREEN, t.__str__(pooled_bold=True))) + print(f"{Fore.GREEN}pool: {t.__str__(pooled_bold=True)}") for tp in t.pooled: - print("%spool: (%s)" % (Fore.BLUE, tp)) + print(f"{Fore.BLUE}pool: ({tp})") if config.debug: - print("%spool: total transactions=%d" % (Fore.CYAN, len(self.all_transactions()))) + print(f"{Fore.CYAN}pool: total transactions={len(self.all_transactions())}") def match_buyback(self, rule): sell_index = buy_index = 0 @@ -109,12 +110,12 @@ def match_buyback(self, rule): return if config.debug: - print("%smatch %s transactions" % (Fore.CYAN, rule.lower())) + print(f"{Fore.CYAN}match {rule.lower()} transactions") pbar = tqdm( total=len(self.sells_ordered), unit="t", - desc="%smatch %s transactions%s" % (Fore.CYAN, rule.lower(), Fore.GREEN), + desc=f"{Fore.CYAN}match {rule.lower()} transactions{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ) @@ -130,27 +131,27 @@ def match_buyback(self, rule): ): if config.debug: if b.quantity > s.quantity: - print("%smatch: %s" % (Fore.GREEN, s.__str__(quantity_bold=True))) - print("%smatch: %s" % (Fore.GREEN, b)) + print(f"{Fore.GREEN}match: {s.__str__(quantity_bold=True)}") + print(f"{Fore.GREEN}match: {b}") elif s.quantity > b.quantity: - print("%smatch: %s" % (Fore.GREEN, s)) - print("%smatch: %s" % (Fore.GREEN, b.__str__(quantity_bold=True))) + print(f"{Fore.GREEN}match: {s}") + print(f"{Fore.GREEN}match: {b.__str__(quantity_bold=True)}") else: - print("%smatch: %s" % (Fore.GREEN, s.__str__(quantity_bold=True))) - print("%smatch: %s" % (Fore.GREEN, b.__str__(quantity_bold=True))) + print(f"{Fore.GREEN}match: {s.__str__(quantity_bold=True)}") + print(f"{Fore.GREEN}match: {b.__str__(quantity_bold=True)}") if b.quantity > s.quantity: b_remainder = b.split_buy(s.quantity) self.buys_ordered.insert(buy_index + 1, b_remainder) if config.debug: - print("%smatch: split: %s" % (Fore.YELLOW, b.__str__(quantity_bold=True))) - print("%smatch: split: %s" % (Fore.YELLOW, b_remainder)) + print(f"{Fore.YELLOW}match: split: {b.__str__(quantity_bold=True)}") + print(f"{Fore.YELLOW}match: split: {b_remainder}") elif s.quantity > b.quantity: s_remainder = s.split_sell(b.quantity) self.sells_ordered.insert(sell_index + 1, s_remainder) if config.debug: - print("%smatch: split: %s" % (Fore.YELLOW, s.__str__(quantity_bold=True))) - print("%smatch: split: %s" % (Fore.YELLOW, s_remainder)) + print(f"{Fore.YELLOW}match: split: {s.__str__(quantity_bold=True)}") + print(f"{Fore.YELLOW}match: split: {s_remainder}") pbar.total += 1 s.matched = b.matched = True @@ -163,7 +164,7 @@ def match_buyback(self, rule): ) self.tax_events[self.which_tax_year(tax_event.date)].append(tax_event) if config.debug: - print("%smatch: %s" % (Fore.CYAN, tax_event)) + print(f"{Fore.CYAN}match: {tax_event}") # Find next sell sell_index += 1 @@ -179,7 +180,7 @@ def match_buyback(self, rule): pbar.close() if config.debug: - print("%smatch: total transactions=%d" % (Fore.CYAN, len(self.all_transactions()))) + print(f"{Fore.CYAN}match: total transactions={len(self.all_transactions())}") def match_sell(self, rule): buy_index = sell_index = 0 @@ -188,12 +189,12 @@ def match_sell(self, rule): return if config.debug: - print("%smatch %s transactions" % (Fore.CYAN, rule.lower())) + print(f"{Fore.CYAN}match {rule.lower()} transactions") pbar = tqdm( total=len(self.buys_ordered), unit="t", - desc="%smatch %s transactions%s" % (Fore.CYAN, rule.lower(), Fore.GREEN), + desc=f"{Fore.CYAN}match {rule.lower()} transactions{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ) @@ -209,28 +210,28 @@ def match_sell(self, rule): ): if config.debug: if b.quantity > s.quantity: - print("%smatch: %s" % (Fore.GREEN, b)) - print("%smatch: %s" % (Fore.GREEN, s.__str__(quantity_bold=True))) + print(f"{Fore.GREEN}match: {b}") + print(f"{Fore.GREEN}match: {s.__str__(quantity_bold=True)}") elif s.quantity > b.quantity: - print("%smatch: %s" % (Fore.GREEN, b.__str__(quantity_bold=True))) - print("%smatch: %s" % (Fore.GREEN, s)) + print(f"{Fore.GREEN}match: {b.__str__(quantity_bold=True)}") + print(f"{Fore.GREEN}match: {s}") else: - print("%smatch: %s" % (Fore.GREEN, b.__str__(quantity_bold=True))) - print("%smatch: %s" % (Fore.GREEN, s.__str__(quantity_bold=True))) + print(f"{Fore.GREEN}match: {b.__str__(quantity_bold=True)}") + print(f"{Fore.GREEN}match: {s.__str__(quantity_bold=True)}") if b.quantity > s.quantity: b_remainder = b.split_buy(s.quantity) self.buys_ordered.insert(buy_index + 1, b_remainder) if config.debug: - print("%smatch: split: %s" % (Fore.YELLOW, b.__str__(quantity_bold=True))) - print("%smatch: split: %s" % (Fore.YELLOW, b_remainder)) + print(f"{Fore.YELLOW}match: split: {b.__str__(quantity_bold=True)}") + print(f"{Fore.YELLOW}match: split: {b_remainder}") pbar.total += 1 elif s.quantity > b.quantity: s_remainder = s.split_sell(b.quantity) self.sells_ordered.insert(sell_index + 1, s_remainder) if config.debug: - print("%smatch: split: %s" % (Fore.YELLOW, s.__str__(quantity_bold=True))) - print("%smatch: split: %s" % (Fore.YELLOW, s_remainder)) + print(f"{Fore.YELLOW}match: split: {s.__str__(quantity_bold=True)}") + print(f"{Fore.YELLOW}match: split: {s_remainder}") b.matched = s.matched = True tax_event = TaxEventCapitalGains( @@ -242,7 +243,7 @@ def match_sell(self, rule): ) self.tax_events[self.which_tax_year(tax_event.date)].append(tax_event) if config.debug: - print("%smatch: %s" % (Fore.CYAN, tax_event)) + print(f"{Fore.CYAN}match: {tax_event}") # Find next buy buy_index += 1 @@ -258,7 +259,7 @@ def match_sell(self, rule): pbar.close() if config.debug: - print("%smatch: total transactions=%d" % (Fore.CYAN, len(self.all_transactions()))) + print(f"{Fore.CYAN}match: total transactions={len(self.all_transactions())}") def _rule_match(self, b_timestamp, s_timestamp, rule): if rule == self.DISPOSAL_SAME_DAY: @@ -282,12 +283,12 @@ def _rule_match(self, b_timestamp, s_timestamp, rule): def process_section104(self, skip_integrity_check): if config.debug: - print("%sprocess section 104" % Fore.CYAN) + print(f"{Fore.CYAN}process section 104") for t in tqdm( sorted(self.all_transactions()), unit="t", - desc="%sprocess section 104%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}process section 104{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if t.is_crypto() and t.asset not in self.holdings: @@ -295,21 +296,21 @@ def process_section104(self, skip_integrity_check): if t.matched: if config.debug: - print("%ssection104: //%s <- matched" % (Fore.BLUE, t)) + print(f"{Fore.BLUE}section104: //{t} <- matched") continue if not config.transfers_include and t.t_type in self.TRANSFER_TYPES: if config.debug: - print("%ssection104: //%s <- transfer" % (Fore.BLUE, t)) + print(f"{Fore.BLUE}section104: //{t} <- transfer") continue if not t.is_crypto(): if config.debug: - print("%ssection104: //%s <- fiat" % (Fore.BLUE, t)) + print(f"{Fore.BLUE}section104: //{t} <- fiat") continue if config.debug: - print("%ssection104: %s" % (Fore.GREEN, t)) + print(f"{Fore.GREEN}section104: {t}") if isinstance(t, Buy): self._add_tokens(t) @@ -365,19 +366,19 @@ def _subtract_tokens(self, t, skip_integrity_check): self.tax_events[self.which_tax_year(tax_event.date)].append(tax_event) if config.debug: - print("%ssection104: %s" % (Fore.CYAN, tax_event)) + print(f"{Fore.CYAN}section104: {tax_event}") if config.transfers_include and not skip_integrity_check: self.holdings[t.asset].check_transfer_mismatch() def process_income(self): if config.debug: - print("%sprocess income" % Fore.CYAN) + print(f"{Fore.CYAN}process income") for t in tqdm( self.transactions, unit="t", - desc="%sprocess income%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}process income{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if t.t_type in self.INCOME_TYPES and (t.is_crypto() or config.fiat_income): @@ -399,7 +400,7 @@ def calculate_capital_gains(self, tax_year): if isinstance(te, TaxEventCapitalGains): self.tax_report[tax_year]["CapitalGains"].tax_summary(te) - if self.tax_rules in config.TAX_RULES_UK_COMPANY: + if self.tax_rules in TAX_RULES_UK_COMPANY: self.tax_report[tax_year]["CapitalGains"].tax_estimate_ct(tax_year) else: self.tax_report[tax_year]["CapitalGains"].tax_estimate_cgt(tax_year) @@ -419,12 +420,12 @@ def calculate_holdings(self, value_asset): totals = {"cost": Decimal(0), "value": Decimal(0), "gain": Decimal(0)} if config.debug: - print("%scalculating holdings" % Fore.CYAN) + print(f"{Fore.CYAN}calculating holdings") for h in tqdm( self.holdings, unit="h", - desc="%scalculating holdings%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}calculating holdings{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if self.holdings[h].quantity > 0 or config.show_empty_wallets: @@ -464,7 +465,7 @@ def which_tax_year(self, timestamp): return tax_year -class TaxEvent(object): +class TaxEvent: def __init__(self, date, asset): self.date = date self.asset = asset @@ -481,7 +482,7 @@ def __lt__(self, other): class TaxEventCapitalGains(TaxEvent): def __init__(self, disposal_type, b, s, cost, fees): - super(TaxEventCapitalGains, self).__init__(s.timestamp, s.asset) + super().__init__(s.timestamp, s.asset) self.disposal_type = disposal_type self.quantity = s.quantity self.cost = cost.quantize(PRECISION) @@ -495,26 +496,23 @@ def format_disposal(self): TaxCalculator.DISPOSAL_BED_AND_BREAKFAST, TaxCalculator.DISPOSAL_TEN_DAY, ): - return "%s (%s)" % ( - self.disposal_type, - self.acquisition_date.strftime("%d/%m/%Y"), - ) + return f"{self.disposal_type} ({self.acquisition_date:%d/%m/%Y})" return self.disposal_type def __str__(self): - return "Disposal(%s) gain=%s (proceeds=%s - cost=%s - fees=%s)" % ( - self.disposal_type.lower(), - config.sym() + "{:0,.2f}".format(self.gain), - config.sym() + "{:0,.2f}".format(self.proceeds), - config.sym() + "{:0,.2f}".format(self.cost), - config.sym() + "{:0,.2f}".format(self.fees), + return ( + f"Disposal({self.disposal_type.lower()}) gain=" + f"{config.sym()}{self.gain:0,.2f} " + f"(proceeds={config.sym()}{self.proceeds:0,.2f} - cost=" + f"{config.sym()}{self.cost:0,.2f} - fees=" + f"{config.sym()}{self.fees:0,.2f})" ) class TaxEventIncome(TaxEvent): # pylint: disable=too-few-public-methods def __init__(self, b): - super(TaxEventIncome, self).__init__(b.timestamp, b.asset) + super().__init__(b.timestamp, b.asset) self.type = b.t_type self.quantity = b.quantity self.amount = b.cost.quantize(PRECISION) @@ -525,7 +523,7 @@ def __init__(self, b): self.fees = Decimal(0) -class CalculateCapitalGains(object): +class CalculateCapitalGains: # Rate changes start from 6th April in previous year, i.e. 2022 is for tax year 2021/22 CG_DATA_INDIVIDUAL = { 2009: {"allowance": 9600, "basic_rate": 18, "higher_rate": 18}, @@ -590,7 +588,7 @@ def __init__(self, tax_year, tax_rules): "total_loss": Decimal(0), } - if tax_rules in config.TAX_RULES_UK_COMPANY: + if tax_rules in TAX_RULES_UK_COMPANY: self.estimate = { "proceeds_warning": False, "ct_small_rates": [], @@ -705,7 +703,7 @@ def tax_estimate_ct(self, tax_year): self.estimate["ct_small_rates"] = [] -class CalculateIncome(object): +class CalculateIncome: def __init__(self): self.totals = {"amount": Decimal(0), "fees": Decimal(0)} self.assets = {} @@ -727,8 +725,8 @@ def totalise(self, te): self.types[te.type].append(te) def totals_by_type(self): - for income_type in self.types: - for te in self.types[income_type]: + for income_type, te_list in self.types.items(): + for te in te_list: if income_type not in self.type_totals: self.type_totals[income_type] = {} self.type_totals[income_type]["amount"] = te.amount diff --git a/src/bittytax/templates/capital_gains.html b/src/bittytax/templates/capital_gains.html index 8f8a1d19..7c79bbb2 100644 --- a/src/bittytax/templates/capital_gains.html +++ b/src/bittytax/templates/capital_gains.html @@ -1,4 +1,4 @@ -{% if args.tax_rules in config.TAX_RULES_UK_COMPANY %} +{% if args.tax_rules in TAX_RULES_UK_COMPANY %}

Chargeable Gains

{% else %}

Capital Gains

@@ -109,7 +109,7 @@

Summary

*Assets sold are more than {{(cgains.estimate['proceeds_limit'])|valuefilter}}, this needs to be reported to HMRC if you already complete a Self Assessment

{% endif %} {% if not args.summary %} - {% if args.tax_rules in config.TAX_RULES_UK_COMPANY %} + {% if args.tax_rules in TAX_RULES_UK_COMPANY %} {% include "ct_estimate.html" %} {% else %} {% include "cgt_estimate.html" %} diff --git a/src/bittytax/transactions.py b/src/bittytax/transactions.py index 5e132fff..bd429fea 100644 --- a/src/bittytax/transactions.py +++ b/src/bittytax/transactions.py @@ -11,22 +11,22 @@ from .record import TransactionRecord -class TransactionHistory(object): +class TransactionHistory: def __init__(self, transaction_records, value_asset): self.value_asset = value_asset self.transactions = [] if config.debug: - print("%ssplit transaction records" % Fore.CYAN) + print(f"{Fore.CYAN}split transaction records") for tr in tqdm( transaction_records, unit="tr", - desc="%ssplit transaction records%s" % (Fore.CYAN, Fore.GREEN), + desc=f"{Fore.CYAN}split transaction records{Fore.GREEN}", disable=bool(config.debug or not sys.stdout.isatty()), ): if config.debug: - print("%ssplit: TR %s" % (Fore.MAGENTA, tr)) + print(f"{Fore.MAGENTA}split: TR {tr}") self.get_all_values(tr) @@ -70,35 +70,35 @@ def __init__(self, transaction_records, value_asset): tr.buy.set_tid() self.transactions.append(tr.buy) if config.debug: - print("%ssplit: %s" % (Fore.GREEN, tr.buy)) + print(f"{Fore.GREEN}split: {tr.buy}") if tr.sell and (tr.sell.quantity or tr.sell.fee_value): tr.sell.set_tid() self.transactions.append(tr.sell) if config.debug: - print("%ssplit: %s" % (Fore.GREEN, tr.sell)) + print(f"{Fore.GREEN}split: {tr.sell}") else: # Special case for LOST sell must be before buy-back if tr.sell and (tr.sell.quantity or tr.sell.fee_value): tr.sell.set_tid() self.transactions.append(tr.sell) if config.debug: - print("%ssplit: %s" % (Fore.GREEN, tr.sell)) + print(f"{Fore.GREEN}split: {tr.sell}") if tr.buy and (tr.buy.quantity or tr.buy.fee_value): tr.buy.set_tid() self.transactions.append(tr.buy) if config.debug: - print("%ssplit: %s" % (Fore.GREEN, tr.buy)) + print(f"{Fore.GREEN}split: {tr.buy}") if tr.fee and tr.fee.quantity: tr.fee.set_tid() self.transactions.append(tr.fee) if config.debug: - print("%ssplit: %s" % (Fore.GREEN, tr.fee)) + print(f"{Fore.GREEN}split: {tr.fee}") if config.debug: - print("%ssplit: total transactions=%d" % (Fore.CYAN, len(self.transactions))) + print(f"{Fore.CYAN}split: total transactions={len(self.transactions)}") def get_all_values(self, tr): if tr.buy and tr.buy.acquisition and tr.buy.cost is None: @@ -196,7 +196,9 @@ def which_asset_value(self, tr): return value, fixed -class TransactionBase(object): # pylint: disable=too-many-instance-attributes +class TransactionBase: # pylint: disable=too-many-instance-attributes + POOLED = "" + def __init__(self, t_type, asset, quantity): self.tid = None self.t_record = None @@ -211,60 +213,46 @@ def __init__(self, t_type, asset, quantity): self.matched = False self.pooled = [] + def name(self): + return self.__class__.__name__ + def set_tid(self): self.tid = self.t_record.set_tid() def is_crypto(self): return bool(self.asset not in config.fiat_list) - def _format_tid(self): - return "%s.%s" % (self.tid[0], self.tid[1]) - def _format_quantity(self): if self.quantity is None: return "" - return "{:0,f}".format(self.quantity.normalize()) - - def _format_asset(self): - if sys.version_info[0] < 3: - return self.asset.decode("utf8") - return self.asset - - def _format_wallet(self): - if sys.version_info[0] < 3: - return self.wallet.decode("utf8") - return self.wallet + return f"{self.quantity.normalize():0,f}" def _format_note(self): if self.note: - if sys.version_info[0] < 3: - return "'%s' " % self.note.decode("utf8") - return "'%s' " % self.note + return f"'{self.note}' " return "" def _format_pooled(self, bold=False): if self.pooled: - return " %s(%s)%s" % ( - Style.BRIGHT if bold else "", - len(self.pooled), - Style.NORMAL if bold else "", + return ( + f" {Style.BRIGHT if bold else ''}({len(self.pooled)})" + f"{Style.NORMAL if bold else ''}" ) return "" def _format_fee(self): if self.fee_value is not None: - return " + fee=%s%s %s" % ( - "" if self.fee_fixed else "~", - config.sym() + "{:0,.2f}".format(self.fee_value), - config.ccy, + return ( + f" + fee={'' if self.fee_fixed else '~'}" + f"{config.sym()}{self.fee_value:0,.2f} {config.ccy}" ) return "" def _format_timestamp(self): if self.timestamp.microsecond: - return self.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f %Z") - return self.timestamp.strftime("%Y-%m-%dT%H:%M:%S %Z") + return f"{self.timestamp:%Y-%m-%dT%H:%M:%S.%f %Z}" + return f"{self.timestamp:%Y-%m-%dT%H:%M:%S %Z}" def __eq__(self, other): return (self.asset, self.timestamp) == (other.asset, other.timestamp) @@ -311,7 +299,7 @@ class Buy(TransactionBase): # pylint: disable=too-many-instance-attributes } def __init__(self, t_type, buy_quantity, buy_asset, buy_value): - super(Buy, self).__init__(t_type, buy_asset, buy_quantity) + super().__init__(t_type, buy_asset, buy_quantity) self.acquisition = bool(self.t_type in self.ACQUISITION_TYPES) self.cost = None self.cost_fixed = False @@ -340,7 +328,7 @@ def __iadd__(self, other): self.timestamp = other.timestamp if other.wallet != self.wallet: - self.wallet = "" + self.wallet = self.POOLED if other.cost_fixed != self.cost_fixed: self.cost_fixed = False @@ -349,7 +337,7 @@ def __iadd__(self, other): self.fee_fixed = False if other.note != self.note: - self.note = "" + self.note = self.POOLED self.pooled.append(other) return self @@ -378,29 +366,27 @@ def split_buy(self, sell_quantity): def _format_cost(self): if self.cost is not None: - return " (%s%s %s)" % ( - "=" if self.cost_fixed else "~", - config.sym() + "{:0,.2f}".format(self.cost), - config.ccy, + return ( + f" ({'=' if self.cost_fixed else '~'}" + f"{config.sym()}{self.cost:0,.2f} {config.ccy})" ) return "" def __str__(self, pooled_bold=False, quantity_bold=False): - return "%s%s %s%s %s %s%s%s%s '%s' %s %s[TID:%s]%s" % ( - type(self).__name__.upper(), - "*" if not self.acquisition else "", - self.t_type, - Style.BRIGHT if quantity_bold else "", - self._format_quantity(), - self._format_asset(), - Style.NORMAL if quantity_bold else "", - self._format_cost(), - self._format_fee(), - self._format_wallet(), - self._format_timestamp(), - self._format_note(), - self._format_tid(), - self._format_pooled(pooled_bold), + return ( + f"{self.name().upper()}{'*' if not self.acquisition else ''} " + f"{self.t_type}" + f"{Style.BRIGHT if quantity_bold else ''} " + f"{self._format_quantity()} " + f"{self.asset}" + f"{Style.NORMAL if quantity_bold else ''}" + f"{self._format_cost()}" + f"{self._format_fee()} " + f"'{self.wallet}' " + f"{self._format_timestamp()} " + f"{self._format_note()}" + f"[TID:{self.tid[0]}.{self.tid[1]}]" + f"{self._format_pooled()}" ) @@ -423,7 +409,7 @@ class Sell(TransactionBase): # pylint: disable=too-many-instance-attributes } def __init__(self, t_type, sell_quantity, sell_asset, sell_value): - super(Sell, self).__init__(t_type, sell_asset, sell_quantity) + super().__init__(t_type, sell_asset, sell_quantity) self.disposal = bool(self.t_type in self.DISPOSAL_TYPES) self.proceeds = None self.proceeds_fixed = False @@ -452,7 +438,7 @@ def __iadd__(self, other): self.timestamp = other.timestamp if other.wallet != self.wallet: - self.wallet = "" + self.wallet = self.POOLED if other.proceeds_fixed != self.proceeds_fixed: self.proceeds_fixed = False @@ -461,7 +447,7 @@ def __iadd__(self, other): self.fee_fixed = False if other.note != self.note: - self.note = "" + self.note = self.POOLED self.pooled.append(other) return self @@ -490,27 +476,25 @@ def split_sell(self, buy_quantity): def _format_proceeds(self): if self.proceeds is not None: - return " (%s%s %s)" % ( - "=" if self.proceeds_fixed else "~", - config.sym() + "{:0,.2f}".format(self.proceeds), - config.ccy, + return ( + f" ({'=' if self.proceeds_fixed else '~'}" + f"{config.sym()}{self.proceeds:0,.2f} {config.ccy})" ) return "" def __str__(self, pooled_bold=False, quantity_bold=False): - return "%s%s %s%s %s %s%s%s%s '%s' %s %s[TID:%s]%s" % ( - type(self).__name__.upper(), - "*" if not self.disposal else "", - self.t_type, - Style.BRIGHT if quantity_bold else "", - self._format_quantity(), - self._format_asset(), - Style.NORMAL if quantity_bold else "", - self._format_proceeds(), - self._format_fee(), - self._format_wallet(), - self._format_timestamp(), - self._format_note(), - self._format_tid(), - self._format_pooled(pooled_bold), + return ( + f"{self.name().upper()}{'*' if not self.disposal else ''} " + f"{self.t_type}" + f"{Style.BRIGHT if quantity_bold else ''} " + f"{self._format_quantity()} " + f"{self.asset}" + f"{Style.NORMAL if quantity_bold else ''}" + f"{self._format_proceeds()}" + f"{self._format_fee()} " + f"'{self.wallet}' " + f"{self._format_timestamp()} " + f"{self._format_note()}" + f"[TID:{self.tid[0]}.{self.tid[1]}]" + f"{self._format_pooled()}" )