bankstatementparser-loader-bai2 0.0.10__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.
- bankstatementparser_loader_bai2/__init__.py +32 -0
- bankstatementparser_loader_bai2/loader.py +440 -0
- bankstatementparser_loader_bai2-0.0.10.dist-info/METADATA +310 -0
- bankstatementparser_loader_bai2-0.0.10.dist-info/RECORD +6 -0
- bankstatementparser_loader_bai2-0.0.10.dist-info/WHEEL +4 -0
- bankstatementparser_loader_bai2-0.0.10.dist-info/licenses/LICENSE +189 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau. All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""BAI2 -> bankstatementparser ``Transaction`` loader.
|
|
5
|
+
|
|
6
|
+
BAI2 (Bank Administration Institute, version 2) is the US cash-management
|
|
7
|
+
file format banks ship for balance and transaction reporting. The
|
|
8
|
+
`bankstatementparser <https://pypi.org/project/bankstatementparser/>`_
|
|
9
|
+
library does not parse BAI2; this companion loader turns a BAI2 payload
|
|
10
|
+
into a flat list of
|
|
11
|
+
:class:`bankstatementparser.transaction_models.Transaction` objects.
|
|
12
|
+
|
|
13
|
+
See :mod:`bankstatementparser_loader_bai2.loader` for the documented
|
|
14
|
+
record subset, amount handling, and debit / credit sign convention.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from bankstatementparser_loader_bai2.loader import (
|
|
18
|
+
Bai2Summary,
|
|
19
|
+
load_bai2,
|
|
20
|
+
load_bai2_file,
|
|
21
|
+
summarize_bai2,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.0.10"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"load_bai2",
|
|
28
|
+
"load_bai2_file",
|
|
29
|
+
"summarize_bai2",
|
|
30
|
+
"Bai2Summary",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (C) 2023-2026 Sebastien Rousseau. All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""BAI2 -> bankstatementparser ``Transaction`` loader.
|
|
5
|
+
|
|
6
|
+
`BAI2 <https://www.bai.org/>`_ (Bank Administration Institute, version 2)
|
|
7
|
+
is the de-facto US cash-management file format that banks ship for
|
|
8
|
+
intraday and prior-day balance reporting. The published
|
|
9
|
+
`bankstatementparser <https://pypi.org/project/bankstatementparser/>`_
|
|
10
|
+
library does **not** parse BAI2; this companion loader fills that gap by
|
|
11
|
+
turning a BAI2 payload into a flat list of
|
|
12
|
+
:class:`bankstatementparser.transaction_models.Transaction` objects that
|
|
13
|
+
downstream deterministic logic can consume.
|
|
14
|
+
|
|
15
|
+
Supported (pragmatic) subset
|
|
16
|
+
----------------------------
|
|
17
|
+
|
|
18
|
+
BAI2 files are line-oriented. Each physical record is a sequence of
|
|
19
|
+
comma-delimited fields and ends with a ``/`` record delimiter. Every
|
|
20
|
+
record begins with a numeric *type code* identifying the record kind.
|
|
21
|
+
This loader implements the following records:
|
|
22
|
+
|
|
23
|
+
``01`` File Header
|
|
24
|
+
``01,senderId,receiverId,fileDate,fileTime,fileId,...`` -- the file
|
|
25
|
+
must start with this record. ``fileId`` is captured for the summary.
|
|
26
|
+
|
|
27
|
+
``02`` Group Header
|
|
28
|
+
``02,ultimateReceiver,originator,groupStatus,asOfDate,asOfTime,currency,...``
|
|
29
|
+
-- the group ``currency`` and ``asOfDate`` are captured. The as-of
|
|
30
|
+
date becomes each transaction's ``booking_date``; the group
|
|
31
|
+
currency is the fallback currency for accounts that omit one.
|
|
32
|
+
|
|
33
|
+
``03`` Account Identifier
|
|
34
|
+
``03,accountNumber,currencyCode,typeCode,amount,itemCount,fundsType,...``
|
|
35
|
+
-- ``accountNumber`` and the optional account ``currencyCode`` are
|
|
36
|
+
captured. The account currency, when present, overrides the group
|
|
37
|
+
currency for every transaction under this account.
|
|
38
|
+
|
|
39
|
+
``16`` Transaction Detail
|
|
40
|
+
``16,typeCode,amount,fundsType,bankRefNum,customerRefNum,text`` --
|
|
41
|
+
one transaction. ``amount`` is an integer in the account currency's
|
|
42
|
+
minor units (see "Amounts" below). ``text`` becomes the
|
|
43
|
+
description.
|
|
44
|
+
|
|
45
|
+
``88`` Continuation
|
|
46
|
+
Continues the text of the immediately preceding ``03`` or ``16``
|
|
47
|
+
record; its content is appended to that record's description.
|
|
48
|
+
|
|
49
|
+
``49`` Account Trailer, ``98`` Group Trailer, ``99`` File Trailer
|
|
50
|
+
Control-total records. This loader **ignores** them -- it does not
|
|
51
|
+
validate the control sums. Ignoring is a deliberate, documented
|
|
52
|
+
choice: the goal is faithful transaction extraction, not file-level
|
|
53
|
+
reconciliation.
|
|
54
|
+
|
|
55
|
+
Any other (or unknown) leading type code is ignored so that vendor
|
|
56
|
+
extensions do not abort the parse.
|
|
57
|
+
|
|
58
|
+
Amounts
|
|
59
|
+
-------
|
|
60
|
+
|
|
61
|
+
BAI2 amounts are unsigned integers expressed in the account currency's
|
|
62
|
+
**minor units** (e.g. cents), with no decimal point. They are converted
|
|
63
|
+
to :class:`decimal.Decimal` by dividing by 100 -- ``Decimal`` is used
|
|
64
|
+
throughout (never ``float``) to avoid binary rounding error. An empty
|
|
65
|
+
amount field is treated as ``0``.
|
|
66
|
+
|
|
67
|
+
Sign convention (debit / credit)
|
|
68
|
+
---------------------------------
|
|
69
|
+
|
|
70
|
+
BAI2 transaction *type codes* encode the direction of funds. This loader
|
|
71
|
+
applies the following documented convention based on the numeric range
|
|
72
|
+
of the ``16`` record's type code:
|
|
73
|
+
|
|
74
|
+
* ``100``-``399`` -> **credit** (amount kept **positive**)
|
|
75
|
+
* ``400``-``699`` -> **debit** (amount made **negative**)
|
|
76
|
+
* anything else -> kept **positive**; the raw type code is preserved.
|
|
77
|
+
|
|
78
|
+
The raw BAI2 type code is always preserved on the resulting
|
|
79
|
+
``Transaction`` in both the ``category`` field (as ``bai2:<code>``) and
|
|
80
|
+
the ``reference`` field, so no information is lost even for codes outside
|
|
81
|
+
the two ranges above.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
from collections.abc import Iterator
|
|
87
|
+
from dataclasses import dataclass
|
|
88
|
+
from datetime import date, datetime
|
|
89
|
+
from decimal import Decimal
|
|
90
|
+
from pathlib import Path
|
|
91
|
+
|
|
92
|
+
from bankstatementparser.transaction_models import Transaction
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"load_bai2",
|
|
96
|
+
"load_bai2_file",
|
|
97
|
+
"summarize_bai2",
|
|
98
|
+
"Bai2Summary",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# ─── Sign-convention boundaries ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
# Inclusive lower/upper bounds for the credit and debit type-code ranges.
|
|
104
|
+
# Documented in the module docstring; kept here as the single source of
|
|
105
|
+
# truth so the loader and the tests agree.
|
|
106
|
+
_CREDIT_RANGE = range(100, 400) # 100-399 -> credit (positive)
|
|
107
|
+
_DEBIT_RANGE = range(400, 700) # 400-699 -> debit (negative)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ─── Record tokeniser ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _iter_records(text: str) -> Iterator[list[str]]:
|
|
114
|
+
"""Yield each BAI2 record as a list of its comma-delimited fields.
|
|
115
|
+
|
|
116
|
+
Tolerates CRLF / LF line endings, blank lines, and an optional
|
|
117
|
+
trailing ``/`` record delimiter. The trailing ``/`` (and anything a
|
|
118
|
+
bank may append after it) is stripped before the fields are split.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
text: The raw BAI2 payload.
|
|
122
|
+
|
|
123
|
+
Yields:
|
|
124
|
+
One ``list[str]`` of fields per non-empty record, in file order.
|
|
125
|
+
"""
|
|
126
|
+
for raw_line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
127
|
+
line = raw_line.strip()
|
|
128
|
+
if not line:
|
|
129
|
+
continue
|
|
130
|
+
# A record ends with the '/' delimiter; drop it (and any
|
|
131
|
+
# trailing remainder) so the final field is clean.
|
|
132
|
+
if "/" in line:
|
|
133
|
+
line = line[: line.index("/")]
|
|
134
|
+
line = line.rstrip()
|
|
135
|
+
if not line:
|
|
136
|
+
continue
|
|
137
|
+
yield [field.strip() for field in line.split(",")]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ─── Field helpers ───────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _field(fields: list[str], index: int) -> str:
|
|
144
|
+
"""Return the field at ``index`` or an empty string if absent.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
fields: The split fields of one record.
|
|
148
|
+
index: Zero-based field position.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The trimmed field value, or ``""`` when the position is missing.
|
|
152
|
+
"""
|
|
153
|
+
if 0 <= index < len(fields):
|
|
154
|
+
return fields[index]
|
|
155
|
+
return ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _amount_to_decimal(raw: str) -> Decimal:
|
|
159
|
+
"""Convert a BAI2 minor-unit integer amount to a major-unit Decimal.
|
|
160
|
+
|
|
161
|
+
BAI2 amounts are unsigned integers in the currency's minor units
|
|
162
|
+
(cents) with no decimal point. An empty field is treated as ``0``.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
raw: The raw amount field (e.g. ``"150000"`` for 1500.00).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The amount as a :class:`decimal.Decimal` in major units.
|
|
169
|
+
"""
|
|
170
|
+
text = raw.strip()
|
|
171
|
+
if not text:
|
|
172
|
+
return Decimal("0")
|
|
173
|
+
return Decimal(text) / Decimal(100)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _signed_amount(type_code: str, magnitude: Decimal) -> Decimal:
|
|
177
|
+
"""Apply the documented sign convention to a transaction magnitude.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
type_code: The raw BAI2 ``16`` record type code.
|
|
181
|
+
magnitude: The non-negative amount in major units.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The magnitude negated for debit type codes (``400``-``699``),
|
|
185
|
+
otherwise returned unchanged (credits and unknown codes stay
|
|
186
|
+
positive).
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
code = int(type_code)
|
|
190
|
+
except ValueError:
|
|
191
|
+
return magnitude
|
|
192
|
+
if code in _DEBIT_RANGE:
|
|
193
|
+
return -magnitude
|
|
194
|
+
return magnitude
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _parse_bai2_date(raw: str) -> date | None:
|
|
198
|
+
"""Parse a BAI2 ``YYMMDD`` date into a :class:`datetime.date`.
|
|
199
|
+
|
|
200
|
+
Years are interpreted with a sliding window matching industry
|
|
201
|
+
practice: ``00``-``79`` -> ``20YY``, ``80``-``99`` -> ``19YY``. An
|
|
202
|
+
empty or malformed value yields ``None`` rather than raising, so a
|
|
203
|
+
missing as-of date never aborts a parse.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
raw: The raw 6-digit date field.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The parsed date, or ``None`` when absent or unparseable.
|
|
210
|
+
"""
|
|
211
|
+
text = raw.strip()
|
|
212
|
+
if len(text) != 6 or not text.isdigit():
|
|
213
|
+
return None
|
|
214
|
+
year = int(text[0:2])
|
|
215
|
+
century = 2000 if year < 80 else 1900
|
|
216
|
+
try:
|
|
217
|
+
return datetime.strptime(
|
|
218
|
+
f"{century + year:04d}{text[2:4]}{text[4:6]}", "%Y%m%d"
|
|
219
|
+
).date()
|
|
220
|
+
except ValueError: # pragma: no cover - guarded by the isdigit check
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ─── Working state ───────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class _PendingTransaction:
|
|
229
|
+
"""Mutable accumulator for one ``16`` record and its continuations."""
|
|
230
|
+
|
|
231
|
+
type_code: str
|
|
232
|
+
amount: Decimal
|
|
233
|
+
bank_ref: str
|
|
234
|
+
customer_ref: str
|
|
235
|
+
text_parts: list[str]
|
|
236
|
+
account_number: str | None
|
|
237
|
+
currency: str | None
|
|
238
|
+
booking_date: date | None
|
|
239
|
+
index: int
|
|
240
|
+
|
|
241
|
+
def to_transaction(self) -> Transaction:
|
|
242
|
+
"""Materialise the accumulated state into a ``Transaction``.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
A frozen :class:`~bankstatementparser.transaction_models.Transaction`
|
|
246
|
+
with the BAI2 sign convention applied and the raw type code
|
|
247
|
+
preserved in both ``category`` and ``reference``.
|
|
248
|
+
"""
|
|
249
|
+
description = " ".join(
|
|
250
|
+
part for part in self.text_parts if part
|
|
251
|
+
).strip()
|
|
252
|
+
transaction_id = self.bank_ref or self.customer_ref or None
|
|
253
|
+
return Transaction(
|
|
254
|
+
account_id=self.account_number,
|
|
255
|
+
currency=self.currency,
|
|
256
|
+
amount=_signed_amount(self.type_code, self.amount),
|
|
257
|
+
booking_date=self.booking_date,
|
|
258
|
+
description=description or None,
|
|
259
|
+
reference=self.type_code or None,
|
|
260
|
+
transaction_id=transaction_id,
|
|
261
|
+
category=f"bai2:{self.type_code}" if self.type_code else None,
|
|
262
|
+
source="bai2",
|
|
263
|
+
source_index=self.index,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ─── Summary model ───────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class Bai2Summary:
|
|
272
|
+
"""High-level counts and identifiers for a parsed BAI2 file.
|
|
273
|
+
|
|
274
|
+
Attributes:
|
|
275
|
+
file_id: The ``fileId`` field from the ``01`` File Header.
|
|
276
|
+
group_count: Number of ``02`` Group Header records.
|
|
277
|
+
account_count: Number of ``03`` Account Identifier records.
|
|
278
|
+
transaction_count: Number of ``16`` Transaction Detail records.
|
|
279
|
+
currency: The first currency seen (account currency preferred,
|
|
280
|
+
otherwise the group currency), or ``None`` if none was given.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
file_id: str | None
|
|
284
|
+
group_count: int
|
|
285
|
+
account_count: int
|
|
286
|
+
transaction_count: int
|
|
287
|
+
currency: str | None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ─── Public API ──────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def load_bai2(text: str) -> list[Transaction]:
|
|
294
|
+
"""Parse a BAI2 payload into a flat list of ``Transaction`` objects.
|
|
295
|
+
|
|
296
|
+
Every ``16`` Transaction Detail record across all groups and
|
|
297
|
+
accounts becomes one transaction, carrying its account number and
|
|
298
|
+
currency. ``88`` continuation records extend the description of the
|
|
299
|
+
preceding ``03`` or ``16`` record.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
text: The raw BAI2 payload. CRLF / LF endings, blank lines, and
|
|
303
|
+
an optional trailing ``/`` per record are all tolerated.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
The parsed transactions in file order. May be empty if the file
|
|
307
|
+
contains headers but no ``16`` records.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
ValueError: If the file does not start with an ``01`` File
|
|
311
|
+
Header record.
|
|
312
|
+
"""
|
|
313
|
+
records = list(_iter_records(text))
|
|
314
|
+
if not records or _field(records[0], 0) != "01":
|
|
315
|
+
raise ValueError("BAI2 payload must start with an '01' File Header")
|
|
316
|
+
|
|
317
|
+
transactions: list[Transaction] = []
|
|
318
|
+
pending: _PendingTransaction | None = None
|
|
319
|
+
# Continuation target: 0 = none, 3 = last account note, 16 = pending tx.
|
|
320
|
+
continuation_target = 0
|
|
321
|
+
group_currency: str | None = None
|
|
322
|
+
group_as_of: date | None = None
|
|
323
|
+
account_number: str | None = None
|
|
324
|
+
account_currency: str | None = None
|
|
325
|
+
|
|
326
|
+
def _flush() -> None:
|
|
327
|
+
"""Append any in-progress transaction to the output list."""
|
|
328
|
+
nonlocal pending
|
|
329
|
+
if pending is not None:
|
|
330
|
+
transactions.append(pending.to_transaction())
|
|
331
|
+
pending = None
|
|
332
|
+
|
|
333
|
+
for fields in records:
|
|
334
|
+
code = _field(fields, 0)
|
|
335
|
+
if code == "02":
|
|
336
|
+
_flush()
|
|
337
|
+
continuation_target = 0
|
|
338
|
+
group_as_of = _parse_bai2_date(_field(fields, 4))
|
|
339
|
+
group_currency = _field(fields, 6) or None
|
|
340
|
+
account_number = None
|
|
341
|
+
account_currency = None
|
|
342
|
+
elif code == "03":
|
|
343
|
+
_flush()
|
|
344
|
+
continuation_target = 3
|
|
345
|
+
account_number = _field(fields, 1) or None
|
|
346
|
+
account_currency = _field(fields, 2) or None
|
|
347
|
+
elif code == "16":
|
|
348
|
+
_flush()
|
|
349
|
+
continuation_target = 16
|
|
350
|
+
pending = _PendingTransaction(
|
|
351
|
+
type_code=_field(fields, 1),
|
|
352
|
+
amount=_amount_to_decimal(_field(fields, 2)),
|
|
353
|
+
bank_ref=_field(fields, 4),
|
|
354
|
+
customer_ref=_field(fields, 5),
|
|
355
|
+
text_parts=[_field(fields, 6)],
|
|
356
|
+
account_number=account_number,
|
|
357
|
+
currency=account_currency or group_currency,
|
|
358
|
+
booking_date=group_as_of,
|
|
359
|
+
index=len(transactions),
|
|
360
|
+
)
|
|
361
|
+
elif code == "88":
|
|
362
|
+
# Continuation text is every field after the leading '88'.
|
|
363
|
+
note = ",".join(fields[1:]).strip()
|
|
364
|
+
if continuation_target == 16 and pending is not None:
|
|
365
|
+
pending.text_parts.append(note)
|
|
366
|
+
# A continuation of an '03' account note has no transaction
|
|
367
|
+
# to attach to yet; it is informational and dropped here.
|
|
368
|
+
elif code in {"49", "98", "99"}:
|
|
369
|
+
# Trailer / control-total records are intentionally ignored.
|
|
370
|
+
_flush()
|
|
371
|
+
continuation_target = 0
|
|
372
|
+
# 01 and any unknown code: nothing to accumulate.
|
|
373
|
+
|
|
374
|
+
_flush()
|
|
375
|
+
return transactions
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def load_bai2_file(path: str | Path) -> list[Transaction]:
|
|
379
|
+
"""Parse a BAI2 file from disk into ``Transaction`` objects.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
path: Filesystem path to the BAI2 file. UTF-8 is assumed.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
The parsed transactions, identical to calling :func:`load_bai2`
|
|
386
|
+
on the file's text content.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If the file does not start with an ``01`` record.
|
|
390
|
+
OSError: If the file cannot be read.
|
|
391
|
+
"""
|
|
392
|
+
return load_bai2(Path(path).read_text(encoding="utf-8"))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def summarize_bai2(text: str) -> Bai2Summary:
|
|
396
|
+
"""Summarise a BAI2 payload without materialising every transaction.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
text: The raw BAI2 payload.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
A :class:`Bai2Summary` with the file id, group / account /
|
|
403
|
+
transaction counts, and the first currency observed.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ValueError: If the file does not start with an ``01`` record.
|
|
407
|
+
"""
|
|
408
|
+
records = list(_iter_records(text))
|
|
409
|
+
if not records or _field(records[0], 0) != "01":
|
|
410
|
+
raise ValueError("BAI2 payload must start with an '01' File Header")
|
|
411
|
+
|
|
412
|
+
file_id = _field(records[0], 5) or None
|
|
413
|
+
group_count = 0
|
|
414
|
+
account_count = 0
|
|
415
|
+
transaction_count = 0
|
|
416
|
+
currency: str | None = None
|
|
417
|
+
group_currency: str | None = None
|
|
418
|
+
|
|
419
|
+
for fields in records:
|
|
420
|
+
code = _field(fields, 0)
|
|
421
|
+
if code == "02":
|
|
422
|
+
group_count += 1
|
|
423
|
+
group_currency = _field(fields, 6) or None
|
|
424
|
+
if currency is None and group_currency is not None:
|
|
425
|
+
currency = group_currency
|
|
426
|
+
elif code == "03":
|
|
427
|
+
account_count += 1
|
|
428
|
+
account_currency = _field(fields, 2) or None
|
|
429
|
+
if account_currency is not None:
|
|
430
|
+
currency = account_currency
|
|
431
|
+
elif code == "16":
|
|
432
|
+
transaction_count += 1
|
|
433
|
+
|
|
434
|
+
return Bai2Summary(
|
|
435
|
+
file_id=file_id,
|
|
436
|
+
group_count=group_count,
|
|
437
|
+
account_count=account_count,
|
|
438
|
+
transaction_count=transaction_count,
|
|
439
|
+
currency=currency,
|
|
440
|
+
)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bankstatementparser-loader-bai2
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: BAI2 (Bank Administration Institute v2) cash-management loader that parses BAI2 files into bankstatementparser Transaction objects.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: bai2,bank,statement,cash-management,transactions
|
|
8
|
+
Author: Sebastien Rousseau
|
|
9
|
+
Author-email: sebastian.rousseau@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: bankstatementparser (>=0.0.9)
|
|
19
|
+
Project-URL: Homepage, https://bankstatementparser.com
|
|
20
|
+
Project-URL: Repository, https://github.com/sebastienrousseau/bankstatementparser-loader-bai2
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img
|
|
27
|
+
src="https://cloudcdn.pro/bankstatementparser/v1/logos/bankstatementparser.svg"
|
|
28
|
+
alt="bankstatementparser-loader-bai2 logo"
|
|
29
|
+
width="120"
|
|
30
|
+
height="120"
|
|
31
|
+
/>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<h1 align="center">bankstatementparser-loader-bai2</h1>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<b>A BAI2 (Bank Administration Institute, version 2) cash-management loader that parses BAI2 files into <code>bankstatementparser</code> <code>Transaction</code> objects.</b>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<a href="https://pypi.org/project/bankstatementparser-loader-bai2/"><img src="https://img.shields.io/pypi/v/bankstatementparser-loader-bai2?style=for-the-badge" alt="PyPI version" /></a>
|
|
42
|
+
<a href="https://pypi.org/project/bankstatementparser-loader-bai2/"><img src="https://img.shields.io/pypi/pyversions/bankstatementparser-loader-bai2.svg?style=for-the-badge" alt="Python versions" /></a>
|
|
43
|
+
<a href="https://pypi.org/project/bankstatementparser-loader-bai2/"><img src="https://img.shields.io/pypi/dm/bankstatementparser-loader-bai2.svg?style=for-the-badge" alt="PyPI downloads" /></a>
|
|
44
|
+
<a href="https://github.com/sebastienrousseau/bankstatementparser-loader-bai2/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/sebastienrousseau/bankstatementparser-loader-bai2/ci.yml?branch=main&label=Tests&style=for-the-badge" alt="Tests" /></a>
|
|
45
|
+
<a href="#license"><img src="https://img.shields.io/pypi/l/bankstatementparser-loader-bai2?style=for-the-badge" alt="License" /></a>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Contents
|
|
51
|
+
|
|
52
|
+
- [What is bankstatementparser-loader-bai2?](#what-is-bankstatementparser-loader-bai2) — the problem it solves
|
|
53
|
+
- [Install](#install) — PyPI, virtualenv
|
|
54
|
+
- [Quick start](#quick-start) — parse a file in three lines
|
|
55
|
+
- [Public API](#public-api) — `load_bai2`, `load_bai2_file`, `summarize_bai2`
|
|
56
|
+
- [Supported BAI2 subset](#supported-bai2-subset) — exactly which records are handled
|
|
57
|
+
- [Amount and sign convention](#amount-and-sign-convention) — how cents and debit/credit map
|
|
58
|
+
- [When not to use this loader](#when-not-to-use-this-loader) — honest boundaries
|
|
59
|
+
- [Development](#development) — gates, make targets
|
|
60
|
+
- [Security](#security) — input-handling posture
|
|
61
|
+
- [Contributing](#contributing) — how to get changes in
|
|
62
|
+
- [License](#license) — Apache-2.0
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What is bankstatementparser-loader-bai2?
|
|
67
|
+
|
|
68
|
+
**BAI2** (Bank Administration Institute, version 2) is the de-facto US
|
|
69
|
+
cash-management file format that banks ship for intraday and prior-day
|
|
70
|
+
balance and transaction reporting. The published
|
|
71
|
+
[`bankstatementparser`](https://pypi.org/project/bankstatementparser/)
|
|
72
|
+
library parses PDF and other statement formats but **does not support
|
|
73
|
+
BAI2**.
|
|
74
|
+
|
|
75
|
+
**bankstatementparser-loader-bai2** is a small, dependency-light companion
|
|
76
|
+
that fills that gap: give it a BAI2 payload and it returns a flat list of
|
|
77
|
+
[`bankstatementparser.transaction_models.Transaction`](https://pypi.org/project/bankstatementparser/)
|
|
78
|
+
objects (`source="bai2"`) that the rest of your deterministic pipeline
|
|
79
|
+
can consume unchanged.
|
|
80
|
+
|
|
81
|
+
| Concern | How this loader handles it |
|
|
82
|
+
| :--- | :--- |
|
|
83
|
+
| Record model | A documented, pragmatic subset of BAI2 (`01`/`02`/`03`/`16`/`88` plus ignored trailers) |
|
|
84
|
+
| Amounts | BAI2 minor-unit integers (cents) converted to `Decimal` (never `float`) |
|
|
85
|
+
| Debit / credit | Derived from the `16` type-code range, with the raw code preserved |
|
|
86
|
+
| Multiple accounts | All `16` records across every group / account are flattened into one list |
|
|
87
|
+
| Robustness | Tolerates CRLF, blank lines, and an optional trailing `/` per record |
|
|
88
|
+
| Errors | A clear `ValueError` if the file does not start with an `01` File Header |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
| Channel | Command | Notes |
|
|
95
|
+
| :--- | :--- | :--- |
|
|
96
|
+
| PyPI | `pip install bankstatementparser-loader-bai2` | Pulls in `bankstatementparser >= 0.0.9` |
|
|
97
|
+
| Source | `git clone https://github.com/sebastienrousseau/bankstatementparser-loader-bai2 && cd bankstatementparser-loader-bai2 && poetry install` | For development |
|
|
98
|
+
|
|
99
|
+
Requires Python 3.10 or later. Works on macOS, Linux, and Windows.
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary>Using an isolated virtual environment (recommended)</summary>
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
python -m venv venv
|
|
106
|
+
source venv/bin/activate # macOS/Linux
|
|
107
|
+
venv\Scripts\activate # Windows
|
|
108
|
+
python -m pip install -U bankstatementparser-loader-bai2
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from bankstatementparser_loader_bai2 import load_bai2_file
|
|
119
|
+
|
|
120
|
+
transactions = load_bai2_file("statement.bai")
|
|
121
|
+
for txn in transactions:
|
|
122
|
+
print(txn.account_id, txn.currency, txn.amount, txn.description)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or parse an in-memory payload:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from bankstatementparser_loader_bai2 import load_bai2
|
|
129
|
+
|
|
130
|
+
payload = (
|
|
131
|
+
"01,SENDER,RECEIVER,260601,1200,FILE001,,,/\n"
|
|
132
|
+
"02,RCVR,ORIG,1,260601,1200,USD,/\n"
|
|
133
|
+
"03,0123456789,USD,010,150000,1,,/\n"
|
|
134
|
+
"16,165,150000,Z,BANKREF1,CUSTREF1,Incoming wire payment/\n"
|
|
135
|
+
"88,from ACME Corp invoice 42/\n"
|
|
136
|
+
"16,475,2500,Z,BANKREF2,,ATM withdrawal/\n"
|
|
137
|
+
"49,152500,2/\n"
|
|
138
|
+
"98,152500,1,4/\n"
|
|
139
|
+
"99,152500,1,6/\n"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for txn in load_bai2(payload):
|
|
143
|
+
print(txn.amount, txn.category, txn.description)
|
|
144
|
+
# 1500 bai2:165 Incoming wire payment from ACME Corp invoice 42
|
|
145
|
+
# -25 bai2:475 ATM withdrawal
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Runnable versions live in [`examples/`](examples/).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Public API
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from bankstatementparser_loader_bai2 import (
|
|
156
|
+
load_bai2,
|
|
157
|
+
load_bai2_file,
|
|
158
|
+
summarize_bai2,
|
|
159
|
+
Bai2Summary,
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Function | Signature | Returns |
|
|
164
|
+
| :--- | :--- | :--- |
|
|
165
|
+
| `load_bai2` | `load_bai2(text: str)` | `list[Transaction]` |
|
|
166
|
+
| `load_bai2_file` | `load_bai2_file(path)` | `list[Transaction]` |
|
|
167
|
+
| `summarize_bai2` | `summarize_bai2(text: str)` | `Bai2Summary` |
|
|
168
|
+
|
|
169
|
+
`Bai2Summary` is a dataclass with the fields `file_id`, `group_count`,
|
|
170
|
+
`account_count`, `transaction_count`, and `currency`.
|
|
171
|
+
|
|
172
|
+
Each produced `Transaction` is populated as follows:
|
|
173
|
+
|
|
174
|
+
| `Transaction` field | Source |
|
|
175
|
+
| :--- | :--- |
|
|
176
|
+
| `account_id` | `03` Account Identifier — `accountNumber` |
|
|
177
|
+
| `currency` | `03` `currencyCode`, falling back to the `02` group currency |
|
|
178
|
+
| `amount` | `16` `amount` (cents / 100), signed per the convention below |
|
|
179
|
+
| `booking_date` | `02` Group Header as-of date, when present |
|
|
180
|
+
| `description` | `16` text plus any `88` continuations |
|
|
181
|
+
| `transaction_id` | `16` `bankRefNum`, falling back to `customerRefNum` |
|
|
182
|
+
| `reference` / `category` | The raw `16` type code (`category` as `bai2:<code>`) |
|
|
183
|
+
| `source` | Always `"bai2"` |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Supported BAI2 subset
|
|
188
|
+
|
|
189
|
+
BAI2 records are comma-delimited fields ending with a `/` delimiter,
|
|
190
|
+
each beginning with a numeric type code. This loader implements a
|
|
191
|
+
**documented, pragmatic subset**:
|
|
192
|
+
|
|
193
|
+
| Record | Meaning | Handling |
|
|
194
|
+
| :--- | :--- | :--- |
|
|
195
|
+
| `01` | File Header | **Required first record.** `fileId` captured for the summary. |
|
|
196
|
+
| `02` | Group Header | Group `currency` and as-of date captured. |
|
|
197
|
+
| `03` | Account Identifier | `accountNumber` + optional `currencyCode` captured; account currency overrides group currency. |
|
|
198
|
+
| `16` | Transaction Detail | One transaction. |
|
|
199
|
+
| `88` | Continuation | Appended to the preceding `03`/`16` record's text. |
|
|
200
|
+
| `49` / `98` / `99` | Account / Group / File trailers | **Ignored** — control totals are not validated. |
|
|
201
|
+
|
|
202
|
+
Any other (or unknown) leading type code is ignored so that vendor
|
|
203
|
+
extensions do not abort the parse. Ignoring control-total trailers is a
|
|
204
|
+
deliberate, documented choice: the goal is faithful transaction
|
|
205
|
+
extraction, not file-level reconciliation.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Amount and sign convention
|
|
210
|
+
|
|
211
|
+
BAI2 amounts are unsigned integers in the account currency's **minor
|
|
212
|
+
units** (cents), with no decimal point. They are converted to
|
|
213
|
+
`decimal.Decimal` by dividing by 100. An empty amount field is treated
|
|
214
|
+
as `0`.
|
|
215
|
+
|
|
216
|
+
Debit / credit direction is derived from the numeric range of the `16`
|
|
217
|
+
record's type code (this is the loader's chosen, documented convention):
|
|
218
|
+
|
|
219
|
+
| Type-code range | Direction | Sign |
|
|
220
|
+
| :--- | :--- | :--- |
|
|
221
|
+
| `100`–`399` | Credit | amount kept **positive** |
|
|
222
|
+
| `400`–`699` | Debit | amount made **negative** |
|
|
223
|
+
| anything else | unknown | amount kept **positive** |
|
|
224
|
+
|
|
225
|
+
The raw BAI2 type code is always preserved on the `Transaction` in both
|
|
226
|
+
`category` (as `bai2:<code>`) and `reference`, so no information is lost
|
|
227
|
+
— even for codes outside the two ranges above.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## When not to use this loader
|
|
232
|
+
|
|
233
|
+
- **You have ISO 20022 camt.053 or SWIFT MT940, not BAI2.** Those are
|
|
234
|
+
different formats with their own dedicated loaders.
|
|
235
|
+
- **You need control-total reconciliation.** This loader extracts
|
|
236
|
+
transactions and deliberately ignores the `49`/`98`/`99` trailers; if
|
|
237
|
+
you must validate file sums, do so before or after loading.
|
|
238
|
+
- **You need the full BAI2 specification.** This is a documented subset
|
|
239
|
+
focused on transaction extraction, not an exhaustive BAI2 parser.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
This project uses [Poetry](https://python-poetry.org/) and
|
|
246
|
+
[mise](https://mise.jdx.dev/).
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
git clone https://github.com/sebastienrousseau/bankstatementparser-loader-bai2.git
|
|
250
|
+
cd bankstatementparser-loader-bai2
|
|
251
|
+
poetry env use python3.12
|
|
252
|
+
poetry install
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A `Makefile` orchestrates the quality gates (kept in lockstep with CI):
|
|
256
|
+
|
|
257
|
+
| Target | What it runs |
|
|
258
|
+
| :--- | :--- |
|
|
259
|
+
| `make check` | All gates (REQUIRED before commit) |
|
|
260
|
+
| `make test` | `pytest --cov=bankstatementparser_loader_bai2 --cov-branch --cov-fail-under=100` |
|
|
261
|
+
| `make lint` | `ruff check` + `black --check` |
|
|
262
|
+
| `make type-check` | `mypy --strict` |
|
|
263
|
+
| `make doc-coverage` | `interrogate --fail-under=100` (docstring coverage) |
|
|
264
|
+
|
|
265
|
+
Current state (v0.0.10): **all tests passing, 100% line + branch
|
|
266
|
+
coverage** against a 100% enforced floor, `mypy --strict` clean,
|
|
267
|
+
interrogate 100%.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Security
|
|
272
|
+
|
|
273
|
+
- **Read-only.** The loader only reads text / files you pass it; it
|
|
274
|
+
writes nothing.
|
|
275
|
+
- **No XML, no network, no code execution.** Parsing is a pure
|
|
276
|
+
string-to-dataclass transformation.
|
|
277
|
+
- **Decimal arithmetic** is used throughout, avoiding `float` rounding
|
|
278
|
+
surprises in financial amounts.
|
|
279
|
+
- **Dependencies** are pinned via `poetry.lock` and audited in CI.
|
|
280
|
+
|
|
281
|
+
To report a vulnerability, please use
|
|
282
|
+
[GitHub private vulnerability reporting](https://github.com/sebastienrousseau/bankstatementparser-loader-bai2/security)
|
|
283
|
+
rather than a public issue.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Contributing
|
|
288
|
+
|
|
289
|
+
Contributions are welcome — see the
|
|
290
|
+
[contributing instructions](https://github.com/sebastienrousseau/bankstatementparser-loader-bai2/blob/main/CONTRIBUTING.md).
|
|
291
|
+
Thanks to all the
|
|
292
|
+
[contributors](https://github.com/sebastienrousseau/bankstatementparser-loader-bai2/graphs/contributors)
|
|
293
|
+
who have helped build `bankstatementparser-loader-bai2`.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
Licensed under the [Apache License, Version 2.0](https://opensource.org/license/apache-2-0/).
|
|
300
|
+
Any contribution submitted for inclusion shall be licensed as above,
|
|
301
|
+
without additional terms.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
<p align="center">
|
|
306
|
+
<a href="https://bankstatementparser.com">bankstatementparser.com</a> ·
|
|
307
|
+
<a href="https://pypi.org/project/bankstatementparser-loader-bai2/">PyPI</a> ·
|
|
308
|
+
<a href="https://github.com/sebastienrousseau/bankstatementparser-loader-bai2">GitHub</a>
|
|
309
|
+
</p>
|
|
310
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
bankstatementparser_loader_bai2/__init__.py,sha256=brUSm0ran5O3AhzsAwjuLBlF5BCCCRdQ1cNZYI3M88Y,942
|
|
2
|
+
bankstatementparser_loader_bai2/loader.py,sha256=Tj9dVM49L54K9ZJVFlMCU5xp4DFoSVI0gM0dTXhEhCk,16133
|
|
3
|
+
bankstatementparser_loader_bai2-0.0.10.dist-info/METADATA,sha256=vdGXVrB_yaT97fu5VunfFBQPr1ZbS5mBlDYVis8soy4,12100
|
|
4
|
+
bankstatementparser_loader_bai2-0.0.10.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
|
|
5
|
+
bankstatementparser_loader_bai2-0.0.10.dist-info/licenses/LICENSE,sha256=BhsQ65IVGLlScIgEHcMm2leCnoRbQqwPcLd7N2c8Gt0,10244
|
|
6
|
+
bankstatementparser_loader_bai2-0.0.10.dist-info/RECORD,,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean any work of authorship, including
|
|
48
|
+
the original version of the Work and any modifications or additions
|
|
49
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
50
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
51
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
52
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
53
|
+
means any form of electronic, verbal, or written communication sent
|
|
54
|
+
to the Licensor or its representatives, including but not limited to
|
|
55
|
+
communication on electronic mailing lists, source code control systems,
|
|
56
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
57
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
58
|
+
excluding communication that is conspicuously marked or otherwise
|
|
59
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
60
|
+
|
|
61
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
62
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
63
|
+
subsequently incorporated within the Work.
|
|
64
|
+
|
|
65
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
66
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
67
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
68
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
69
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
70
|
+
Work and such Derivative Works in Source or Object form.
|
|
71
|
+
|
|
72
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
73
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
74
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
75
|
+
(except as stated in this section) patent license to make, have made,
|
|
76
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
77
|
+
where such license applies only to those patent claims licensable
|
|
78
|
+
by such Contributor that are necessarily infringed by their
|
|
79
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
80
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
81
|
+
institute patent litigation against any entity (including a
|
|
82
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
83
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
84
|
+
or contributory patent infringement, then any patent licenses
|
|
85
|
+
granted to You under this License for that Work shall terminate
|
|
86
|
+
as of the date such litigation is filed.
|
|
87
|
+
|
|
88
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
89
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
90
|
+
modifications, and in Source or Object form, provided that You
|
|
91
|
+
meet the following conditions:
|
|
92
|
+
|
|
93
|
+
(a) You must give any other recipients of the Work or
|
|
94
|
+
Derivative Works a copy of this License; and
|
|
95
|
+
|
|
96
|
+
(b) You must cause any modified files to carry prominent notices
|
|
97
|
+
stating that You changed the files; and
|
|
98
|
+
|
|
99
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
100
|
+
that You distribute, all copyright, patent, trademark, and
|
|
101
|
+
attribution notices from the Source form of the Work,
|
|
102
|
+
excluding those notices that do not pertain to any part of
|
|
103
|
+
the Derivative Works; and
|
|
104
|
+
|
|
105
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
106
|
+
distribution, then any Derivative Works that You distribute must
|
|
107
|
+
include a readable copy of the attribution notices contained
|
|
108
|
+
within such NOTICE file, excluding any notices that do not
|
|
109
|
+
pertain to any part of the Derivative Works, in at least one
|
|
110
|
+
of the following places: within a NOTICE text file distributed
|
|
111
|
+
as part of the Derivative Works; within the Source form or
|
|
112
|
+
documentation, if provided along with the Derivative Works; or,
|
|
113
|
+
within a display generated by the Derivative Works, if and
|
|
114
|
+
wherever such third-party notices normally appear. The contents
|
|
115
|
+
of the NOTICE file are for informational purposes only and
|
|
116
|
+
do not modify the License. You may add Your own attribution
|
|
117
|
+
notices within Derivative Works that You distribute, alongside
|
|
118
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
119
|
+
that such additional attribution notices cannot be construed
|
|
120
|
+
as modifying the License.
|
|
121
|
+
|
|
122
|
+
You may add Your own copyright statement to Your modifications and
|
|
123
|
+
may provide additional or different license terms and conditions
|
|
124
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
125
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
126
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
127
|
+
the conditions stated in this License.
|
|
128
|
+
|
|
129
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
130
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
131
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
132
|
+
this License, without any additional terms or conditions.
|
|
133
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
134
|
+
the terms of any separate license agreement you may have executed
|
|
135
|
+
with Licensor regarding such Contributions.
|
|
136
|
+
|
|
137
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
138
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
139
|
+
except as required for reasonable and customary use in describing the
|
|
140
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
141
|
+
|
|
142
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
143
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
144
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
145
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
146
|
+
implied, including, without limitation, any warranties or conditions
|
|
147
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
148
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
149
|
+
appropriateness of using or redistributing the Work and assume any
|
|
150
|
+
risks associated with Your exercise of permissions under this License.
|
|
151
|
+
|
|
152
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
153
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
154
|
+
unless required by applicable law (such as deliberate and grossly
|
|
155
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
156
|
+
liable to You for damages, including any direct, indirect, special,
|
|
157
|
+
incidental, or consequential damages of any character arising as a
|
|
158
|
+
result of this License or out of the use or inability to use the
|
|
159
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
160
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
161
|
+
other commercial damages or losses), even if such Contributor
|
|
162
|
+
has been advised of the possibility of such damages.
|
|
163
|
+
|
|
164
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
165
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
166
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
167
|
+
or other liability obligations and/or rights consistent with this
|
|
168
|
+
License. However, in accepting such obligations, You may act only
|
|
169
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
170
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
171
|
+
defend, and hold each Contributor harmless for any liability
|
|
172
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
173
|
+
of your accepting any such warranty or additional liability.
|
|
174
|
+
|
|
175
|
+
END OF TERMS AND CONDITIONS
|
|
176
|
+
|
|
177
|
+
Copyright 2023-2026 Sebastien Rousseau
|
|
178
|
+
|
|
179
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
|
+
you may not use this file except in compliance with the License.
|
|
181
|
+
You may obtain a copy of the License at
|
|
182
|
+
|
|
183
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
184
|
+
|
|
185
|
+
Unless required by applicable law or agreed to in writing, software
|
|
186
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
187
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
188
|
+
See the License for the specific language governing permissions and
|
|
189
|
+
limitations under the License.
|