Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add InvestStatmentLine as possible return from parse_record to fix typing check errors. #263

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d75b085
Add InvestStatmentLine as possible reutrn to fix typing check errors.
jaik03 Jan 14, 2024
498485b
Return pkg_resources namespaces back
kedder Jan 17, 2024
f7af059
Merge pull request #264 from kedder/pkg-resources-ns
kedder Jan 21, 2024
1e2abea
Bump pytest from 7.4.4 to 8.0.0
dependabot[bot] Feb 3, 2024
214bb9c
Use union instead of pipe for older python versions
jaik03 Feb 4, 2024
d506798
Ignore type checking due to ambiguous declaration
jaik03 Feb 4, 2024
8dc6085
Fix formatting via black
jaik03 Feb 4, 2024
0942bbd
Merge pull request #266 from kedder/dependabot/pip/pytest-8.0.0
kedder Feb 10, 2024
c581777
Bump pytest from 8.0.0 to 8.0.1
dependabot[bot] Feb 17, 2024
f8b4275
Merge pull request #269 from kedder/dependabot/pip/pytest-8.0.1
kedder Feb 22, 2024
ac317a7
Switch metadata to pyproject.toml
kedder Feb 22, 2024
a852d23
Install type hints for appdirs
kedder Feb 22, 2024
777f662
Reformat with newer black
kedder Feb 22, 2024
338a9f6
Drop support for 3.8, add 3.12
kedder Feb 22, 2024
280bc40
Install zipp for older pythons
kedder Feb 22, 2024
54db7c4
Merge pull request #270 from kedder/pyproject
kedder Feb 24, 2024
8f8008d
Don't add a blank security to the list
edwagner Feb 18, 2024
1944606
Decrease precision of total to 2 decimal places
edwagner Feb 18, 2024
05e2699
Add cap gain income types
edwagner Feb 18, 2024
86560e7
Bump zipp from 3.8 to 3.17.0
dependabot[bot] Mar 2, 2024
cfd9002
Bump pytest from 8.0.1 to 8.0.2
dependabot[bot] Mar 2, 2024
78cba7e
Handle INVBANKTRAN
edwagner Mar 4, 2024
8c1112c
Handle TRANSFER type
edwagner Mar 4, 2024
15f8c23
Merge pull request #273 from edwagner/invest-statement-enhancements
kedder Mar 5, 2024
c659d45
Fix plugin loading
edwagner Mar 6, 2024
273ca39
Merge pull request #274 from edwagner/fix-plugin-loading
kedder Mar 6, 2024
f7e5b1f
Bump mypy from 1.8.0 to 1.9.0
dependabot[bot] Mar 9, 2024
fd8f502
Bump black from 24.2.0 to 24.3.0
dependabot[bot] Mar 16, 2024
5de9b51
Bump zipp from 3.17.0 to 3.18.1
dependabot[bot] Mar 16, 2024
f0dd25c
Bump pytest from 8.0.2 to 8.1.1
dependabot[bot] Mar 16, 2024
4e4e716
Bump types-mock from 5.1.0.20240106 to 5.1.0.20240311
dependabot[bot] Mar 16, 2024
44a0c18
break out parse_invest_record that returns an InvestStatementLine object
jaik03 Mar 30, 2024
6d93bcf
Add InvestStatmentLine as possible reutrn to fix typing check errors.
jaik03 Jan 14, 2024
0be6f56
Use union instead of pipe for older python versions
jaik03 Feb 4, 2024
651fce2
Ignore type checking due to ambiguous declaration
jaik03 Feb 4, 2024
0bdfa59
Fix formatting via black
jaik03 Feb 4, 2024
831f6e9
break out parse_invest_record that returns an InvestStatementLine object
jaik03 Mar 30, 2024
cb9f7d9
Merge branch 'AddInvestStatementLineType' of ssh://kvmhost.home.arpa/…
jaik03 Mar 31, 2024
6a6cf3d
fix this back to original structure
jaik03 Mar 31, 2024
3b6e19c
add parse_invest_record line
jaik03 Mar 31, 2024
9688f8f
black run
jaik03 Mar 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 3 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ mypy = "*"
types-mock = "*"
exceptiongroup = {markers="python_version < '3.11'"}
tomli = {markers="python_version < '3.11'"}
types-appdirs = "*"

