beancount-gocardless 0.1.11__py3-none-any.whl → 0.1.13__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.
- beancount_gocardless/importer.py +49 -33
- beancount_gocardless/models.py +36 -2
- {beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/METADATA +13 -35
- {beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/RECORD +7 -7
- {beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/WHEEL +0 -0
- {beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/entry_points.txt +0 -0
- {beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/licenses/LICENSE +0 -0
beancount_gocardless/importer.py
CHANGED
|
@@ -400,32 +400,61 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
400
400
|
for b in balances.balances
|
|
401
401
|
],
|
|
402
402
|
)
|
|
403
|
-
expected_balance = None
|
|
404
|
-
other_balances = []
|
|
405
|
-
for bal in balances.balances:
|
|
406
|
-
if bal.balance_type == "expected":
|
|
407
|
-
expected_balance = bal
|
|
408
|
-
else:
|
|
409
|
-
other_balances.append(bal)
|
|
410
403
|
|
|
411
|
-
|
|
404
|
+
# Prioritized balance selection
|
|
405
|
+
PRIORITY = {
|
|
406
|
+
"expected": 0,
|
|
407
|
+
"closingBooked": 1,
|
|
408
|
+
"interimBooked": 2,
|
|
409
|
+
"interimAvailable": 3,
|
|
410
|
+
"openingBooked": 4,
|
|
411
|
+
}
|
|
412
|
+
if account.preferred_balance_type:
|
|
413
|
+
PRIORITY[account.preferred_balance_type] = -1
|
|
414
|
+
|
|
415
|
+
# Sort balances based on priority, with unknown types at the end
|
|
416
|
+
sorted_balances = sorted(
|
|
417
|
+
balances.balances, key=lambda b: PRIORITY.get(b.balance_type, 99)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if sorted_balances:
|
|
421
|
+
selected_balance = sorted_balances[0]
|
|
412
422
|
balance_amount = amount.Amount(
|
|
413
|
-
D(str(
|
|
414
|
-
|
|
423
|
+
D(str(selected_balance.balance_amount.amount)),
|
|
424
|
+
selected_balance.balance_amount.currency,
|
|
415
425
|
)
|
|
426
|
+
|
|
427
|
+
# Determine balance date
|
|
428
|
+
if selected_balance.reference_date:
|
|
429
|
+
try:
|
|
430
|
+
balance_date = date.fromisoformat(
|
|
431
|
+
selected_balance.reference_date
|
|
432
|
+
) + timedelta(days=1)
|
|
433
|
+
except ValueError:
|
|
434
|
+
balance_date = date.today() + timedelta(days=1)
|
|
435
|
+
else:
|
|
436
|
+
balance_date = date.today() + timedelta(days=1)
|
|
437
|
+
|
|
416
438
|
balance_meta = {}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
|
|
440
|
+
# Collect all distinct balance values for metadata
|
|
441
|
+
distinct_details = []
|
|
442
|
+
seen_values = set()
|
|
443
|
+
for b in sorted_balances:
|
|
444
|
+
val_str = f"{b.balance_amount.amount} {b.balance_amount.currency}"
|
|
445
|
+
if val_str not in seen_values:
|
|
446
|
+
distinct_details.append(f"{b.balance_type}: {val_str}")
|
|
447
|
+
seen_values.add(val_str)
|
|
448
|
+
|
|
449
|
+
balance_meta["detail"] = " / ".join(distinct_details)
|
|
450
|
+
|
|
423
451
|
# Include custom metadata from config for consistency with transactions
|
|
424
452
|
balance_meta.update(custom_metadata)
|
|
425
453
|
meta = data.new_metadata("", 0, balance_meta)
|
|
454
|
+
|
|
426
455
|
balance_entry = data.Balance(
|
|
427
456
|
meta=meta,
|
|
428
|
-
date=
|
|
457
|
+
date=balance_date,
|
|
429
458
|
account=asset_account,
|
|
430
459
|
amount=balance_amount,
|
|
431
460
|
tolerance=None,
|
|
@@ -433,26 +462,13 @@ class GoCardLessImporter(beangulp.Importer):
|
|
|
433
462
|
)
|
|
434
463
|
entries.append(balance_entry)
|
|
435
464
|
logger.debug(
|
|
436
|
-
"Added balance assertion for account %s using
|
|
465
|
+
"Added balance assertion for account %s using %s balance: %s %s",
|
|
437
466
|
account_id,
|
|
467
|
+
selected_balance.balance_type,
|
|
438
468
|
balance_amount,
|
|
439
|
-
|
|
469
|
+
balance_date,
|
|
440
470
|
)
|
|
441
471
|
|
|
442
|
-
# Log other balances if they differ from expected
|
|
443
|
-
expected_amount = D(str(expected_balance.balance_amount.amount))
|
|
444
|
-
for bal in other_balances:
|
|
445
|
-
other_amount = D(str(bal.balance_amount.amount))
|
|
446
|
-
if other_amount != expected_amount:
|
|
447
|
-
logger.info(
|
|
448
|
-
"Account %s has different balance for type %s: %s %s vs expected %s",
|
|
449
|
-
account_id,
|
|
450
|
-
bal.balance_type,
|
|
451
|
-
other_amount,
|
|
452
|
-
bal.balance_amount.currency,
|
|
453
|
-
expected_amount,
|
|
454
|
-
)
|
|
455
|
-
|
|
456
472
|
logger.info(
|
|
457
473
|
"Processed %d total transactions across %d accounts, created %d entries",
|
|
458
474
|
total_transactions,
|
beancount_gocardless/models.py
CHANGED
|
@@ -4,7 +4,7 @@ Complete coverage of all schemas from swagger.json
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from typing import Optional, List, Dict, Any, TypedDict
|
|
7
|
-
from pydantic import BaseModel, Field, ConfigDict,
|
|
7
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
8
8
|
from pydantic.alias_generators import to_camel
|
|
9
9
|
from enum import Enum
|
|
10
10
|
|
|
@@ -166,6 +166,8 @@ class AccountDetail(BaseModel):
|
|
|
166
166
|
class TransactionAmountSchema(BaseModel):
|
|
167
167
|
"""Transaction amount schema."""
|
|
168
168
|
|
|
169
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
170
|
+
|
|
169
171
|
amount: str
|
|
170
172
|
currency: str
|
|
171
173
|
|
|
@@ -173,6 +175,8 @@ class TransactionAmountSchema(BaseModel):
|
|
|
173
175
|
class InstructedAmount(BaseModel):
|
|
174
176
|
"""Instructed amount schema."""
|
|
175
177
|
|
|
178
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
179
|
+
|
|
176
180
|
amount: str
|
|
177
181
|
currency: str
|
|
178
182
|
|
|
@@ -180,6 +184,8 @@ class InstructedAmount(BaseModel):
|
|
|
180
184
|
class CurrencyExchangeSchema(BaseModel):
|
|
181
185
|
"""Currency exchange schema."""
|
|
182
186
|
|
|
187
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
188
|
+
|
|
183
189
|
source_currency: str
|
|
184
190
|
exchange_rate: Optional[str] = None
|
|
185
191
|
unit_currency: Optional[str] = None
|
|
@@ -192,6 +198,8 @@ class CurrencyExchangeSchema(BaseModel):
|
|
|
192
198
|
class BalanceAfterTransactionSchema(BaseModel):
|
|
193
199
|
"""Balance after transaction schema."""
|
|
194
200
|
|
|
201
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
202
|
+
|
|
195
203
|
balance_after_transaction: Optional[BalanceAmountSchema] = None
|
|
196
204
|
balance_type: Optional[str] = None
|
|
197
205
|
|
|
@@ -206,6 +214,19 @@ class TransactionSchema(BaseModel):
|
|
|
206
214
|
value_date_time: Optional[str] = Field(None, description="Value date and time.")
|
|
207
215
|
transaction_amount: TransactionAmountSchema
|
|
208
216
|
currency_exchange: Optional[List[CurrencyExchangeSchema]] = None
|
|
217
|
+
|
|
218
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
219
|
+
|
|
220
|
+
@field_validator("currency_exchange", mode="before")
|
|
221
|
+
@classmethod
|
|
222
|
+
def normalize_currency_exchange(cls, v):
|
|
223
|
+
"""Normalize currency_exchange to always be a list."""
|
|
224
|
+
if v is None:
|
|
225
|
+
return None
|
|
226
|
+
if isinstance(v, dict):
|
|
227
|
+
return [v]
|
|
228
|
+
return v
|
|
229
|
+
|
|
209
230
|
creditor_name: Optional[str] = Field(None, description="Creditor name.")
|
|
210
231
|
creditor_account: Optional[AccountSchema] = None
|
|
211
232
|
creditor_agent: Optional[str] = Field(None, description="Creditor agent.")
|
|
@@ -260,6 +281,17 @@ class BankTransaction(BaseModel):
|
|
|
260
281
|
creditor_name: Optional[str] = Field(None, description="Creditor name.")
|
|
261
282
|
creditor_account: Optional[AccountSchema] = None
|
|
262
283
|
currency_exchange: Optional[List[CurrencyExchangeSchema]] = None
|
|
284
|
+
|
|
285
|
+
@field_validator("currency_exchange", mode="before")
|
|
286
|
+
@classmethod
|
|
287
|
+
def normalize_currency_exchange(cls, v):
|
|
288
|
+
"""Normalize currency_exchange to always be a list."""
|
|
289
|
+
if v is None:
|
|
290
|
+
return None
|
|
291
|
+
if isinstance(v, dict):
|
|
292
|
+
return [v]
|
|
293
|
+
return v
|
|
294
|
+
|
|
263
295
|
balance_after_transaction: Optional[BalanceAfterTransactionSchema] = None
|
|
264
296
|
bank_transaction_code: Optional[str] = Field(
|
|
265
297
|
None, description="Bank transaction code."
|
|
@@ -543,8 +575,10 @@ class AccountConfig(BaseModel):
|
|
|
543
575
|
asset_account: str
|
|
544
576
|
metadata: Dict[str, Any] = {}
|
|
545
577
|
transaction_types: List[str] = ["booked", "pending"]
|
|
578
|
+
preferred_balance_type: Optional[str] = None
|
|
546
579
|
|
|
547
|
-
@
|
|
580
|
+
@field_validator("transaction_types")
|
|
581
|
+
@classmethod
|
|
548
582
|
def validate_transaction_types(cls, v):
|
|
549
583
|
allowed = {"booked", "pending"}
|
|
550
584
|
if not set(v).issubset(allowed):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beancount-gocardless
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
License-Expression: Unlicense
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: <4,>=3.12
|
|
@@ -52,30 +52,8 @@ You'll need to create a GoCardLess account on https://bankaccountdata.gocardless
|
|
|
52
52
|
|
|
53
53
|
### API Coverage
|
|
54
54
|
|
|
55
|
-
The GoCardless client
|
|
56
|
-
|
|
57
|
-
**🏦 Core Banking:**
|
|
58
|
-
- **Accounts**: Full account metadata, balances, details, and transactions
|
|
59
|
-
- **Institutions**: Bank/institution information and capabilities
|
|
60
|
-
- **Requisitions**: Bank link management with full lifecycle support
|
|
61
|
-
|
|
62
|
-
**📋 Agreements & Permissions:**
|
|
63
|
-
- **End User Agreements**: Complete agreement lifecycle management
|
|
64
|
-
- **Access Scopes**: Granular permission control
|
|
65
|
-
- **Reconfirmations**: Agreement renewal workflows
|
|
66
|
-
|
|
67
|
-
**🔧 Advanced Features:**
|
|
68
|
-
- **Integrations**: Institution capability discovery
|
|
69
|
-
- **Token Management**: JWT token handling (internal)
|
|
70
|
-
- **Pagination**: Full paginated response support
|
|
71
|
-
- **Error Handling**: Error response models
|
|
72
|
-
|
|
73
|
-
**📊 Rich Data Models:**
|
|
74
|
-
- **Transactions**: Complete transaction details with currency exchange, balances, and metadata
|
|
75
|
-
- **Balances**: Multi-currency balance information with transaction impact
|
|
76
|
-
- **Account Details**: Extensive account information including ownership and addresses
|
|
77
|
-
|
|
78
|
-
Models manually recreated from the swagger spec, providing type-safe access to every API feature.
|
|
55
|
+
The GoCardless client tries to provide complete API coverage with Pydantic models for all endpoints and data structures.
|
|
56
|
+
Models are manually recreated from the swagger spec, providing type-safe access to every API feature.
|
|
79
57
|
|
|
80
58
|
**Installation:**
|
|
81
59
|
|
|
@@ -98,7 +76,8 @@ cache_options: # by default, no caching if cache_options is not provided
|
|
|
98
76
|
accounts:
|
|
99
77
|
- id: <REDACTED_UUID>
|
|
100
78
|
asset_account: "Assets:Banks:Revolut:Checking"
|
|
101
|
-
transaction_types: ["booked", "pending"] # optional, defaults to both
|
|
79
|
+
transaction_types: ["booked", "pending"] # optional list, defaults to both
|
|
80
|
+
preferred_balance_type: "interimAvailable" # optional, use specific balance type
|
|
102
81
|
```
|
|
103
82
|
|
|
104
83
|
```python
|
|
@@ -107,20 +86,19 @@ accounts:
|
|
|
107
86
|
|
|
108
87
|
import beangulp
|
|
109
88
|
from beancount_gocardless import GoCardLessImporter
|
|
110
|
-
from smart_importer import
|
|
89
|
+
from smart_importer import PredictPostings, PredictPayees
|
|
111
90
|
|
|
112
91
|
importers = [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
92
|
+
GoCardLessImporter()
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
hooks = [
|
|
96
|
+
PredictPostings().hook,
|
|
97
|
+
PredictPayees().hook,
|
|
120
98
|
]
|
|
121
99
|
|
|
122
100
|
if __name__ == "__main__":
|
|
123
|
-
ingest = beangulp.Ingest(importers)
|
|
101
|
+
ingest = beangulp.Ingest(importers, hooks=hooks)
|
|
124
102
|
ingest()
|
|
125
103
|
```
|
|
126
104
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
beancount_gocardless/__init__.py,sha256=JVJivGs-o5yY8YGqTYwn1cXY7aiA76lUyc95s1Ye-j0,253
|
|
2
2
|
beancount_gocardless/cli.py,sha256=EDa01K1-E7e_Y0mHyISR4oimlo_nh5mLlE0RbhEFDAQ,4775
|
|
3
3
|
beancount_gocardless/client.py,sha256=2Sl8yeS7Lb1Y9qH8lZfGYrIVI_wkKy7ckZHG0GLE30Y,15572
|
|
4
|
-
beancount_gocardless/importer.py,sha256=
|
|
5
|
-
beancount_gocardless/models.py,sha256=
|
|
4
|
+
beancount_gocardless/importer.py,sha256=RHKvhB1VA7WHpk_Az-HXxUfFIsycz8dRvdsrKoJqAMI,17068
|
|
5
|
+
beancount_gocardless/models.py,sha256=tQyzEvqs9_7NmyCi3fZdEUxy5_rTo8VSoFsg7mupLh8,19351
|
|
6
6
|
beancount_gocardless/tui.py,sha256=Tk5AN0PeZZfXlQqm81P5Bmtdx-_whUyuKSsBv4eiF5w,26409
|
|
7
7
|
beancount_gocardless/openapi/swagger.json,sha256=t8TLbt0l2UOyorZX8JyoG4XO2qtXDRYUlsllM3ckyG4,264957
|
|
8
|
-
beancount_gocardless-0.1.
|
|
9
|
-
beancount_gocardless-0.1.
|
|
10
|
-
beancount_gocardless-0.1.
|
|
11
|
-
beancount_gocardless-0.1.
|
|
12
|
-
beancount_gocardless-0.1.
|
|
8
|
+
beancount_gocardless-0.1.13.dist-info/METADATA,sha256=6Bocz1344DLizkXbivAS_b_fRnnw29hj5DeAPPzoqX8,3229
|
|
9
|
+
beancount_gocardless-0.1.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
10
|
+
beancount_gocardless-0.1.13.dist-info/entry_points.txt,sha256=_l8n11k9nGPaSDou4atu5NkV0Ojf-qjW2zIIGPzmmgA,128
|
|
11
|
+
beancount_gocardless-0.1.13.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
12
|
+
beancount_gocardless-0.1.13.dist-info/RECORD,,
|
|
File without changes
|
{beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{beancount_gocardless-0.1.11.dist-info → beancount_gocardless-0.1.13.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|