tariochbctools 1.4.1__tar.gz → 1.5.1__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 (100) hide show
  1. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/PKG-INFO +2 -2
  2. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/importers.rst +44 -27
  3. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/setup.cfg +1 -1
  4. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/awardwalletimp/config.py +7 -8
  5. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/awardwalletimp/importer.py +73 -27
  6. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/radicant/importer.py +36 -6
  7. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/truelayer/importer.py +60 -32
  8. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/PKG-INFO +2 -2
  9. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/requires.txt +1 -1
  10. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/importers/test_awardwallet.py +43 -30
  11. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/importers/test_truelayer.py +50 -9
  12. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/.coveragerc +0 -0
  13. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/.isort.cfg +0 -0
  14. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/.pre-commit-config.yaml +0 -0
  15. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/.readthedocs.yml +0 -0
  16. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/LICENSE.txt +0 -0
  17. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/MANIFEST.in +0 -0
  18. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/README.rst +0 -0
  19. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/_static/.gitignore +0 -0
  20. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/conf.py +0 -0
  21. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/index.rst +0 -0
  22. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/installation.rst +0 -0
  23. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/license.rst +0 -0
  24. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/plugins.rst +0 -0
  25. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/price_fetchers.rst +0 -0
  26. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/docs/requirements.txt +0 -0
  27. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/pyproject.toml +0 -0
  28. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/setup.py +0 -0
  29. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/__init__.py +0 -0
  30. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/__init__.py +0 -0
  31. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/awardwalletimp/__init__.py +0 -0
  32. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/bcge/__init__.py +0 -0
  33. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/bcge/importer.py +0 -0
  34. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/bitst/__init__.py +0 -0
  35. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/bitst/importer.py +0 -0
  36. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/blockchain/__init__.py +0 -0
  37. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/blockchain/importer.py +0 -0
  38. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/cembrastatement/__init__.py +0 -0
  39. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/cembrastatement/importer.py +0 -0
  40. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/general/__init__.py +0 -0
  41. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/general/deduplication.py +0 -0
  42. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/general/mailAdapterImporter.py +0 -0
  43. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/general/mt940importer.py +0 -0
  44. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/general/priceLookup.py +0 -0
  45. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/ibkr/__init__.py +0 -0
  46. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/ibkr/importer.py +0 -0
  47. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/neon/__init__.py +0 -0
  48. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/neon/importer.py +0 -0
  49. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/netbenefits/__init__.py +0 -0
  50. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/netbenefits/importer.py +0 -0
  51. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/nordigen/__init__.py +0 -0
  52. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/nordigen/importer.py +0 -0
  53. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/nordigen/nordigen_config.py +0 -0
  54. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/postfinance/__init__.py +0 -0
  55. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/postfinance/importer.py +0 -0
  56. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/quickfile/__init__.py +0 -0
  57. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/quickfile/importer.py +0 -0
  58. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/radicant/__init__.py +0 -0
  59. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/raiffeisench/importer.py +0 -0
  60. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/revolut/__init__.py +0 -0
  61. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/revolut/importer.py +0 -0
  62. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/schedule/__init__.py +0 -0
  63. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/schedule/importer.py +0 -0
  64. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/swisscard/__init__.py +0 -0
  65. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/swisscard/importer.py +0 -0
  66. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/transferwise/__init__.py +0 -0
  67. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/transferwise/importer.py +0 -0
  68. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/truelayer/__init__.py +0 -0
  69. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/viseca/__init__.py +0 -0
  70. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/viseca/importer.py +0 -0
  71. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/zak/__init__.py +0 -0
  72. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/zak/importer.py +0 -0
  73. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/zkb/__init__.py +0 -0
  74. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/importers/zkb/importer.py +0 -0
  75. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/plugins/__init__.py +0 -0
  76. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/plugins/check_portfolio_sum.py +0 -0
  77. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/plugins/generate_base_ccy_prices.py +0 -0
  78. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/plugins/prices/__init__.py +0 -0
  79. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools/plugins/prices/ibkr.py +0 -0
  80. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/SOURCES.txt +0 -0
  81. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/dependency_links.txt +0 -0
  82. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/entry_points.txt +0 -0
  83. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/not-zip-safe +0 -0
  84. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/src/tariochbctools.egg-info/top_level.txt +0 -0
  85. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/__init__.py +0 -0
  86. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/conftest.py +0 -0
  87. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/__init__.py +0 -0
  88. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/importers/__init__.py +0 -0
  89. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/importers/test_quickfile.py +0 -0
  90. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/__init__.py +0 -0
  91. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/entry_already_exists_expected.beancount +0 -0
  92. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/entry_already_exists_input.beancount +0 -0
  93. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/issue122_expected.beancount +0 -0
  94. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/issue122_input.beancount +0 -0
  95. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/missing_fx_expected.beancount +0 -0
  96. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/missing_fx_input.beancount +0 -0
  97. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/normal_expected.beancount +0 -0
  98. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/data/generate_base_ccy_prices/normal_input.beancount +0 -0
  99. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tests/tariochbctools/plugins/test_generate_base_ccy_prices.py +0 -0
  100. {tariochbctools-1.4.1 → tariochbctools-1.5.1}/tox.ini +0 -0
