Skip to content

Commit 8dd9dcf

Browse files
authored
Merge pull request #137 from markferry/awardwallet-v0.2
awardwallet v0.2
2 parents 3bde9cc + 56098eb commit 8dd9dcf

File tree

5 files changed

+168
-93
lines changed

5 files changed

+168
-93
lines changed

docs/importers.rst

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -447,48 +447,65 @@ Import PDF from `radicant <https://radicant.com/>`__
447447
AwardWallet
448448
------------------------------
449449

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>`__.
451452

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.
453456

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.
455459

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.
457463

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.
459466

460-
.. code-block:: console
467+
3. Link and authorize personal accounts using the included ``awardwallet-conf``
468+
CLI tool:
461469

462-
awardwallet-conf --api-key YOUR_API_KEY get_link_url
470+
.. code-block:: console
463471
472+
awardwallet-conf --api-key YOUR_API_KEY get_link_url
464473
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.
467477

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.
469481

470-
awardwallet-conf --api-key YOUR_API_KEY generate > awardwallet.yaml
482+
.. code-block:: console
471483
472-
Example configuration file:
484+
awardwallet-conf --api-key YOUR_API_KEY generate > awardwallet.yaml
473485
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.
475489

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:
486491

492+
.. code-block:: yaml
487493
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
489504
490-
.. code-block:: python
505+
5. Finally, initialize the importer:
506+
507+
.. code-block:: python
491508
492-
from tariochbctools.importers.awardwalletimp import importer as awimp
509+
from tariochbctools.importers.awardwalletimp import importer as awimp
493510
494-
CONFIG = [awimp.Importer()]
511+
CONFIG = [awimp.Importer()]

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ package_dir =
3737
# install_requires = numpy; scipy
3838
install_requires =
3939
importlib-metadata; python_version<"3.8"
40-
awardwallet
40+
awardwallet>=0.2
4141
beancount>=3
4242
beangulp
4343
beanprice

src/tariochbctools/importers/awardwalletimp/config.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import yaml
77
from awardwallet import AwardWalletClient
8-
from awardwallet.api import AccessLevel
8+
from awardwallet.client import AccessLevel
99

1010

1111
def get_link_url(client):
@@ -33,19 +33,18 @@ def generate(client):
3333
connected_users = client.list_connected_users()
3434

3535
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)
3837
account_config = {}
3938

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,
4342
"account": "Assets:Current:Points", # Placeholder, user should adjust
4443
"currency": "POINTS",
4544
}
4645

47-
config["users"][user_id] = {
48-
"name": user["userName"],
46+
config["users"][user.user_id] = {
47+
"name": user.user_name,
4948
"all_history": False,
5049
"accounts": account_config,
5150
}

src/tariochbctools/importers/awardwalletimp/importer.py

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import datetime
12
import logging
3+
import re
4+
from operator import attrgetter
25
from os import path
3-
from typing import Any
46

57
import beangulp
68
import dateutil.parser
79
import yaml
8-
from awardwallet import AwardWalletClient
10+
from awardwallet import AwardWalletClient, model
911
from beancount.core import amount, data
1012
from beancount.core.number import D
1113

@@ -46,27 +48,33 @@ def extract(self, filepath: str, existing: data.Entries = None) -> data.Entries:
4648
return entries
4749

4850
def _extract_user_history(
49-
self, user: dict, user_details: dict
51+
self, user: dict, user_details: model.GetConnectedUserDetailsResponse
5052
) -> list[data.Transaction]:
5153
"""
5254
User history is limited to the last 10 history elements per account
5355
"""
5456
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]
6161

6262
entries.extend(
6363
self._extract_transactions(
64-
account["history"], account_config, account_id
64+
account.history, account_config, account.account_id
6565
)
6666
)
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+
)
6773
else:
6874
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,
7078
)
7179
return entries
7280

@@ -76,13 +84,15 @@ def _extract_account_history(
7684
entries = []
7785
for account_id, account_config in user["accounts"].items():
7886
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
8088

8189
entries.extend(
82-
self._extract_transactions(
83-
account["history"], account_config, account_id
84-
)
90+
self._extract_transactions(account.history, account_config, account_id)
8591
)
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))
8696
return entries
8797

8898
def _extract_transactions(
@@ -109,7 +119,7 @@ def _extract_transactions(
109119

110120
def _extract_transaction(
111121
self,
112-
trx: dict[str, Any],
122+
trx: model.HistoryItem,
113123
local_account: data.Account,
114124
currency: str,
115125
account_id: str,
@@ -121,20 +131,23 @@ def _extract_transaction(
121131
trx_description = None
122132
trx_amount = None
123133

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", " ")
134144

135145
assert trx_date
136146
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 []
138151

139152
meta = data.new_metadata("", 0, metakv)
140153
entry = data.Transaction(
@@ -158,3 +171,36 @@ def _extract_transaction(
158171
)
159172
entries.append(entry)
160173
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]

0 commit comments

Comments
 (0)