[packages]
ofxstatement = {editable = true,path = "."}
exceptiongroup = "*"
importlib_metadata = {version = ">=3.8", markers="python_version < '3.10'"}
importlib_metadata = {version = ">=3.8", markers="python_version < '3.10'"}
zipp = {version = ">=3.8", markers="python_version < '3.10'"}
272 changes: 138 additions & 134 deletions Pipfile.lock

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, something is messed up. This shouldn't be part of the diff. Can you rebase on master or merge it into your branch?


[project]
name = "ofxstatement"
version = "0.9.2.dev0"
authors = [
{ name="Andrey Lebedev", email="[email protected]" },
]
description = "Tool to convert proprietary bank statement to OFX format, suitable for importing to GnuCash"
readme = "README.rst"
requires-python = ">=3.9"
license = { file="LICENSE.txt" }
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Natural Language :: English",
"Topic :: Office/Business :: Financial :: Accounting",
"Topic :: Utilities",
"Environment :: Console",
"Operating System :: OS Independent",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
keywords = ["ofx", "banking", "statement"]
dependencies = [
"appdirs>=1.3.0",
"importlib_metadata>=3.8;python_version<'3.10'",
"zipp;python_version<'3.10'"
]

[project.urls]
Homepage = "https://github.com/kedder/ofxstatement"

[project.scripts]
ofxstatement = "ofxstatement.tool:run"

[tool.setuptools.package-data]
ofxstatement = ["tests/samples"]
49 changes: 4 additions & 45 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,51 +1,10 @@
[metadata]
name = ofxstatement
version = 0.9.2.dev0
description = Tool to convert proprietary bank statement to OFX format, suitable for importing to GnuCash
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/kedder/ofxstatement
author = Andrey Lebedev
author_email = [email protected]
classifiers =
Development Status :: 3 - Alpha
Programming Language :: Python :: 3
Natural Language :: English
Topic :: Office/Business :: Financial :: Accounting
Topic :: Utilities
Environment :: Console
Operating System :: OS Independent
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
keywords = ofx banking statement
project_urls =

[options]
package_dir = =src
packages = find:
python_requires = >=3.8, <4
install_requires =
appdirs>=1.3.0
importlib_metadata>=3.8;python_version<'3.10'
data_files =
namespace_packages =
ofxstatement
ofxstatement.plugins


[options.packages.find]
where = src

[options.entry_points]
console_scripts =
ofxstatement=ofxstatement.tool:run

[options.extras_require]
dev =
black
mypy
test =
mock
pytest
pytest-coverage

[mypy]
ignore_missing_imports = True

[pycodestyle]
max-line-length=88
1 change: 1 addition & 0 deletions src/ofxstatement/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__import__("pkg_resources").declare_namespace(__name__)
29 changes: 20 additions & 9 deletions src/ofxstatement/ofx.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ def buildInvestTransactionList(self) -> None:
for security_id in dict.fromkeys(
map(lambda x: x.security_id, self.statement.invest_lines)
):
if security_id is None:
continue
tb.start("STOCKINFO", {})
tb.start("SECINFO", {})
tb.start("SECID", {})
Expand Down Expand Up @@ -199,12 +201,21 @@ def buildInvestTransactionList(self) -> None:
tb.end("INVSTMTMSGSRSV1")

def buildInvestTransaction(self, line: InvestStatementLine) -> None:
# invest transactions must always have trntype and trntype_detailed
if line.trntype is None or line.trntype_detailed is None:
# invest transactions must always have trntype
if line.trntype is None:
return

tb = self.tb