@@ -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
@@ -447,48 +447,65 @@ Import PDF from `radicant <https://radicant.com/>`__
447
447
  AwardWallet
448
448
  ------------------------------
449
449
 
450
- Import from `AwardWallet <https://awardwallet.com/>`__ using their `Account Access API <https://awardwallet.com/api/account>`__.
450
+ Import from `AwardWallet <https://awardwallet.com/>`__ using their `Account
451
+ Access API <https://awardwallet.com/api/account>`__.
451
452
 
452
- As of 2025 AwardWallet integrates over 460 airline, hotel, shopping and other loyalty programmes.
453
+ As of 2025 AwardWallet integrates over 460 airline, hotel, shopping and other
454
+ loyalty programmes. Many programmes do not support retrieval of transactions,
455
+ only balances.
453
456
 
454
- Follow the instructions in the `API documentation <https://awardwallet.com/api/account#introduction>`__ to register for a free Business account and create an API key.
457
+ 1. Update your personal AwardWallet account.
458
+ The importer can only retrieve data that has already been synced to your AwardWallet account.
455
459
 
456
- The API key is restricted to the **allowed IP addresses** you specify in the Business interface API Settings.
460
+ 2. Follow the instructions in the `API documentation
461
+ <https://awardwallet.com/api/account#introduction>`__ to register for a
462
+ free Business account and create an API key.
457
463
 
458
- Link and authorize personal accounts using the included ``awardwallet-conf`` CLI tool:
464
+ The API key is restricted to the **allowed IP addresses** you specify in
465
+ the Business interface API Settings.
459
466
 
460
- .. code-block:: console
467
+ 3. Link and authorize personal accounts using the included ``awardwallet-conf``
468
+ CLI tool:
461
469
 
462
- awardwallet-conf --api-key YOUR_API_KEY get_link_url
470
+ .. code-block:: console
463
471
 
472
+ awardwallet-conf --api-key YOUR_API_KEY get_link_url
464
473
 
465
- Generate a config file for all linked users called (or ending with) ``awardwallet.yaml`` in your import location (e.g. download folder) and edit it to your needs.
466
- Note that not all providers support retrieval of transaction history.
474
+ Manage access to your personal account under `Manage Users
475
+ <https://awardwallet.com/user/connections>`__ in the personal web
476
+ interface.
467
477
 
468
- .. code-block:: console
478
+ 4. Generate a config file for all linked users ending with
479
+ ``awardwallet.yaml`` in your import location (e.g. download folder) and
480
+ edit it to your needs.
469
481
 
470
- awardwallet-conf --api-key YOUR_API_KEY generate > awardwallet.yaml
482
+ .. code-block:: console
471
483
 
472
- Example configuration file:
484
+ awardwallet-conf --api-key YOUR_API_KEY generate > awardwallet.yaml
473
485
 
474
- .. code-block:: yaml
486
+ If you have multiple accounts using the same points currency (e.g. ``AVIOS``
487
+ used by BA, Iberia, ...) create sub-accounts for each so that ``balance``
488
+ directives will work.
475
489
 
476
- api_key: YOUR_API_KEY
477
- users:
478
- 12345:
479
- name: John Smith
480
- all_history: false
481
- accounts:
482
- 7654321:
483
- provider: "British Airways Club"
484
- account: Assets:Current:Points
485
- currency: AVIOS
490
+ Example configuration file:
486
491
 
492
+ .. code-block:: yaml
487
493
 
