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.
@@ -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
- if expected_balance:
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(expected_balance.balance_amount.amount)),
414
- expected_balance.balance_amount.currency,
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
- detail_parts = [
418
- f"{b.balance_type}: {b.balance_amount.amount} {b.balance_amount.currency}"
419
- for b in balances.balances
420
- ]
421
- detail = " / ".join(detail_parts)
422
- balance_meta["detail"] = detail
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=date.today() + timedelta(days=1),
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 expected balance: %s %s",
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
- date.today() + timedelta(days=1),
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,
@@ -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, validator
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
- @validator("transaction_types")
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.11
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 provides **complete API coverage** with Pydantic models for all endpoints and data structures:
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 apply_hooks, PredictPostings, PredictPayees
89
+ from smart_importer import PredictPostings, PredictPayees
111
90
 
112
91
  importers = [
113
- apply_hooks(
114
- GoCardLessImporter(),
115
- [
116
- PredictPostings(),
117
- PredictPayees(),
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=4sh66Vmi8eWEXcmkUIq3UJcqjyZNUitZpkp1-ZcNyCU,16688
5
- beancount_gocardless/models.py,sha256=UuuX8mj8X2_kS17Q3CeJ68J960h0Nq1-fN7kT06oiYg,18269
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.11.dist-info/METADATA,sha256=Vwqq9Mr99heobkJwihJIc8qY2rIMjFpov4mgU2wICNo,4124
9
- beancount_gocardless-0.1.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
- beancount_gocardless-0.1.11.dist-info/entry_points.txt,sha256=_l8n11k9nGPaSDou4atu5NkV0Ojf-qjW2zIIGPzmmgA,128
11
- beancount_gocardless-0.1.11.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
12
- beancount_gocardless-0.1.11.dist-info/RECORD,,
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,,