if line.trntype == "INVBANKTRAN":
tb.start(line.trntype, {})
bankTran = StatementLine(line.id, line.date, line.memo, line.amount)
bankTran.trntype = line.trntype_detailed
self.buildBankTransaction(bankTran)
self.buildText("SUBACCTFUND", "OTHER")
tb.end(line.trntype)
return

tran_type_detailed_tag_name = None
inner_tran_type_tag_name = None
if line.trntype.startswith("BUY"):
Expand All @@ -213,14 +224,19 @@ def buildInvestTransaction(self, line: InvestStatementLine) -> None:
elif line.trntype.startswith("SELL"):
tran_type_detailed_tag_name = "SELLTYPE"
inner_tran_type_tag_name = "INVSELL"
elif line.trntype == "TRANSFER":
# Transfer transactions don't have details or an envelope
tran_type_detailed_tag_name = None
inner_tran_type_tag_name = None
else:
tran_type_detailed_tag_name = "INCOMETYPE"
inner_tran_type_tag_name = (
None # income transactions don't have an envelope element
)

tb.start(line.trntype, {})
self.buildText(tran_type_detailed_tag_name, line.trntype_detailed, False)
if tran_type_detailed_tag_name:
self.buildText(tran_type_detailed_tag_name, line.trntype_detailed, False)

if inner_tran_type_tag_name:
tb.start(inner_tran_type_tag_name, {})
Expand Down Expand Up @@ -266,12 +282,7 @@ def buildInvestTransaction(self, line: InvestStatementLine) -> None:
precision=self.invest_transactions_float_precision,
)

self.buildAmount(
"TOTAL",
line.amount,
False,
precision=self.invest_transactions_float_precision,
)
self.buildAmount("TOTAL", line.amount)

if inner_tran_type_tag_name:
tb.end(inner_tran_type_tag_name)
Expand Down
12 changes: 9 additions & 3 deletions src/ofxstatement/parser.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Dict, Optional, Any, Iterable, List, TextIO, TypeVar, Generic
from typing import Dict, Optional, Any, Iterable, List, TextIO, TypeVar, Generic, Union
from abc import abstractmethod
import csv
from decimal import Decimal, Decimal as D
from datetime import datetime

from ofxstatement.statement import Statement, StatementLine
from ofxstatement.statement import Statement, StatementLine, InvestStatementLine

LT = TypeVar("LT")

Expand Down Expand Up @@ -46,7 +46,7 @@ def parse(self) -> Statement:
stmt_line = self.parse_record(line)
if stmt_line:
stmt_line.assert_valid()
self.statement.lines.append(stmt_line)
self.statement.lines.append(stmt_line) # type: ignore
return self.statement
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This # type: ignore is not needed now?

FWICT, Statement class has two lists:

class Statement(Printable):
    lines: List["StatementLine"]
    invest_lines: List["InvestStatementLine"]

Plugins that parse investment statement are supposed to add lines to invest_lines. Those that parse regular bank statemrents add to lines.

Someone who added InvestStatementLine feature probably just overrode parse() and didn't use this implementation at all. And we didn't bother to add the same level of support for the investment statements that regular statements have.

That said, I think parse_invest_record is the move in right direction, we just need to use it here somehow, because right now it looks like a lonely unused method. What I suggest is to return None by default from both parse_record() and parse_invest_record(), indicating they wasn't able to parse the record.

Then, in parse itself add another couple of lines:

if stmt_line:
    ...
else:
    invest_stmt_line = self.parse_invest_record(line)
    if invest_stmt_line:
        self.statement.invest_lines.append(invest_stmt_line)

This way users can override one of the parse_*_record depending on what statements their plugins are parsing and still make use of the generic parse() method.

And we won't need these shameful # type: ignore comments as well :)

What do you think?


def split_records(self) -> Iterable[LT]: # pragma: no cover
Expand All @@ -57,6 +57,12 @@ def parse_record(self, line: LT) -> Optional[StatementLine]: # pragma: no cover
"""Parse given transaction line and return StatementLine object"""
raise NotImplementedError