488
- Finally, initialize the importer:
494
+ api_key: YOUR_API_KEY
495
+ users:
496
+ 12345:
497
+ name: John Smith
498
+ all_history: false # only the last 10 txns per account
499
+ accounts:
500
+ 7654321:
501
+ provider: "British Airways Club"
502
+ account: Assets:Current:Points
503
+ currency: AVIOS
489
504
 
490
- .. code-block:: python
505
+ 5. Finally, initialize the importer:
506
+
507
+ .. code-block:: python
491
508
 
492
- from tariochbctools.importers.awardwalletimp import importer as awimp
509
+ from tariochbctools.importers.awardwalletimp import importer as awimp
493
510
 
494
- CONFIG = [awimp.Importer()]
511
+ CONFIG = [awimp.Importer()]
@@ -28,7 +28,7 @@ package_dir =
28
28
  =src
29
29
  install_requires =
30
30
  importlib-metadata; python_version<"3.8"
31
- awardwallet
31
+ awardwallet>=0.2
32
32
  beancount>=3
33
33
  beangulp
34
34
  beanprice
@@ -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,4 +1,4 @@
1
- awardwallet
1
+ awardwallet>=0.2
2
2
  beancount>=3
3
3
  beangulp
4
4
  beanprice
@@ -2,6 +2,8 @@ import json
2
2
  from unittest.mock import MagicMock, patch
3
3
 
4
4
  import pytest
5
+ from awardwallet import model
6
+ from beancount.core.data import Balance
5
7
  from beancount.core.number import D
6
8
 
7
9
  from tariochbctools.importers.awardwalletimp import importer as awimp
@@ -25,6 +27,7 @@ TEST_CONFIG = b"""
25
27
  currency: VIRGINPTS
26
28
  34:
27
29
  name: User with dummy account
30
+ all_history: true
28
31
  accounts:
29
32
  0:
30
33
  """
@@ -56,6 +59,7 @@ TEST_TRX = b"""
56
59
  }
57
60
  """
58
61
 
62
+ # second account has no optional fields
59
63
  TEST_USER_DETAILS = b"""
60
64
  {
61
65
  "userId": 12,
@@ -82,8 +86,8 @@ TEST_USER_DETAILS = b"""
82
86
  "autologinUrl": "https://business.awardwallet.com/account/redirect?ID=7654321",
83
87
  "updateUrl": "https://business.awardwallet.com/account/edit/7654321?autosubmit=1",
84
88
  "editUrl": "https://business.awardwallet.com/account/edit/7654321",
85
- "balance": "146,780",
86
- "balanceRaw": 146780,
89
+ "balance": "123,456",
90
+ "balanceRaw": 123456,
87
91
  "owner": "John Smith",
88
92
  "errorCode": 1,
89
93
  "lastDetectedChange": "+750",
@@ -93,49 +97,49 @@ TEST_USER_DETAILS = b"""
93
97
  "properties": [
94
98
  {
95
99
  "name": "Next Elite Level",
96
- "value": {"value": "Bronze", "type": "string"},
100
+ "value": "Bronze",
97
101
  "kind": 9
98
102
  },
99
103
  {
100
104
  "name": "Date of joining the club",
101
- "value": {"value": "20 Jun 2013", "type": "string"},
105
+ "value": "20 Jun 2013",
102
106
  "kind": 5
103
107
  },
104
108
  {
105
109
  "name": "Lifetime Tier Points",
106
- "value": {"value": "35,000", "type": "string"}
110
+ "value": "35,000"
107
111
  },
108
112
  {
109
113
  "name": "Executive Club Tier Points",
110
- "value": {"value": "35,000", "type": "string"}
114
+ "value": "35,000"
111
115
  },
112
116
  {
113
117
  "name": "Card expiry date",
114
- "value": {"value": "31 Mar 2017", "type": "string"}
118
+ "value": "31 Mar 2017"
115
119
  },
116
120
  {
117
121
  "name": "Membership year ends",
118
- "value": {"value": "08 Feb 2016", "type": "string"}
122
+ "value": "08 Feb 2016"
119
123
  },
120
124
  {
121
125
  "name": "Last Activity",
122
- "value": {"value": "10-Dec-15", "type": "string"},
126
+ "value": "10-Dec-15",
123
127
  "kind": 13
124
128
  },
125
129
  {
126
130
  "name": "Name",
127
- "value": {"value": "Mr Smith", "type": "string"},
131
+ "value": "Mr Smith",
128
132
  "kind": 12
129
133
  },
130
134
  {
131
135
  "name": "Level",
132
- "value": {"value": "Blue", "type": "string"},
136
+ "value": "Blue",
133
137
  "rank": 0,
134
138
  "kind": 3
135
139
  },
136
140
  {
137
141
  "name": "Membership no",
138
- "value": {"value": "1122334455", "type": "string"},
142
+ "value": "1122334455",
139
143
  "kind": 1
140
144
  }
141
145
  ],
@@ -219,18 +223,14 @@ TEST_USER_DETAILS = b"""
219
223
  "code": "virgin",
