tariochbctools 0.38.1__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/.pre-commit-config.yaml +2 -1
  2. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/PKG-INFO +5 -2
  3. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/importers.rst +4 -4
  4. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/setup.cfg +4 -1
  5. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/bcge/importer.py +4 -3
  6. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/bitst/importer.py +23 -19
  7. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/blockchain/importer.py +9 -8
  8. tariochbctools-1.0.0/src/tariochbctools/importers/cembrastatement/importer.py +118 -0
  9. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/general/mailAdapterImporter.py +12 -13
  10. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/general/mt940importer.py +18 -19
  11. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/general/priceLookup.py +6 -6
  12. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/ibkr/importer.py +21 -18
  13. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/neon/importer.py +17 -15
  14. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/netbenefits/importer.py +24 -16
  15. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/nordigen/importer.py +7 -7
  16. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/nordigen/nordigen_config.py +12 -11
  17. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/postfinance/importer.py +20 -17
  18. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/quickfile/importer.py +11 -11
  19. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/raiffeisench/importer.py +3 -7
  20. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/revolut/importer.py +23 -18
  21. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/schedule/importer.py +11 -8
  22. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/swisscard/importer.py +17 -15
  23. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/transferwise/importer.py +15 -14
  24. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/truelayer/importer.py +25 -16
  25. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/viseca/importer.py +24 -16
  26. tariochbctools-1.0.0/src/tariochbctools/importers/zak/importer.py +129 -0
  27. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/zkb/importer.py +3 -2
  28. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/plugins/prices/ibkr.py +6 -3
  29. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/PKG-INFO +5 -2
  30. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/requires.txt +4 -1
  31. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/importers/test_quickfile.py +2 -3
  32. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/importers/test_truelayer.py +2 -3
  33. tariochbctools-0.38.1/src/tariochbctools/importers/cembrastatement/importer.py +0 -102
  34. tariochbctools-0.38.1/src/tariochbctools/importers/zak/importer.py +0 -122
  35. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/.coveragerc +0 -0
  36. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/.isort.cfg +0 -0
  37. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/.readthedocs.yml +0 -0
  38. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/LICENSE.txt +0 -0
  39. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/MANIFEST.in +0 -0
  40. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/README.rst +0 -0
  41. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/_static/.gitignore +0 -0
  42. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/conf.py +0 -0
  43. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/index.rst +0 -0
  44. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/installation.rst +0 -0
  45. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/license.rst +0 -0
  46. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/plugins.rst +0 -0
  47. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/price_fetchers.rst +0 -0
  48. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/docs/requirements.txt +0 -0
  49. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/pyproject.toml +0 -0
  50. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/setup.py +0 -0
  51. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/__init__.py +0 -0
  52. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/__init__.py +0 -0
  53. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/bcge/__init__.py +0 -0
  54. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/bitst/__init__.py +0 -0
  55. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/blockchain/__init__.py +0 -0
  56. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/cembrastatement/__init__.py +0 -0
  57. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/general/__init__.py +0 -0
  58. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/ibkr/__init__.py +0 -0
  59. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/neon/__init__.py +0 -0
  60. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/netbenefits/__init__.py +0 -0
  61. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/nordigen/__init__.py +0 -0
  62. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/postfinance/__init__.py +0 -0
  63. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/quickfile/__init__.py +0 -0
  64. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/revolut/__init__.py +0 -0
  65. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/schedule/__init__.py +0 -0
  66. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/swisscard/__init__.py +0 -0
  67. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/transferwise/__init__.py +0 -0
  68. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/truelayer/__init__.py +0 -0
  69. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/viseca/__init__.py +0 -0
  70. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/zak/__init__.py +0 -0
  71. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/importers/zkb/__init__.py +0 -0
  72. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/plugins/__init__.py +0 -0
  73. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/plugins/check_portfolio_sum.py +0 -0
  74. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/plugins/generate_base_ccy_prices.py +0 -0
  75. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools/plugins/prices/__init__.py +0 -0
  76. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/SOURCES.txt +0 -0
  77. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/dependency_links.txt +0 -0
  78. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/entry_points.txt +0 -0
  79. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/not-zip-safe +0 -0
  80. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/src/tariochbctools.egg-info/top_level.txt +0 -0
  81. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/__init__.py +0 -0
  82. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/conftest.py +0 -0
  83. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/__init__.py +0 -0
  84. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/importers/__init__.py +0 -0
  85. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/__init__.py +0 -0
  86. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/entry_already_exists_expected.beancount +0 -0
  87. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/entry_already_exists_input.beancount +0 -0
  88. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/missing_fx_expected.beancount +0 -0
  89. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/missing_fx_input.beancount +0 -0
  90. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/normal_expected.beancount +0 -0
  91. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/normal_input.beancount +0 -0
  92. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tests/tariochbctools/plugins/test_generate_base_ccy_prices.py +0 -0
  93. {tariochbctools-0.38.1 → tariochbctools-1.0.0}/tox.ini +0 -0
