tariochbctools 1.4.1__py2.py3-none-any.whl → 1.5.1__py2.py3-none-any.whl

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.
@@ -5,7 +5,7 @@ from typing import Any
5
5
 
6
6
  import yaml
7
7
  from awardwallet import AwardWalletClient
8
- from awardwallet.api import AccessLevel
8
+ from awardwallet.client import AccessLevel
9
9
 
10
10
 
11
11
  def get_link_url(client):
@@ -33,19 +33,18 @@ def generate(client):
33
33
  connected_users = client.list_connected_users()
34
34
 
35
35
  for user in connected_users:
36
- user_id = user["userId"]
37
- user_details = client.get_connected_user_details(user_id)
36
+ user_details = client.get_connected_user_details(user.user_id)
38
37
  account_config = {}
39
38
 
40
- for account in user_details.get("accounts", []):
41
- account_config[account["accountId"]] = {
42
- "provider": account["displayName"],
39
+ for account in user_details.accounts:
40
+ account_config[account.account_id] = {
41
+ "provider": account.display_name,
43
42
  "account": "Assets:Current:Points", # Placeholder, user should adjust
44
43
  "currency": "POINTS",
45
44
  }
46
45
 
47
- config["users"][user_id] = {
48
- "name": user["userName"],
46
+ config["users"][user.user_id] = {
47
+ "name": user.user_name,
49
48
  "all_history": False,
50
49
  "accounts": account_config,
51
50
  }
@@ -1,11 +1,13 @@
1
+ import datetime
1
2
  import logging
3
+ import re
4
+ from operator import attrgetter
2
5
  from os import path
3
- from typing import Any
4
6
 
5
7
  import beangulp
6
8
  import dateutil.parser
7
9
  import yaml
8
- from awardwallet import AwardWalletClient
10
+ from awardwallet import AwardWalletClient, model
9
11
  from beancount.core import amount, data
10
12
  from beancount.core.number import D
11
13
 
@@ -46,27 +48,33 @@ class Importer(beangulp.Importer):
46
48
  return entries
47
49
 
48
50
  def _extract_user_history(
49
- self, user: dict, user_details: dict
51
+ self, user: dict, user_details: model.GetConnectedUserDetailsResponse
50
52
  ) -> list[data.Transaction]:
51
53
  """
52
54
  User history is limited to the last 10 history elements per account
53
55
  """
54
56
  entries = []
55
- for account in user_details["accounts"]:
56
- account_id = account["accountId"]
57
-
58
- if account_id in user["accounts"]:
59
- logging.info("Extracting account ID %s", account_id)
60
- account_config = user["accounts"][account_id]
57
+ for account in user_details.accounts:
58
+ if account.account_id in user["accounts"]:
59
+ logging.info("Extracting account ID %s", account.account_id)
60
+ account_config = user["accounts"][account.account_id]
61
61
 
62
62
  entries.extend(
63
63
  self._extract_transactions(
64
- account["history"], account_config, account_id
64
+ account.history, account_config, account.account_id
65
65
  )
66
66
  )
67
+
68
+ # we fudge the date by using the latest txn (across *all* accounts)
69
+ latest_txn = max(entries, key=attrgetter("date"), default=None)
70
+ entries.extend(
71
+ self._extract_balance(account, account_config, latest_txn)
72
+ )
67
73
  else:
68
74
  logging.warning(
69
- "Ignoring account ID %s: %s", account_id, account["displayName"]
75
+ "Ignoring account ID %s: %s",
76
+ account.account_id,
77
+ account.display_name,
70
78
  )
71
79
  return entries
72
80
 
@@ -76,13 +84,15 @@ class Importer(beangulp.Importer):
76
84
  entries = []
77
85
  for account_id, account_config in user["accounts"].items():
78
86
  logging.info("Extracting account ID %s", account_id)
79
- account = client.get_account_details(account_id)["account"]
87
+ account = client.get_account_details(account_id).account
80
88
 
81
89
  entries.extend(
82
- self._extract_transactions(
83
- account["history"], account_config, account_id
84
- )
90
+ self._extract_transactions(account.history, account_config, account_id)
85
91
  )
92
+
93
+ # we fudge the date by using the latest txn (across *all* accounts)
94
+ latest_txn = max(entries, key=attrgetter("date"), default=None)
95
+ entries.extend(self._extract_balance(account, account_config, latest_txn))
86
96
  return entries
87
97
 
88
98
  def _extract_transactions(
@@ -109,7 +119,7 @@ class Importer(beangulp.Importer):
109
119
 
110
120
  def _extract_transaction(
111
121
  self,
112
- trx: dict[str, Any],
122
+ trx: model.HistoryItem,
113
123
  local_account: data.Account,
114
124
  currency: str,
115
125
  account_id: str,
@@ -121,20 +131,23 @@ class Importer(beangulp.Importer):
121
131
  trx_description = None
122
132
  trx_amount = None
123
133
 
124
- for f in trx.get("fields", []):
125
- if f["code"] == "PostingDate":
126
- trx_date = dateutil.parser.parse(f["value"]["value"]).date()
127
- if f["code"] == "Description":
128
- trx_description = f["value"]["value"].replace("\n", " ")
129
- if f["code"] == "Miles":
130
- trx_amount = D(f["value"]["value"])
131
- if f["code"] == "Info":
132
- name = f["name"].lower().replace(" ", "-")
133
- metakv[name] = f["value"]["value"].replace("\n", " ")
134
+ for f in trx.fields:
135
+ if f.code == "PostingDate":
136
+ trx_date = dateutil.parser.parse(f.value.value).date()
137
+ if f.code == "Description":
138
+ trx_description = f.value.value.replace("\n", " ")
139
+ if f.code == "Miles":
140
+ trx_amount = D(f.value.value)
141
+ if f.code == "Info":
142
+ name = re.sub(r"\W", "-", f.name).lower()
143
+ metakv[name] = f.value.value.replace("\n", " ")
134
144
 
135
145
  assert trx_date
136
146
  assert trx_description
137
- assert trx_amount is not None, f"No amount in trx: {trx}"
147
+
148
+ if trx_amount is None:
149
+ logging.warning("Skipping transaction with no amount: %s", trx)
150
+ return []
138
151
 
139
152
  meta = data.new_metadata("", 0, metakv)
140
153
  entry = data.Transaction(
@@ -158,3 +171,36 @@ class Importer(beangulp.Importer):
158
171
  )
159
172
  entries.append(entry)
160
173
  return entries
174
+
175
+ def _extract_balance(
176
+ self,
177
+ account: model.Account,
178
+ account_config: dict,
179
+ latest_txn: data.Transaction | None,
180
+ ) -> list[data.Transaction]:
181
+ local_account = account_config["account"]
182
+ currency = account_config["currency"]
183
+ balance = amount.Amount(D(account.balance_raw), currency)
184
+ metakv = {"account-id": str(account.account_id)}
185
+
186
+ optional_date = account.last_change_date or account.last_retrieve_date
187
+ if optional_date:
188
+ date = optional_date.date()
189
+ elif latest_txn:
190
+ date = latest_txn.date
191
+ else:
192
+ logging.warning(
193
+ "No date information available for balance of account %s, using today",
194
+ account.account_id,
195
+ )
196
+ date = datetime.date.today()
197
+
198
+ entry = data.Balance(
199
+ data.new_metadata("", 0, metakv),
200
+ date + datetime.timedelta(days=1),
201
+ local_account,
202
+ balance,
203
+ None,
204
+ None,
205
+ )
206
+ return [entry]
@@ -1,5 +1,5 @@
1
1
  import re
2
- from datetime import datetime
2
+ from datetime import datetime, timedelta
3
3
 
4
4
  import beangulp
5
5
  import camelot
@@ -46,6 +46,11 @@ class Importer(beangulp.Importer):
46
46
  if conversionOriginal and conversionRate:
47
47
  kv = {"original": conversionOriginal, "rate": conversionRate}
48
48
  text = text.replace("Amount: " + conversionOriginal, "")
49
+ # handle decimal seperated original amounts
50
+ [originalCcy, originalAmt] = conversionOriginal.split(" ")
51
+ text = text.replace(
52
+ "Amount: " + f"{originalCcy} {float(originalAmt):,}", ""
53
+ )
49
54
  else:
50
55
  kv = None
51
56
 
@@ -63,17 +68,34 @@ class Importer(beangulp.Importer):
63
68
  ],
64
69
  )
65
70
 
71
+ def createBalance(
72
+ self,
73
+ filepath: str,
74
+ date: str,
75
+ amt: amount.Amount,
76
+ ) -> data.Balance:
77
+ meta = data.new_metadata(filepath, 0, None)
78
+ return data.Balance(
79
+ meta,
80
+ datetime.strptime(date, "%d.%m.%Y").date() + timedelta(days=1),
81
+ self._account,
82
+ amt,
83
+ None,
84
+ None,
85
+ )
86
+
66
87
  def extract(self, filepath: str, existing: data.Entries) -> data.Entries:
67
88
  entries = []
68
89
 
69
90
  conversionPattern = re.compile(r"(?P<original>.+) at the rate of (?P<rate>.+)")
91
+ balancePattern = re.compile(r"Balance as of (?P<date>\d\d\.\d\d\.\d\d\d\d)")
70
92
 
71
93
  tables = camelot.read_pdf(
72
94
  filepath,
73
95
  flavor="stream",
74
96
  pages="all",
75
- table_regions=["40,470,580,32"],
76
- columns=["110,300,370,440,500"],
97
+ table_regions=["40,600,580,32"],
98
+ columns=["110,305,370,440,500"],
77
99
  strip_text="\n",
78
100
  layout_kwargs={"word_margin": 0.50},
79
101
  split_text=True,
@@ -89,7 +111,7 @@ class Importer(beangulp.Importer):
89
111
  conversionOriginal = None
90
112
  conversionRate = None
91
113
  for _, row in df.iterrows():
92
- date, text, _, debit, credit, _ = tuple(row)
114
+ date, text, _, debit, credit, bal = tuple(row)
93
115
 
94
116
  # skip stuff before
95
117
  if beforeStart and "Date" != date:
@@ -98,8 +120,16 @@ class Importer(beangulp.Importer):
98
120
  beforeStart = False
99
121
  continue
100
122
 
101
- # skip stuff after
102
- if "Balance as of" in text:
123
+ # create balance and skip stuff after
124
+ balanceMatch = balancePattern.match(text)
125
+ if balanceMatch:
126
+ entries.append(
127
+ self.createBalance(
128
+ filepath,
129
+ balanceMatch.group("date"),
130
+ amount.Amount(D(bal.replace("'", "")), self.currency),
131
+ )
132
+ )
103
133
  break
104
134
 
105
135
  trxDate = date
@@ -10,6 +10,8 @@ import yaml
10
10
  from beancount.core import amount, data
11
11
  from beancount.core.number import D
12
12
 
13
+ from tariochbctools.importers.general.deduplication import ReferenceDuplicatesComparator
14
+
13
15
  # https://docs.truelayer.com/#retrieve-account-transactions
14
16
 
15
17
  TX_MANDATORY_ID_FIELDS = ("transaction_id",)
@@ -125,6 +127,17 @@ class Importer(beangulp.Importer):
125
127
  logging.warning("Ignoring account ID %s", accountId)
126
128
  continue
127
129
 
130
+ r = requests.get(
131
+ f"https://api.{self.domain}/data/v1/{endpoint}/{accountId}/balance",
132
+ headers=headers,
133
+ )
134
+ balances = r.json()["results"]
135
+
136
+ for balance in balances:
137
+ entries.extend(
138
+ self._extract_balance(balance, local_account, invert_sign)
139
+ )
140
+
128
141
  r = requests.get(
129
142
  f"https://api.{self.domain}/data/v1/{endpoint}/{accountId}/transactions",
130
143
  headers=headers,
@@ -148,7 +161,7 @@ class Importer(beangulp.Importer):
148
161
  invert_sign: bool,
149
162
  ) -> data.Transaction:
150
163
  entries = []
151
- metakv = {}
164
+ metakv: dict[str, Any] = {}
152
165
 
153
166
  id_meta_kvs = {
154
167
  k: trx["meta"][k] for k in TX_OPTIONAL_META_ID_FIELDS if trx["meta"].get(k)
@@ -193,37 +206,52 @@ class Importer(beangulp.Importer):
193
206
  )
194
207
  entries.append(entry)
195
208
 
196
- if trx["transaction_id"] == transactions[-1]["transaction_id"]:
197
- balDate = trxDate + timedelta(days=1)
198
- metakv = {}
199
- if self.existing is not None:
200
- for exEntry in self.existing:
201
- if (
202
- isinstance(exEntry, data.Balance)
203
- and exEntry.date == balDate
204
- and exEntry.account == local_account
205
- ):
206
- metakv["__duplicate__"] = True
207
-
208
- meta = data.new_metadata("", 0, metakv)
209
-
210
- # Only if the 'balance' permission is present
211
- if "running_balance" in trx:
212
- tx_balance = D(str(trx["running_balance"]["amount"]))
213
- # avoid pylint invalid-unary-operand-type
214
- signed_balance = -1 * tx_balance if invert_sign else tx_balance
215
-
216
- entries.append(
217
- data.Balance(
218
- meta,
219
- balDate,
220
- local_account,
221
- amount.Amount(
222
- signed_balance, trx["running_balance"]["currency"]
223
- ),
224
- None,
225
- None,
226
- )
209
+ return entries
210
+
211
+ def _extract_balance(
212
+ self,
213
+ result: dict[str, Any],
214
+ local_account: data.Account,
215
+ invert_sign: bool,
216
+ ) -> data.Transaction:
217
+ entries = []
218
+
219
+ meta = data.new_metadata("", 0)
220
+
221
+ balance = D(str(result["current"]))
222
+ # avoid pylint invalid-unary-operand-type
223
+ signed_balance = -1 * balance if invert_sign else balance
224
+ balance_date = dateutil.parser.parse(result["update_timestamp"]).date()
225
+
226
+ entries.append(
227
+ data.Balance(
228
+ meta,
229
+ balance_date + timedelta(days=1),
230
+ local_account,
231
+ amount.Amount(signed_balance, result["currency"]),
232
+ None,
233
+ None,
234
+ )
235
+ )
236
+
237
+ if "last_statement_balance" in result:
238
+ statement_balance = D(str(result["last_statement_balance"]))
239
+ signed_statement_balance = (
240
+ -1 * statement_balance if invert_sign else statement_balance
241
+ )
242
+ statement_date = dateutil.parser.parse(result["last_statement_date"]).date()
243
+
244
+ entries.append(
245
+ data.Balance(
246
+ meta,
247
+ statement_date,
248
+ local_account,
249
+ amount.Amount(signed_statement_balance, result["currency"]),
250
+ None,
251
+ None,
227
252
  )
253
+ )
228
254
 
229
255
  return entries
256
+
257
+ cmp = ReferenceDuplicatesComparator(TX_MANDATORY_ID_FIELDS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tariochbctools
3
- Version: 1.4.1
3
+ Version: 1.5.1
4
4
  Summary: Importers, plugins and price fetchers for Beancount
5
5
  Home-page: https://github.com/tarioch/beancounttools/
6
6
  Author: Patrick Ruckstuhl
@@ -19,7 +19,7 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
19
19
  Description-Content-Type: text/x-rst; charset=UTF-8
20
20
  License-File: LICENSE.txt
21
21
  Requires-Dist: importlib-metadata; python_version < "3.8"
22
- Requires-Dist: awardwallet
22
+ Requires-Dist: awardwallet>=0.2
23
23
  Requires-Dist: beancount>=3
24
24
  Requires-Dist: beangulp
25
25
  Requires-Dist: beanprice
@@ -1,8 +1,8 @@
1
1
  tariochbctools/__init__.py,sha256=pH5i4Fj1tbXLqLtTVIdoojiplZssQn0nnud8-HXodRE,577
2
2
  tariochbctools/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  tariochbctools/importers/awardwalletimp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- tariochbctools/importers/awardwalletimp/config.py,sha256=EPwvUE9WBdCk9xxleJ8qDLfPy8GwcgAtG1uGSShyt4w,2460
5
- tariochbctools/importers/awardwalletimp/importer.py,sha256=0G3DcxYP_chy1zUYMCijDOwNZBBBdX8hR2sPKiVHZS8,5071
4
+ tariochbctools/importers/awardwalletimp/config.py,sha256=OEhKKkA-lJqyT6QImqlsJVAT4iiNeGwX2DKNPZN6cic,2423
5
+ tariochbctools/importers/awardwalletimp/importer.py,sha256=Z4GeK3xqQTbdNDFQVObDXqL3SWX-KPjhbZ25TtBD-cg,6754
6
6
  tariochbctools/importers/bcge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  tariochbctools/importers/bcge/importer.py,sha256=bAQpkBLURatdsfSoMQdiha-8QfExAXXvpzAgnFFMwoE,1176
8
8
  tariochbctools/importers/bitst/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,7 +30,7 @@ tariochbctools/importers/postfinance/importer.py,sha256=PLBbnswb_tHt8wzd7yCC4UAW
30
30
  tariochbctools/importers/quickfile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  tariochbctools/importers/quickfile/importer.py,sha256=HgS7lSi7egxkj-IWd77MS-vhepYNCRXZwVbeCornkzg,6479
32
32
  tariochbctools/importers/radicant/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- tariochbctools/importers/radicant/importer.py,sha256=d2AKncK4-5PMDJ-98gqgWYubMWpO46LJ7eY8koa6y90,4490
33
+ tariochbctools/importers/radicant/importer.py,sha256=chjHX25YoU7jaonegJ8gaV2QzvSm7sdH8a7cj-rUmPA,5602
34
34
  tariochbctools/importers/raiffeisench/importer.py,sha256=5r7hHEG0ZtEp-ePlsF4aHapBv9DJgjeVxE1m_G62bLU,912
35
35
  tariochbctools/importers/revolut/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  tariochbctools/importers/revolut/importer.py,sha256=5jI91gGIDcP2TMFcauKkA9OWCTqCIl5Phz9n6r9kOeI,3953
@@ -41,7 +41,7 @@ tariochbctools/importers/swisscard/importer.py,sha256=84nyAHzz5Y8mHw-zCkoUE0CsaE
41
41
  tariochbctools/importers/transferwise/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  tariochbctools/importers/transferwise/importer.py,sha256=IdtNmrLHKxrBBBVLf8Qgysw0Mx8XswF4cZv9iRl2b3g,6312
43
43
  tariochbctools/importers/truelayer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
- tariochbctools/importers/truelayer/importer.py,sha256=fkASr_cqxkGPf45Eltl4rwKdwoMN0ild3HbCPLk_r1U,7172
44
+ tariochbctools/importers/truelayer/importer.py,sha256=rHJdbqP9wT1BackBq7lIoLAT04D-MM0Bc3-YtQSZF1c,7875
45
45
  tariochbctools/importers/viseca/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  tariochbctools/importers/viseca/importer.py,sha256=U9HHmRyIutJYuAtS-W3gHW4kJYEfnyJmnrOVVMmZQAY,3346
47
47
  tariochbctools/importers/zak/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -53,9 +53,9 @@ tariochbctools/plugins/check_portfolio_sum.py,sha256=naJ2j6BFpQhJhT2c-gfjyIdcYe0
53
53
  tariochbctools/plugins/generate_base_ccy_prices.py,sha256=4CDzUosjMWCZfsBJMLrf-i5WNCHexI2rdm5vIDYW-AI,1314
54
54
  tariochbctools/plugins/prices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  tariochbctools/plugins/prices/ibkr.py,sha256=GYCjnlF-MK-ZFPEr0M6T4iO3Etq0tMmrMlsHGInXUO8,1405
56
- tariochbctools-1.4.1.dist-info/licenses/LICENSE.txt,sha256=VR2hkz3p9Sw4hSXc7S5iZTOXGeV4h-i8AO_q0zEmtkE,1074
57
- tariochbctools-1.4.1.dist-info/METADATA,sha256=x5jAZ6K1do4OMGOoJYbteq1nf3Qw4dW1DR7693B1XCk,2175
58
- tariochbctools-1.4.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
59
- tariochbctools-1.4.1.dist-info/entry_points.txt,sha256=bo7wO1u-PIDHNuqsTEekC56VCAmn2i2vTRcKXxqc770,158
60
- tariochbctools-1.4.1.dist-info/top_level.txt,sha256=CiA_NepCI6zDNsaORA55zmpuJFSnTvLESraIL13xiOQ,15
61
- tariochbctools-1.4.1.dist-info/RECORD,,
56
+ tariochbctools-1.5.1.dist-info/licenses/LICENSE.txt,sha256=VR2hkz3p9Sw4hSXc7S5iZTOXGeV4h-i8AO_q0zEmtkE,1074
57
+ tariochbctools-1.5.1.dist-info/METADATA,sha256=ByXtYGQYJRS1tEQFArWce0cdpm13CHQmQBH-r7yJ8zY,2180
58
+ tariochbctools-1.5.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
59
+ tariochbctools-1.5.1.dist-info/entry_points.txt,sha256=bo7wO1u-PIDHNuqsTEekC56VCAmn2i2vTRcKXxqc770,158
60
+ tariochbctools-1.5.1.dist-info/top_level.txt,sha256=CiA_NepCI6zDNsaORA55zmpuJFSnTvLESraIL13xiOQ,15
61
+ tariochbctools-1.5.1.dist-info/RECORD,,