220
224
  "displayName": "Virgin Atlantic (Flying Club)",
221
225
  "kind": "Airlines",
222
- "balance": "146,780",
223
- "balanceRaw": 146780,
226
+ "login": "johnsmith",
227
+ "autologinUrl": "https://business.awardwallet.com/account/redirect?ID=7654321",
228
+ "updateUrl": "https://business.awardwallet.com/account/edit/7654321?autosubmit=1",
229
+ "editUrl": "https://business.awardwallet.com/account/edit/7654321",
230
+ "balance": "24,680",
231
+ "balanceRaw": 24680,
224
232
  "owner": "John Smith",
225
233
  "errorCode": 1,
226
- "lastDetectedChange": "+750",
227
- "properties": [
228
- {
229
- "name": "Next Elite Level",
230
- "value": {"value": "Bronze", "type": "string"},
231
- "kind": 9
232
- }
233
- ],
234
234
  "history": [
235
235
  ]
236
236
  }
@@ -271,12 +271,14 @@ def awardwallet_importer_factory(tmp_path):
271
271
 
272
272
  @pytest.fixture(name="tmp_trx")
273
273
  def tmp_trx_fixture():
274
- yield json.loads(TEST_TRX)
274
+ j = json.loads(TEST_TRX, strict=False)
275
+ yield model.HistoryItem(**j)
275
276
 
276
277
 
277
278
  @pytest.fixture(name="tmp_user_details")
278
279
  def tmp_user_details_fixture():
279
- yield json.loads(TEST_USER_DETAILS, strict=False)
280
+ j = json.loads(TEST_USER_DETAILS, strict=False)
281
+ yield model.GetConnectedUserDetailsResponse(**j)
280
282
 
281
283
 
282
284
  def test_identify(importer, tmp_config):
@@ -285,9 +287,7 @@ def test_identify(importer, tmp_config):
285
287
 
286
288
  def test_extract_transaction_simple(importer, tmp_trx):
287
289
  entries = importer._extract_transaction(tmp_trx, "Assets:Other", "POINTS", 7654321)
288
- assert entries[0].postings[0].units.number == D(
289
- tmp_trx["fields"][3]["value"]["value"]
290
- )
290
+ assert entries[0].postings[0].units.number == D(tmp_trx.fields[3].value.value)
291
291
 
292
292
 
293
293
  def test_extract_user_history(importer, tmp_user_details):
@@ -295,21 +295,34 @@ def test_extract_user_history(importer, tmp_user_details):
295
295
  importer.config["users"][12],
296
296
  tmp_user_details,
297
297
  )
298
- assert len(entries) == 3
298
+
299
+ assert len(entries) == 5 # three txns, two balances
300
+
301
+ balances = [e for e in entries if isinstance(e, Balance)]
302
+ assert len(balances) == 2
303
+ assert balances[0].amount.number == D("123456")
304
+ assert balances[0].amount.currency == "AVIOS"
305
+ assert balances[1].amount.number == D("24680")
306
+ assert balances[1].amount.currency == "VIRGINPTS"
299
307
 
300
308
 
301
309
  @patch("tariochbctools.importers.awardwalletimp.importer.AwardWalletClient")
302
310
  def test_extract_all_users(mock_api, importer, tmp_config, tmp_user_details):
303
311
  importer._extract_user_history = MagicMock()
312
+ importer._extract_account_history = MagicMock()
304
313
 
305
314
  importer.extract(tmp_config)
306
- assert importer._extract_user_history.call_count == 2
315
+ assert importer._extract_user_history.call_count == 1
316
+ assert importer._extract_account_history.call_count == 1
307
317
 
308
318
 
309
319
  @patch("tariochbctools.importers.awardwalletimp.importer.AwardWalletClient")
310
320
  def test_extract_all_accounts(mock_api, importer, tmp_config, tmp_user_details):