@@ -52,4 +52,5 @@ repos:
52
52
  rev: v1.10.1
53
53
  hooks:
54
54
  - id: mypy
55
- args: [--install-types, --non-interactive, --ignore-missing-imports]
55
+ args: [--install-types, --non-interactive, --ignore-missing-imports, --disallow-incomplete-defs]
56
+ # args: [--install-types, --non-interactive, --ignore-missing-imports, --disallow-untyped-defs]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tariochbctools
3
- Version: 0.38.1
3
+ Version: 1.0.0
4
4
  Summary: Importers, plugins and price fetchers for Beancount
5
5
  Home-page: https://github.com/tarioch/beancounttools/
6
6
  Author: Patrick Ruckstuhl
@@ -20,7 +20,9 @@ Classifier: License :: OSI Approved :: MIT License
20
20
  Description-Content-Type: text/x-rst; charset=UTF-8
21
21
  License-File: LICENSE.txt
22
22
  Requires-Dist: importlib-metadata; python_version < "3.8"
23
- Requires-Dist: beancount<3,>=2
23
+ Requires-Dist: beancount>=3
24
+ Requires-Dist: beangulp
25
+ Requires-Dist: beanprice
24
26
  Requires-Dist: bitstampclient
25
27
  Requires-Dist: mt-940
26
28
  Requires-Dist: pyyaml
@@ -31,6 +33,7 @@ Requires-Dist: opencv-python
31
33
  Requires-Dist: blockcypher
32
34
  Requires-Dist: imap-tools
33
35
  Requires-Dist: undictify
36
+ Requires-Dist: rsa
34
37
  Provides-Extra: testing
35
38
  Requires-Dist: pytest; extra == "testing"
36
39
  Requires-Dist: pytest-cov; extra == "testing"
@@ -170,12 +170,12 @@ TrueLayer account IDs to beancount accounts. e.g.:
170
170
  If it is present, transactions for *only these accounts* will be imported.
171
171
 
172
172
 
173
- Nordigen
174
- --------
173
+ GoCardless (formerly Nordigen)
174
+ ------------------------------
175
175
 
176
- Import from `Nordigen <http://nordigen.com/>`__ using their api services. e.g. supports Revolut.
176
+ Import from `GoCardless <https://gocardless.com/bank-account-data/>`__ (formerly known as `Nordigen <https://gocardless.com/en-us/g/gc-nordigen/>`__) using their api services. Supports `2500+ banks <https://gocardless.com/bank-account-data/coverage/>`__ in Europe and the UK including e.g. Revolut.
177
177
  You need to create a free account and create a token. I've included a small cli to allow to hook up
178
- to different banks with nordigen. If you're country is not supported you can play around with other countries
178
+ to different banks with GoCardless. If you're country is not supported you can play around with other countries
179
179
  e.g. CH is not allowed but things like revolut still work. You can also create multiple links and they will
