Skip to content

Commit

Permalink
Add typing annotations for Mypy and Pytype checks
Browse files Browse the repository at this point in the history
* Added new base class `Broker` as abstract base class
* Added custom decorator to provide `typing.override` for Python versions before 3.12
  • Loading branch information
mbrukman committed Aug 19, 2024
1 parent 98c74b8 commit d36877f
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 108 deletions.
38 changes: 38 additions & 0 deletions broker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Optional

import utils


class Broker(ABC):

@classmethod
@abstractmethod
def name(cls) -> str:
...

@classmethod
@abstractmethod
def isFileForBroker(cls, file: str) -> bool:
...

@classmethod
@abstractmethod
def parseFileToTxnList(cls, file: str, tax_year: Optional[int]) -> list[utils.Transaction]:
...
18 changes: 11 additions & 7 deletions brokers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,25 @@ def isFileForBroker(cls, filename):
3) Add your class to the BROKERS map below.
"""

from __future__ import annotations

from typing import Optional, Type

from broker import Broker
from interactive_brokers import InteractiveBrokers
from tdameritrade import TDAmeritrade
from vanguard import Vanguard


BROKERS = {
BROKERS: dict[str, Type[Broker]] = {
'amtd': TDAmeritrade,
'ib': InteractiveBrokers,
'tdameritrade': TDAmeritrade,
'vanguard': Vanguard,
}


def DetectBroker(filename):
def DetectBroker(filename: str) -> Optional[Type[Broker]]:
for (broker_name, broker) in BROKERS.items():
if hasattr(broker, 'isFileForBroker'):
if broker.isFileForBroker(filename):
Expand All @@ -50,12 +55,11 @@ def DetectBroker(filename):
return None


def GetBroker(broker_name, filename):
if not broker_name or broker_name not in BROKERS:
broker = DetectBroker(filename)
else:
broker = BROKERS[broker_name]
def GetBroker(broker_name: str, filename: str) -> Type[Broker]:
if broker_name in BROKERS:
return BROKERS[broker_name]

broker: Optional[Type[Broker]] = DetectBroker(filename)
if not broker:
raise Exception('Invalid broker name: %s' % broker_name)

Expand Down
28 changes: 18 additions & 10 deletions csv2txf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,23 @@
* TXF standard: http://turbotax.intuit.com/txf/
"""

from __future__ import annotations

from decimal import Decimal
from datetime import datetime
import sys
from utils import txfDate
from typing import List

from brokers import GetBroker
import utils


def ConvertTxnListToTxf(txn_list, tax_year, date):
def ConvertTxnListToTxf(txn_list: list[utils.Transaction], tax_year: int, date: str) -> List[str]:
lines = []
lines.append('V042') # Version
lines.append('Acsv2txf') # Program name/version
if date is None:
date = txfDate(datetime.today())
date = utils.txfDate(datetime.today())
lines.append('D%s' % date) # Export date
lines.append('^')
for txn in txn_list:
Expand All @@ -54,19 +58,21 @@ def ConvertTxnListToTxf(txn_list, tax_year, date):
return lines


def RunConverter(broker_name, filename, tax_year, date):
def RunConverter(broker_name: str, filename: str, tax_year: int, date: str) -> List[str]:
broker = GetBroker(broker_name, filename)
txn_list = broker.parseFileToTxnList(filename, tax_year)
return ConvertTxnListToTxf(txn_list, tax_year, date)


def GetSummary(broker_name, filename, tax_year):
def GetSummary(broker_name: str, filename: str, tax_year: int) -> str:
broker = GetBroker(broker_name, filename)
total_cost = Decimal(0)
total_sales = Decimal(0)
txn_list = broker.parseFileToTxnList(filename, tax_year)
for txn in txn_list:
assert txn.costBasis is not None
total_cost += txn.costBasis
assert txn.saleProceeds is not None
total_sales += txn.saleProceeds

return '\n'.join([
Expand Down Expand Up @@ -94,15 +100,17 @@ def main(argv):
sys.stderr.write('Filename is required; specify with `--file` flag.\n')
sys.exit(1)

if not options.year:
options.year = datetime.today().year - 1
if options.year:
year = int(options.year)
else:
year = datetime.today().year - 1
utils.Warning(f'Year not specified, defaulting to {year} (last year)\n')

output = None
if options.out_format == 'summary':
output = GetSummary(options.broker, options.filename, options.year)
output = GetSummary(options.broker, options.filename, year)
else:
txf_lines = RunConverter(options.broker, options.filename, options.year,
options.date)
txf_lines = RunConverter(options.broker, options.filename, year, options.date)
output = '\n'.join(txf_lines)

if options.out_filename:
Expand Down
33 changes: 33 additions & 0 deletions decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import sys


if (sys.version_info.major, sys.version_info.minor) >= (3, 12):
# This was added in Python 3.12:
#
# * https://docs.python.org/3/library/typing.html#typing.override
# * https://peps.python.org/pep-0698/
from typing import override
else:
from typing import TypeVar

_F = TypeVar('_F')

def override(func: _F) -> _F:
"""No-op @override for Python versions prior to 3.12."""
return func
32 changes: 21 additions & 11 deletions interactive_brokers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@
* dividends
"""

from __future__ import annotations

import csv
from datetime import datetime
from decimal import Decimal
from typing import Optional

from broker import Broker
from decorators import override
import utils


FIRST_LINE = 'Title,Worksheet for Form 8949,'


class InteractiveBrokers:
class InteractiveBrokers(Broker):

@classmethod
def name(cls):
@override
def name(cls) -> str:
return 'Interactive Brokers'

@classmethod
def DetermineEntryCode(cls, part, box):
def DetermineEntryCode(cls, part: int, box: str) -> Optional[int]:
if part == 1:
if box == 'A':
return 321
Expand All @@ -51,34 +59,36 @@ def DetermineEntryCode(cls, part, box):
return None

@classmethod
def TryParseYear(cls, date_str):
def TryParseYear(cls, date_str: str) -> Optional[int]:
try:
return datetime.strptime(date_str, '%m/%d/%Y').year
except ValueError:
return None

@classmethod
def ParseDollarValue(cls, value):
def ParseDollarValue(cls, value: str) -> Decimal:
return Decimal(value.replace(',', '').replace('"', ''))

@classmethod
def isFileForBroker(cls, filename):
@override
def isFileForBroker(cls, filename: str) -> bool:
with open(filename) as f:
first_line = f.readline()
return first_line.find(FIRST_LINE) == 0

@classmethod
def parseFileToTxnList(cls, filename, tax_year):
@override
def parseFileToTxnList(cls, filename: str, tax_year: Optional[int]) -> list[utils.Transaction]:
with open(filename) as f:
# First 2 lines are headers.
f.readline()
f.readline()
txns = csv.reader(f, delimiter=',', quotechar='"')

txn_list = []
part = None
box = None
entry_code = None
txn_list: list[utils.Transaction] = []
part: Optional[int] = None
box: Optional[str] = None
entry_code: Optional[int] = None

for row in txns:
if row[0] == 'Part' and len(row) == 3:
Expand Down
Loading

0 comments on commit d36877f

Please sign in to comment.