Skip to content

Commit b99342a

Browse files
authored
Transactions JSON now matches SimpleFIN output (#20)
- Include `accounts` header so a consumer of the file can identify the account. `transactions` now live under the account. - Timestamps remain in Unix timestamps
1 parent 54c3fe8 commit b99342a

File tree

4 files changed

+87
-54
lines changed

4 files changed

+87
-54
lines changed

README.md

+59-28
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,6 @@ cog.out(
131131

132132
##### JSON output
133133

134-
We convert the posted and transacted_at, if provided, values into ISO strings.
135-
136134
<!-- [[[cog
137135
import cog
138136
from simplefin import cli
@@ -145,31 +143,64 @@ cog.out(
145143
]]] -->
146144
```
147145
❯ simplefin transactions "Demo Savings" --format json
148-
[
149-
{
150-
"id": "1738382400",
151-
"posted": "2025-02-01T04:00:00+00:00",
152-
"amount": "1960.00",
153-
"description": "Pay day!",
154-
"payee": "You",
155-
"memo": "PAY DAY - FROM YER JOB"
156-
},
157-
{
158-
"id": "1738396800",
159-
"posted": "2025-02-01T08:00:00+00:00",
160-
"amount": "-05.50",
161-
"description": "Fishing bait",
162-
"payee": "John's Fishin Shack",
163-
"memo": "JOHNS FISHIN SHACK BAIT"
164-
},
165-
{
166-
"id": "1738425600",
167-
"posted": "2025-02-01T16:00:00+00:00",
168-
"amount": "-135.50",
169-
"description": "Grocery store",
170-
"payee": "Grocery store",
171-
"memo": "LOCAL GROCER STORE #1133"
172-
}
173-
]
146+
{
147+
"errors": [],
148+
"accounts": [
149+
{
150+
"org": {
151+
"domain": "beta-bridge.simplefin.org",
152+
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
153+
"name": "SimpleFIN Demo",
154+
"url": "https://beta-bridge.simplefin.org",
155+
"id": "simplefin.demoorg"
156+
},
157+
"id": "Demo Savings",
158+
"name": "SimpleFIN Savings",
159+
"currency": "USD",
160+
"balance": "115525.50",
161+
"available-balance": "115525.50",
162+
"balance-date": 1738368000,
163+
"transactions": [
164+
{
165+
"id": "1738382400",
166+
"posted": 1738382400,
167+
"amount": "1960.00",
168+
"description": "Pay day!",
169+
"payee": "You",
170+
"memo": "PAY DAY - FROM YER JOB"
171+
},
172+
{
173+
"id": "1738396800",
174+
"posted": 1738396800,
175+
"amount": "-05.50",
176+
"description": "Fishing bait",
177+
"payee": "John's Fishin Shack",
178+
"memo": "JOHNS FISHIN SHACK BAIT"
179+
},
180+
{
181+
"id": "1738425600",
182+
"posted": 1738425600,
183+
"amount": "-135.50",
184+
"description": "Grocery store",
185+
"payee": "Grocery store",
186+
"memo": "LOCAL GROCER STORE #1133"
187+
}
188+
],
189+
"holdings": [
190+
{
191+
"id": "25bc4910-4cb4-437b-9924-ee98003651c5",
192+
"created": 345427200,
193+
"cost_basis": "55.00",
194+
"currency": "USD",
195+
"description": "Shares of Apple",
196+
"market_value": "105884.8",
197+
"purchase_price": "0.10",
198+
"shares": "550.0",
199+
"symbol": "AAPL"
200+
}
201+
]
202+
}
203+
]
204+
}
174205
```
175206
<!-- [[[end]]] -->

src/simplefin/cli/__init__.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import datetime
22
import json
33
import os
4-
from datetime import date
54

5+
# from datetime import date
66
import click
77
from rich.console import Console
88
from rich.pretty import pprint
@@ -11,6 +11,10 @@
1111
from simplefin.client import SimpleFINClient
1212

1313

14+
def epoch_to_datetime(epoch: int) -> datetime:
15+
return datetime.datetime.fromtimestamp(epoch, tz=datetime.timezone.utc)
16+
17+
1418
class DateTimeEncoder(json.JSONEncoder):
1519
def default(self, obj):
1620
if isinstance(obj, datetime.datetime):
@@ -89,24 +93,30 @@ def accounts(format: str) -> None:
8993
)
9094
def transactions(account_id: str, format: str, lookback_days: int) -> None:
9195
c = SimpleFINClient(access_url=os.getenv("SIMPLEFIN_ACCESS_URL"))
92-
start_dt = date.today() - datetime.timedelta(days=lookback_days)
93-
transactions = c.get_transactions(account_id, start_dt)
96+
start_dt = datetime.date.today() - datetime.timedelta(days=lookback_days)
97+
resp = c.get_transactions(account_id, start_dt)
98+
99+
console = Console()
94100

95101
if format == "json":
96-
console = Console()
97-
console.print(json.dumps(transactions, indent=4, cls=DateTimeEncoder))
102+
console.print(json.dumps(resp, indent=4))
98103
else:
104+
if len(resp["accounts"]) == 0:
105+
console.print("No transactions found")
106+
return
107+
99108
table = Table(title=f"Transactions for {account_id}")
100109
table.add_column("Date")
101110
table.add_column("Payee")
102111
table.add_column("Amount")
103112

104-
for txn in transactions:
113+
for txn in resp["accounts"][0]["transactions"]:
105114
table.add_row(
106-
txn["posted"].strftime("%d %b %Y"), txn["payee"], str(txn["amount"])
115+
epoch_to_datetime(txn["posted"]).strftime("%d %b %Y"),
116+
txn["payee"],
117+
str(txn["amount"]),
107118
)
108119

109-
console = Console()
110120
console.print(table)
111121

112122

src/simplefin/client.py

+4-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from base64 import b64decode
3-
from datetime import date, datetime, timezone
3+
from datetime import date, datetime
44
from functools import wraps
55
from typing import Optional
66

@@ -9,10 +9,6 @@
99
logger = logging.getLogger(__name__)
1010

1111

12-
def epoch_to_datetime(epoch: int) -> datetime:
13-
return datetime.fromtimestamp(epoch, tz=timezone.utc)
14-
15-
1612
# TODO: Custom exception, if still required?
1713
def ensure_client_initialized(func):
1814
@wraps(func)
@@ -71,17 +67,11 @@ def get_transactions(self, account_id: str, start_date: date):
7167
)
7268
response.raise_for_status()
7369

74-
transactions = response.json()["accounts"][0]["transactions"]
70+
# Include errors & holdings?
7571

76-
for transaction in transactions:
77-
if "posted" in transaction.keys():
78-
transaction["posted"] = epoch_to_datetime(transaction["posted"])
79-
if "transacted_at" in transaction.keys():
80-
transaction["transacted_at"] = epoch_to_datetime(
81-
transaction["transacted_at"]
82-
)
72+
resp = response.json()
8373

84-
return transactions
74+
return resp
8575

8676
@ensure_client_initialized
8777
def get_info(self):

tests/test_simplefin_client.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ def test_get_transactions(httpx_mock: pytest_httpx.HTTPXMock):
6969
json=transactions_response,
7070
)
7171

72-
transactions = client.get_transactions(account_id, start_date)
73-
assert len(transactions) == 4
74-
assert transactions[0]["payee"] == "John's Fishin Shack"
75-
assert transactions[1]["payee"] == "Grocery store"
72+
resp = client.get_transactions(account_id, start_date)
73+
assert len(resp["accounts"]) == 1
74+
assert len(resp["accounts"][0]["transactions"]) == 4
75+
assert type(resp["accounts"][0]["transactions"][0]["posted"]) is int
76+
# assert transactions[0]["payee"] == "John's Fishin Shack"
77+
# assert transactions[1]["payee"] == "Grocery store"
7678

7779

7880
def test_get_info(httpx_mock: httpx.MockTransport):

0 commit comments

Comments
 (0)