311
321
  importer._extract_transactions = MagicMock()
322
+ importer._extract_balance = MagicMock()
312
323
  mock_api.return_value.get_connected_user_details.return_value = tmp_user_details
324
+ # get_account_details mock (for user 34) returns zero txns
313
325
 
314
326
  importer.extract(tmp_config)
315
- assert importer._extract_transactions.call_count == 2
327
+ assert importer._extract_transactions.call_count == 3
328
+ assert importer._extract_balance.call_count == 3
@@ -1,5 +1,7 @@
1
1
  import json
2
+ from datetime import timedelta
2
3
 
4
+ import dateutil.parser
3
5
  import pytest
4
6
  import yaml
5
7
  from beancount.core.amount import Decimal as D
@@ -67,6 +69,28 @@ TEST_TRX_WITHOUT_IDS = b"""
67
69
  }
68
70
  """
69
71
 
72
+ TEST_BALANCE_SIMPLE = b"""
73
+ {
74
+ "currency": "GBP",
75
+ "current": 20.0,
76
+ "update_timestamp": "2017-02-24T17:29:24.740Z"
77
+ }
78
+ """
79
+
80
+ TEST_BALANCE_OPTIONAL = b"""
81
+ {
82
+ "available": 3279.0,
83
+ "currency": "GBP",
84
+ "current": 20.0,
85
+ "credit_limit": 3300,
86
+ "last_statement_balance": 226,
87
+ "last_statement_date": "2017-01-28",
88
+ "payment_due": 5.0,
89
+ "payment_due_date": "2017-02-24",
90
+ "update_timestamp": "2017-02-24T17:29:24.740Z"
91
+ }
92
+ """
93
+
70
94
 
71
95
  @pytest.fixture(name="tmp_config")
72
96
  def tmp_config_fixture(tmp_path):
@@ -114,15 +138,6 @@ def test_extract_transaction_simple(importer, tmp_trx):
114
138
  assert entries[0].postings[0].units.number == D(str(tmp_trx["amount"]))
115
139
 
116
140
 
117
- def test_extract_transaction_with_balance(importer, tmp_trx):
118
- entries = importer._extract_transaction(
119
- tmp_trx, "Assets:Other", [tmp_trx], invert_sign=False
120
- )
121
- # one entry, one balance
122
- assert len(entries) == 2
123
- assert entries[1].amount.number == D(str(tmp_trx["running_balance"]["amount"]))
124
-
125
-
126
141
  def test_extract_transaction_invert_sign(importer, tmp_trx):
127
142
  """Show that sign inversion works"""
128
143
  entries = importer._extract_transaction(
@@ -131,6 +146,32 @@ def test_extract_transaction_invert_sign(importer, tmp_trx):
131
146
  assert entries[0].postings[0].units.number == -D(str(tmp_trx["amount"]))
132
147
 
133
148
 
149
+ def test_extract_balance(importer, tmp_trx):
150
+ tmp_balance = json.loads(TEST_BALANCE_SIMPLE)
151
+
152
+ entries = importer._extract_balance(tmp_balance, "Assets:Other", invert_sign=False)
153
+
154
+ assert len(entries) == 1
155
+ assert entries[-1].amount.number == D(str(tmp_balance["current"]))
156
+ assert (
157
+ entries[-1].date - timedelta(days=1)
158
+ == dateutil.parser.parse(tmp_balance["update_timestamp"]).date()
159
+ ), "balance date should be one day after update_timestamp"
160
+
161
+
162
+ def test_extract_statement_balance(importer, tmp_trx):
163
+ tmp_balance = json.loads(TEST_BALANCE_OPTIONAL)
164
+
165
+ entries = importer._extract_balance(tmp_balance, "Assets:Other", invert_sign=False)
166
+
167
+ assert len(entries) == 2
168
+ assert entries[-1].amount.number == D(str(tmp_balance["last_statement_balance"]))
169
+ assert (
170
+ entries[-1].date
171
+ == dateutil.parser.parse(tmp_balance["last_statement_date"]).date()
172
+ )
173
+
174
+
134
175
  @pytest.mark.parametrize("id_field", tlimp.TX_MANDATORY_ID_FIELDS)
135
176
  def test_extract_transaction_has_transaction_id(importer, tmp_trx, id_field):
136
177
  """Ensure mandatory IDs are in extracted transactions."""
File without changes
File without changes