180
180
  all be listed in the end.
181
181
 
@@ -29,7 +29,9 @@ package_dir =
29
29
  =src
30
30
  install_requires =
31
31
  importlib-metadata; python_version<"3.8"
32
- beancount>=2,<3
32
+ beancount>=3
33
+ beangulp
34
+ beanprice
33
35
  bitstampclient
34
36
  mt-940
35
37
  pyyaml
@@ -40,6 +42,7 @@ install_requires =
40
42
  blockcypher
41
43
  imap-tools
42
44
  undictify
45
+ rsa
43
46
 
44
47
  [options.packages.find]
45
48
  where = src
@@ -1,14 +1,15 @@
1
1
  import re
2
+ from typing import Any
2
3
 
3
4
  from tariochbctools.importers.general import mt940importer
4
5
 
5
6
 
6
- def strip_newline(string):
7
+ def strip_newline(string: str) -> str:
7
8
  return string.replace("\n", "").replace("\r", "")
8
9
 
9
10
 
10
11
  class BCGEImporter(mt940importer.Importer):
11
- def prepare_payee(self, trxdata):
12
+ def prepare_payee(self, trxdata: dict[str, Any]) -> str:
12
13
  transaction_details = strip_newline(trxdata["transaction_details"])
13
14
  payee = re.search(r"ORDP/([^/]+)", transaction_details)
14
15
  if payee is None:
@@ -16,7 +17,7 @@ class BCGEImporter(mt940importer.Importer):
16
17
  else:
17
18
  return payee.group(1)
18
19
 
19
- def prepare_narration(self, trxdata):
20
+ def prepare_narration(self, trxdata: dict[str, Any]) -> str:
20
21
  transaction_details = strip_newline(trxdata["transaction_details"])
21
22
  extra_details = strip_newline(trxdata["extra_details"])
22
23
  beneficiary = re.search(r"/BENM/([^/]+)", transaction_details)
@@ -1,36 +1,38 @@
1
1
  from datetime import date
2
2
  from os import path
3
+ from typing import Any
3
4
 
5
+ import beangulp
4
6
  import bitstamp.client
5
7
  import yaml
6
8
  from beancount.core import amount, data
7
9
  from beancount.core.number import MISSING, D
8
- from beancount.ingest import importer
9
10
  from dateutil.parser import parse
10
11
  from dateutil.relativedelta import relativedelta
11
12
 
12
13
  from tariochbctools.importers.general.priceLookup import PriceLookup
13
14
 
14
15
 
15
- class Importer(importer.ImporterProtocol):
16
+ class Importer(beangulp.Importer):
16
17
  """An importer for Bitstamp."""
17
18
 
18
- def identify(self, file):
19
- return path.basename(file.name).endswith("bitstamp.yaml")
19
+ def identify(self, filepath: str) -> bool:
20
+ return path.basename(filepath).endswith("bitstamp.yaml")
20
21
 
21
- def file_account(self, file):
22
+ def account(self, filepath: str) -> data.Account:
22
23
  return ""
23
24
 
24
- def extract(self, file, existing_entries):
25
- self.priceLookup = PriceLookup(existing_entries, "CHF")
25
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
26
+ self.priceLookup = PriceLookup(existing, "CHF")
26
27
 
27
- config = yaml.safe_load(file.contents())
28
+ with open(filepath) as file:
29
+ config = yaml.safe_load(file)
28
30
  self.config = config
29
31
  self.client = bitstamp.client.Trading(
30
32
  username=config["username"], key=config["key"], secret=config["secret"]
31
33
  )
32
34
  self.currencies = config["currencies"]
33
- self.account = config["account"]
35
+ self._account = config["account"]
34
36
  self.otherExpensesAccount = config["otherExpensesAccount"]
35
37
  self.capGainAccount = config["capGainAccount"]
36
38
 
@@ -46,7 +48,7 @@ class Importer(importer.ImporterProtocol):
46
48
 