def parse_invest_record(
self, line: LT
) -> Optional[InvestStatementLine]: # pragma: no cover
"""Parse given investement transaction line and return InvetStatementLine object"""
raise NotImplementedError

def parse_value(self, value: Optional[str], field: str) -> Any:
tp = StatementLine.__annotations__.get(field)
if value is None:
Expand Down
3 changes: 2 additions & 1 deletion src/ofxstatement/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Plugins are objects that configures and coordinates conversion machinery.
"""

from typing import List, Tuple, Type
from collections.abc import MutableMapping
import sys
Expand All @@ -21,7 +22,7 @@ def get_plugin(name: str, ui: UI, settings: MutableMapping) -> "Plugin":
raise PluginNotRegistered(name)
if len(plugins) > 1:
raise PluginNameConflict(plugins)
plugin = plugins[0].load() # type: ignore[index] # index requires a int but class expects a string
plugin = plugins[name].load()
return plugin(ui, settings)


Expand Down
1 change: 1 addition & 0 deletions src/ofxstatement/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__import__("pkg_resources").declare_namespace(__name__)
58 changes: 46 additions & 12 deletions src/ofxstatement/statement.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Statement model"""

from typing import List, Optional
from datetime import datetime
from decimal import Decimal as D
Expand Down Expand Up @@ -31,9 +32,11 @@
INVEST_TRANSACTION_TYPES = [
"BUYSTOCK",
"BUYDEBT",
"INCOME",
"INVBANKTRAN",
"SELLSTOCK",
"SELLDEBT",
"INCOME",
"TRANSFER",
]

INVEST_TRANSACTION_TYPES_DETAILED = [
Expand All @@ -43,6 +46,17 @@
"SELLSHORT", # open short sale
"DIV", # only for INCOME
"INTEREST", # only for INCOME
"CGLONG", # only for INCOME
"CGSHORT", # only for INCOME
]

INVBANKTRAN_TYPES_DETAILED = [
"INT",
"XFER",
"DEBIT",
"CREDIT",
"SRVCHG",
"OTHER",
]

ACCOUNT_TYPE = [
Expand Down Expand Up @@ -274,19 +288,39 @@ def assert_valid(self) -> None:
INVEST_TRANSACTION_TYPES,
)

assert (
self.trntype_detailed in INVEST_TRANSACTION_TYPES_DETAILED
), "trntype_detailed %s is not valid, must be one of %s" % (
self.trntype_detailed,
INVEST_TRANSACTION_TYPES_DETAILED,
)
if self.trntype == "INVBANKTRAN":
assert self.trntype_detailed in INVBANKTRAN_TYPES_DETAILED, (
"trntype_detailed %s is not valid for INVBANKTRAN, must be one of %s"
% (
self.trntype_detailed,
INVBANKTRAN_TYPES_DETAILED,
)
)
elif self.trntype == "TRANSFER":
assert (
self.trntype_detailed is None
), f"trntype_detailed '{self.trntype_detailed}' should be empty for TRANSFERS"
else:
assert (
self.trntype_detailed in INVEST_TRANSACTION_TYPES_DETAILED
), "trntype_detailed %s is not valid, must be one of %s" % (
self.trntype_detailed,
INVEST_TRANSACTION_TYPES_DETAILED,
)

assert self.id
assert self.security_id
assert self.amount

assert self.trntype == "INCOME" or self.units
assert self.trntype == "INCOME" or self.unit_price
assert self.date
assert self.trntype == "TRANSFER" or self.amount
assert self.trntype == "INVBANKTRAN" or self.security_id

if self.trntype == "INVBANKTRAN":
pass
elif self.trntype == "INCOME":
assert self.security_id
else:
assert self.security_id
assert self.units
assert self.trntype == "TRANSFER" or self.unit_price


class BankAccount(Printable):
Expand Down
Loading
Loading