47
49
  return result
48
50
 
49
- def fetchSingle(self, trx):
51
+ def fetchSingle(self, trx: dict[str, Any]) -> data.Transaction:
50
52
  id = int(trx["id"])
51
53
  type = int(trx["type"])
52
54
  date = parse(trx["datetime"]).date()
@@ -67,12 +69,13 @@ class Importer(importer.ImporterProtocol):
67
69
 
68
70
  if type == 0:
69
71
  narration = "Deposit"
70
- cost = data.Cost(
71
- self.priceLookup.fetchPriceAmount(posCcy, date), "CHF", None, None
72
- )
72
+ if posCcy:
73
+ cost = data.Cost(
74
+ self.priceLookup.fetchPriceAmount(posCcy, date), "CHF", None, None
75
+ )
73
76
  postings = [
74
77
  data.Posting(
75
- self.account + ":" + posCcy,
78
+ self._account + ":" + posCcy,
76
79
  amount.Amount(posAmt, posCcy),
77
80
  cost,
78
81
  None,
@@ -84,7 +87,7 @@ class Importer(importer.ImporterProtocol):
84
87
  narration = "Withdrawal"
85
88
  postings = [
86
89
  data.Posting(
87
- self.account + ":" + negCcy,
90
+ self._account + ":" + negCcy,
88
91
  amount.Amount(negAmt, negCcy),
89
92
  None,
90
93
  None,
@@ -94,14 +97,15 @@ class Importer(importer.ImporterProtocol):
94
97
  ]
95
98
  elif type == 2:
96
99
  fee = D(trx["fee"])
97
- if posCcy.lower() + "_" + negCcy.lower() in trx:
100
+ if posCcy and negCcy and posCcy.lower() + "_" + negCcy.lower() in trx:
98
101
  feeCcy = negCcy
99
102
  negAmt -= fee
100
103
  else:
101
104
  feeCcy = posCcy
102
105
  posAmt -= fee
103
106
 
104
- rateFiatCcy = self.priceLookup.fetchPriceAmount(feeCcy, date)
107
+ if feeCcy:
108
+ rateFiatCcy = self.priceLookup.fetchPriceAmount(feeCcy, date)
105
109
  if feeCcy == posCcy:
106
110
  posCcyCost = None
107
111
  posCcyPrice = amount.Amount(rateFiatCcy, "CHF")
@@ -119,7 +123,7 @@ class Importer(importer.ImporterProtocol):
119
123
 
120
124
  postings = [
121
125
  data.Posting(
122
- self.account + ":" + posCcy,
126
+ self._account + ":" + posCcy,
123
127
  amount.Amount(posAmt, posCcy),
124
128
  posCcyCost,
125
129
  posCcyPrice,
@@ -127,7 +131,7 @@ class Importer(importer.ImporterProtocol):
127
131
  None,
128
132
  ),
129
133
  data.Posting(
130
- self.account + ":" + negCcy,
134
+ self._account + ":" + negCcy,
131
135
  amount.Amount(negAmt, negCcy),
132
136
  negCcyCost,
133
137
  negCcyPrice,
@@ -1,28 +1,29 @@
1
1
  from os import path
2
2
 
3
+ import beangulp
3
4
  import blockcypher
4
5
  import yaml
5
6
  from beancount.core import amount, data
6
7
  from beancount.core.number import D
7
- from beancount.ingest import importer
8
8
 
9
9
  from tariochbctools.importers.general.priceLookup import PriceLookup
10
10
 
11
11
 
12
- class Importer(importer.ImporterProtocol):
12
+ class Importer(beangulp.Importer):
13
13
  """An importer for Blockchain data."""
14
14
 
15
- def identify(self, file):
16
- return path.basename(file.name).endswith("blockchain.yaml")
15
+ def identify(self, filepath: str) -> bool:
16
+ return path.basename(filepath).endswith("blockchain.yaml")
17
17
 
18
- def file_account(self, file):
18
+ def account(self, filepath: str) -> data.Entries:
19
19
  return ""
20
20
 
21
- def extract(self, file, existing_entries):
22
- config = yaml.safe_load(file.contents())
21
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
22
+ with open(filepath) as file:
23
+ config = yaml.safe_load(file)
23
24
  self.config = config
24
25
  baseCcy = config["base_ccy"]
25
- priceLookup = PriceLookup(existing_entries, baseCcy)
26
+ priceLookup = PriceLookup(existing, baseCcy)
26
27
 
27
28
  entries = []
28
29
  for address in self.config["addresses"]:
@@ -0,0 +1,118 @@
1
+ import datetime
2
+ import re
3
+ from datetime import timedelta
4
+
5
+ import beangulp
6
+ import camelot
7
+ from beancount.core import amount, data
8
+ from beancount.core.number import D
9
+
10
+
11
+ class Importer(beangulp.Importer):
12
+ """An importer for Cembra Card Statement PDF files."""
13
+
14
+ def __init__(self, filepattern: str, account: data.Account):
15
+ self._filepattern = filepattern
16
+ self._account = account
17
+ self.currency = "CHF"
18
+
19
+ def identify(self, filepath: str) -> bool:
20
+ return re.search(self._filepattern, filepath) is not None
21
+
22
+ def account(self, filepath: str) -> data.Account:
23
+ return self._account
24
+
25
+ def createEntry(
26
+ self, filepath: str, date: datetime.date, amt: data.Decimal, text: str
27
+ ) -> data.Transaction:
28
+ meta = data.new_metadata(filepath, 0)
29
+ return data.Transaction(
30
+ meta,
31
+ date,
32
+ "*",
33
+ "",
34
+ text,
35
+ data.EMPTY_SET,
36
+ data.EMPTY_SET,
37
+ [
38
+ data.Posting(self._account, amt, None, None, None, None),
39
+ ],
40
+ )
41
+
42
+ def createBalanceEntry(
43
+ self, filepath: str, date: datetime.date, amt: data.Decimal
44
+ ) -> data.Balance:
45
+ meta = data.new_metadata(filepath, 0)
46
+ return data.Balance(meta, date, self._account, amt, None, None)
47
+
48
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
49
+ entries = []
50
+
51
+ tables = camelot.read_pdf(
52
+ filepath, pages="2-end", flavor="stream", table_areas=["50,700,560,50"]
53
+ )
54
+ for table in tables:
55
+ df = table.df
56
+
57
+ # skip incompatible tables
58
+ if df.columns.size != 5:
59
+ continue
60
+
61
+ for index, row in df.iterrows():
62
+ trx_date, book_date, text, credit, debit = tuple(row)
63
+ trx_date, book_date, text, credit, debit = (
64
+ trx_date.strip(),
65
+ book_date.strip(),
66
+ text.strip(),
67
+ credit.strip(),
68
+ debit.strip(),
69
+ )
70
+
71
+ # Transaction entry
72
+ try:
73
+ book_date = datetime.datetime.strptime(book_date, "%d.%m.%Y").date()
74
+ except Exception:
75
+ book_date = None
76
+
77
+ if book_date:
78
+ amount = self.getAmount(debit, credit)
79
+
80
+ if amount:
81
+ entries.append(
82
+ self.createEntry(filepath, book_date, amount, text)
83
+ )
84
+ continue
85
+
86
+ # Balance entry
87
+ try:
88
+ m = re.search(
89
+ r"Saldo per (\d\d\.\d\d\.\d\d\d\d) zu unseren Gunsten CHF",
90
+ text,
91
+ )
92
+ if m:
93
+ book_date = m.group(1)
94
+ book_date = datetime.datetime.strptime(
95
+ book_date, "%d.%m.%Y"
96
+ ).date()
97
+ # add 1 day: cembra provides balance at EOD, but beancount checks it at SOD
98
+ book_date = book_date + timedelta(days=1)
99
+ except Exception:
100
+ book_date = None
101
+
102
+ if book_date:
103
+ amount = self.getAmount(debit, credit)
104
+
105
+ if amount:
106
+ entries.append(
107
+ self.createBalanceEntry(filepath, book_date, amount)
108
+ )
109
+
110
+ return entries
111
+
112
+ def cleanDecimal(self, formattedNumber: str) -> data.Decimal:
113
+ return D(formattedNumber.replace("'", ""))
114
+
115
+ def getAmount(self, debit: str, credit: str) -> data.Amount:
116
+ amt = -self.cleanDecimal(debit) if debit else self.cleanDecimal(credit)
117
+ if amt:
118
+ return amount.Amount(amt, self.currency)
@@ -2,24 +2,26 @@ import tempfile
2
2
  from os import path
3
3
 
4
4
  import yaml
5
- from beancount.ingest import cache, importer
5
+ from beancount.core import data
6
+ from beangulp import Importer
6
7
  from imap_tools import MailBox
7
8
 
8
9
 
9
- class MailAdapterImporter(importer.ImporterProtocol):
10
+ class MailAdapterImporter(Importer):
10
11
  """An importer adapter that fetches file from mails and then calls another importer."""
11
12
 
12
- def __init__(self, importers):
13
+ def __init__(self, importers: list[Importer]):
13
14
  self.importers = importers
14
15
 
15
- def identify(self, file):
16
- return "mail.yaml" == path.basename(file.name)
16
+ def identify(self, filepath: str) -> bool:
17
+ return "mail.yaml" == path.basename(filepath)
17
18
 
18
- def file_account(self, file):
19
+ def account(self, filepath: str) -> data.Account:
19
20
  return ""
20
21
 
21
- def extract(self, file, existing_entries):
22
- config = yaml.safe_load(file.contents())
22
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
23
+ with open(filepath) as file:
24
+ config = yaml.safe_load(file)
23
25
 
24
26
  with MailBox(config["host"]).login(
25
27
  config["user"], config["password"], initial_folder=config["folder"]
@@ -33,13 +35,10 @@ class MailAdapterImporter(importer.ImporterProtocol):
33
35
  with open(attFileName, "wb") as attFile:
34
36
  attFile.write(att.payload)
35
37
  attFile.flush()
36
- fileMemo = cache.get_file(attFileName)
37
38
 
38
39
  for delegate in self.importers:
39
- if delegate.identify(fileMemo):
40
- newEntries = delegate.extract(
41
- fileMemo, existing_entries
42
- )
40
+ if delegate.identify(attFileName):
41
+ newEntries = delegate.extract(attFileName, existing)
43
42
  result.extend(newEntries)
44
43
  processed = True
45
44
 
@@ -1,29 +1,28 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ import beangulp
1
5
  import mt940
2
6
  from beancount.core import amount, data
3
7
  from beancount.core.number import D
4
- from beancount.ingest import importer
5
- from beancount.ingest.importers.mixins import identifier
6
8
 
7
9
 
8
- class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
10
+ class Importer(beangulp.Importer):
9
11
  """An importer for MT940 files."""
10
12
 
11
- def __init__(self, regexps, account):
12
- identifier.IdentifyMixin.__init__(self, matchers=[("filename", regexps)])
13
- self.account = account
14
-
15
- def identify(self, file):
16
- if file.mimetype() != "text/plain":
17
- return False
13
+ def __init__(self, filepattern: str, account: data.Account):
14
+ self._filepattern = filepattern
15
+ self._account = account
18
16
 
19
- return super().identify(file)
17
+ def identify(self, filepath: str) -> bool:
18
+ return re.search(self._filepattern, filepath) is not None
20
19
 
21
- def file_account(self, file):
22
- return self.account
20
+ def account(self, filepath: str) -> data.Account:
21
+ return self._account
23
22
 
24
- def extract(self, file, existing_entries):
23
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
25
24
  entries = []
26
- transactions = mt940.parse(file.contents())
25
+ transactions = mt940.parse(filepath)
27
26
  for trx in transactions:
28
27
  trxdata = trx.data
29
28
  ref = trxdata["bank_reference"]
@@ -31,7 +30,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
31
30
  metakv = {"ref": ref}
32
31
  else:
33
32
  metakv = None
34
- meta = data.new_metadata(file.name, 0, metakv)
33
+ meta = data.new_metadata(filepath, 0, metakv)
35
34
  if "entry_date" in trxdata:
36
35
  date = trxdata["entry_date"]
37
36
  else:
@@ -46,7 +45,7 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
46
45
  data.EMPTY_SET,
47
46
  [
48
47
  data.Posting(
49
- self.account,
48
+ self._account,
50
49
  amount.Amount(
51
50
  D(trxdata["amount"].amount), trxdata["amount"].currency
52
51
  ),
@@ -61,8 +60,8 @@ class Importer(identifier.IdentifyMixin, importer.ImporterProtocol):
61
60
 
62
61
  return entries
63
62
 
64
- def prepare_payee(self, trxdata):
63
+ def prepare_payee(self, trxdata: dict[str, Any]) -> str:
65
64
  return ""
66
65
 
67
- def prepare_narration(self, trxdata):
66
+ def prepare_narration(self, trxdata: dict[str, Any]) -> str:
68
67
  return trxdata["transaction_details"] + " " + trxdata["extra_details"]
@@ -1,18 +1,18 @@
1
1
  from datetime import date
2
2
 
3
- from beancount.core import amount, prices
3
+ from beancount.core import amount, data, prices
4
4
  from beancount.core.number import D
5
5
 
6
6
 
7
7
  class PriceLookup:
8
- def __init__(self, existing_entries, baseCcy: str):
9
- if existing_entries:
10
- self.priceMap = prices.build_price_map(existing_entries)
8
+ def __init__(self, existing: data.Entries, baseCcy: str):
9
+ if existing:
10
+ self.priceMap = prices.build_price_map(existing)
11
11
  else:
12
12
  self.priceMap = None
13
13
  self.baseCcy = baseCcy
14
14
 
15
- def fetchPriceAmount(self, instrument: str, date: date):
15
+ def fetchPriceAmount(self, instrument: str, date: date) -> data.Amount:
16
16
  if self.priceMap:
17
17
  price = prices.get_price(
18
18
  self.priceMap, tuple([instrument, self.baseCcy]), date
@@ -21,7 +21,7 @@ class PriceLookup:
21
21
  else:
22
22
  return D(1)
23
23
 
24
- def fetchPrice(self, instrument: str, date: date):
24
+ def fetchPrice(self, instrument: str, date: date) -> data.Amount:
25
25
  if instrument == self.baseCcy:
26
26
  return None
27
27
 
@@ -2,27 +2,30 @@ import re
2
2
  from datetime import date
3
3
  from decimal import Decimal
4
4
  from os import path
5
+ from typing import Any
5
6
 
7
+ import beangulp
6
8
  import yaml
7
9
  from beancount.core import amount, data
8
10
  from beancount.core.number import D
9
- from beancount.ingest import importer
10
11
  from ibflex import Types, client, parser
11
12
  from ibflex.enums import CashAction
12
13
 
13
14
  from tariochbctools.importers.general.priceLookup import PriceLookup
14
15
 
15
16
 
16
- class Importer(importer.ImporterProtocol):
17
+ class Importer(beangulp.Importer):
17
18
  """An importer for Interactive Broker using the flex query service."""
18
19
 
19
- def identify(self, file):
20
- return path.basename(file.name).endswith("ibkr.yaml")
20
+ def identify(self, filepath: str) -> bool:
21
+ return path.basename(filepath).endswith("ibkr.yaml")
21
22
 
22
- def file_account(self, file):
23
+ def account(self, filepath: str) -> data.Account:
23
24
  return ""
24
25
 
25
- def matches(self, trx, t, account):
26
+ def matches(
27
+ self, trx: Types.CashTransaction, t: Any, account: data.Account
28
+ ) -> bool:
26
29
  p = re.compile(r".* (?P<perShare>\d+\.?\d+) PER SHARE")
27
30
 
28
31
  trxPerShareGroups = p.search(trx.description)
@@ -38,13 +41,13 @@ class Importer(importer.ImporterProtocol):
38
41
  and t["account"] == account
39
42
  )
40
43
 
41
- def extract(self, file, existing_entries):
42
- with open(file.name, "r") as f:
44
+ def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
45
+ with open(filepath, "r") as f:
43
46
  config = yaml.safe_load(f)
44
47
  token = config["token"]
45
48
  queryId = config["queryId"]
46
49
 
47
- priceLookup = PriceLookup(existing_entries, config["baseCcy"])
50
+ priceLookup = PriceLookup(existing, config["baseCcy"])
48
51
 
49
52
  response = client.download(token, queryId)
50
53
  statement = parser.parse(response)
@@ -52,7 +55,7 @@ class Importer(importer.ImporterProtocol):
52
55
 
53
56
  result = []
54
57
  for stmt in statement.FlexStatements:
55
- transactions = []
58
+ transactions: list = []
56
59
  account = stmt.accountId
57
60
  for trx in stmt.Trades:
58
61
  result.append(
@@ -147,7 +150,7 @@ class Importer(importer.ImporterProtocol):
147
150
  priceLookup: PriceLookup,
148
151
  description: str,
149
152
  account: str,
150
- ):
153
+ ) -> data.Transaction:
151
154
  narration = "Dividend: " + description
152
155
  liquidityAccount = self.getLiquidityAccount(account, currency)
153
156
  incomeAccount = self.getIncomeAccount(account)
@@ -190,7 +193,7 @@ class Importer(importer.ImporterProtocol):
190
193
  def createBuy(
191
194
  self,
192
195
  date: date,
193
- account: str,
196
+ account: data.Account,
194
197
  asset: str,
195
198
  quantity: Decimal,
196
199
  currency: str,
@@ -199,7 +202,7 @@ class Importer(importer.ImporterProtocol):
199
202
  netCash: amount.Amount,
200
203
  baseCcy: str,
201
204
  fxRateToBase: Decimal,
202
- ):
205
+ ) -> data.Transaction:
203
206
  narration = "Buy"
204
207
  feeAccount = self.getFeeAccount(account)
205
208
  liquidityAccount = self.getLiquidityAccount(account, currency)
@@ -238,17 +241,17 @@ class Importer(importer.ImporterProtocol):
238
241
  meta, date, "*", "", narration, data.EMPTY_SET, data.EMPTY_SET, postings
239
242
  )
240
243
 
241
- def getAssetAccount(self, account: str, asset: str):
244
+ def getAssetAccount(self, account: str, asset: str) -> data.Account:
242
245
  return f"Assets:{account}:Investment:IB:{asset}"
243
246
 
244
- def getLiquidityAccount(self, account: str, currency: str):
247
+ def getLiquidityAccount(self, account: str, currency: str) -> data.Account:
245
248
  return f"Assets:{account}:Liquidity:IB:{currency}"
246
249
 
247
- def getReceivableAccount(self, account: str):
250
+ def getReceivableAccount(self, account: str) -> data.Account:
248
251
  return f"Assets:{account}:Receivable:Verrechnungssteuer"
249
252
 
250
- def getIncomeAccount(self, account: str):
253
+ def getIncomeAccount(self, account: str) -> data.Account:
251
254
  return f"Income:{account}:Interest"
252
255
 
253
- def getFeeAccount(self, account: str):
256
+ def getFeeAccount(self, account: str) -> data.Account:
254
257
  return f"Expenses:{account}:Fees"