Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ output/*/index.html
docs/_build
/docs/doctrees/
/docs/html/
# API reference pages, regenerated from docs/conf.py on every build
/docs/modules.rst
/docs/mt940*.rst
.aider*
.env

Expand Down
187 changes: 116 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,71 +8,70 @@
[![Coverage](https://coveralls.io/repos/github/WoLpH/mt940/badge.svg?branch=develop)](https://coveralls.io/github/WoLpH/mt940?branch=develop)
[![License](https://img.shields.io/pypi/l/mt-940)](https://github.com/WoLpH/mt940/blob/develop/LICENSE)

`mt940` is a library to parse MT940 files and return smart Python collections
for statistics and manipulation. It has no runtime dependencies, is fully type
annotated (`py.typed`), and ships smart models that make working with bank
statements straightforward.

## Quick Start

```bash
pip install mt-940
```
`mt940` parses **MT940 bank statement files** into smart, fully typed Python
collections you can iterate, aggregate and serialize. It has no runtime
dependencies, ships type information (`py.typed`), and copes with the quirks of
many real-world banks.

```python
import mt940
import pprint

transactions = mt940.parse('mt940_tests/jejik/abnamro.sta')

print('Transactions:')
print(transactions)
pprint.pprint(transactions.data)

transactions = mt940.parse('statement.sta')
for transaction in transactions:
print('Transaction: ', transaction)
pprint.pprint(transaction.data)
print(transaction.data['date'], transaction.data['amount'])
```

## Usage
## Why mt940

### Set opening / closing balance information on each transaction
- **Zero runtime dependencies** — pure standard library.
- **Fully typed** — ships `py.typed`; checked under pyright, mypy and pyrefly.
- **Battle-tested** — 100% test coverage against fixtures from many banks.
- **Smart models** — amounts, balances and dates come back as rich Python
objects, not raw strings.
- **JSON-ready** — a single encoder serializes a whole statement.
- **Extensible** — opt-in tags and pre/post processors for bank-specific
formats.
- **Modern Python** — supports 3.10 through 3.13.

```python
import mt940
import pprint
## Installation

mt940.tags.BalanceBase.scope = mt940.models.Transaction
```bash
pip install mt-940
```

# The currency has to be set manually when setting the BalanceBase scope to
# Transaction.
transactions = mt940.models.Transactions(processors=dict(
pre_statement=[
mt940.processors.add_currency_pre_processor('EUR'),
],
))
Using [uv](https://docs.astral.sh/uv/):

with open('mt940_tests/jejik/abnamro.sta') as f:
data = f.read()
```bash
uv add mt-940
```

transactions.parse(data)
Requires Python 3.10 or newer.

for transaction in transactions:
print('Transaction: ', transaction)
pprint.pprint(transaction.data)
```
## Quick start

### Simple JSON encoding
`parse()` accepts a filename, an open file handle, or the raw `str`/`bytes`
contents, and returns a `Transactions` collection:

```python
import json
import mt940
import pprint

transactions = mt940.parse('mt940_tests/jejik/abnamro.sta')

print(json.dumps(transactions, indent=4, cls=mt940.JSONEncoder))
# Statement-level data (balances, account, ...) lives on the collection:
pprint.pprint(transactions.data)

# Iterate the individual transactions:
for transaction in transactions:
print(transaction.data['date'], transaction.data['amount'])
pprint.pprint(transaction.data)
```

Each `Transaction` exposes a `data` dictionary with the parsed fields. Which
fields are present depends on the source bank and the tags in the file.

## Usage

### Reading balances

Statement-level balances live on the `Transactions` object's `data`, not on the
Expand All @@ -87,13 +86,34 @@ print(transactions.data['final_closing_balance'])
print(transactions.data['available_balance'])
```

### Set opening / closing balance on each transaction

```python
import mt940
import pprint

mt940.tags.BalanceBase.scope = mt940.models.Transaction

# The currency has to be set manually when moving the BalanceBase scope to
# Transaction.
transactions = mt940.models.Transactions(processors=dict(
pre_statement=[mt940.processors.add_currency_pre_processor('EUR')],
))

with open('mt940_tests/jejik/abnamro.sta') as fh:
transactions.parse(fh.read())

for transaction in transactions:
pprint.pprint(transaction.data)
```

### Multiple statements in one file

A single `parse()` merges everything into one `Transactions` and keeps only the
**last** block's statement-level data (e.g. balances). For files that concatenate
several statements (including balance-only blocks), use `parse_statements()`,
which splits on `:20:` boundaries and returns one `Transactions` per statement,
each with its own balances:
**last** block's statement-level data (e.g. balances). For files that
concatenate several statements (including balance-only blocks), use
`parse_statements()`, which splits on `:20:` boundaries and returns one
`Transactions` per statement, each with its own balances:

```python
import mt940
Expand All @@ -104,47 +124,35 @@ for statement in mt940.parse_statements('statements.sta'):
print(statement.data['final_closing_balance'])
```

### Parsing statements from the Dutch bank ASN

Tag 61 in ASN statements does not follow the SWIFT specification, so a custom
tag is used:
### Serializing to JSON

```python
import pprint
import json
import mt940


def ASNB_mt940_data():
with open('mt940_tests/ASNB/0708271685_09022020_164516.940.txt') as fh:
return fh.read()


tag_parser = mt940.tags.StatementASNB()
trs = mt940.models.Transactions(tags={tag_parser.id: tag_parser})
trs.parse(ASNB_mt940_data())

print(pprint.pformat(trs.data, sort_dicts=False))
transactions = mt940.parse('statement.sta')
print(json.dumps(transactions, indent=4, cls=mt940.JSONEncoder))
```

### Transaction grouping (opt-in)

By default a new transaction is started only on the `:61:` statement tag.
Some banks delimit transactions differently — for example by repeating the
`:20:` transaction reference per block. Because changing the default grouping
would break existing users, this behaviour is **opt-in**: pass
`transaction_boundary` (an iterable of tag *slugs*) to start a new transaction
on those tags too. Omitting it preserves the historical behaviour.
By default a new transaction is started only on the `:61:` statement tag. Some
banks delimit transactions differently — for example by repeating the `:20:`
transaction reference per block. Because changing the default grouping would
break existing users, this behaviour is **opt-in**: pass `transaction_boundary`
(an iterable of tag *slugs*) to start a new transaction on those tags too.

```python
import mt940

# Each `:20:` (transaction_reference_number) starts its own transaction:
transactions = mt940.parse(
data, transaction_boundary={'transaction_reference_number'}
'statement.sta', transaction_boundary={'transaction_reference_number'}
)
```

The same option is accepted by `mt940.models.Transactions(transaction_boundary=...)`.
The same option is accepted by
`mt940.models.Transactions(transaction_boundary=...)`.

### Banks with longer reference fields (opt-in)

Expand All @@ -157,12 +165,49 @@ same-line data, so this is handled by an **opt-in** `StatementGLS` tag:
import mt940

gls = mt940.tags.StatementGLS()
transactions = mt940.parse(data, tags={gls.id: gls})
transactions = mt940.parse('statement.sta', tags={gls.id: gls})
```

(Longer *supplementary details* — issue #117, e.g. Wise — are handled by the
default parser and need no opt-in.)

### Statements from the Dutch bank ASN (opt-in)

Tag 61 in ASN statements does not follow the SWIFT specification, so the opt-in
`StatementASNB` tag is used:

```python
import mt940
import pprint

tag = mt940.tags.StatementASNB()
transactions = mt940.models.Transactions(tags={tag.id: tag})

with open('mt940_tests/ASNB/mt940.txt') as fh:
transactions.parse(fh.read())

pprint.pprint(transactions.data, sort_dicts=False)
```

## Supported tags

| Tag | Meaning |
| --- | --- |
| `:13:` | Date/time the report was created |
| `:20:` | Transaction reference number |
| `:21:` | Related reference |
| `:25:` | Account identification |
| `:28C:` | Statement number / sequence number |
| `:34F:` | Floor limit for debit and credit |
| `:60F:` / `:60M:` | (Final / intermediate) opening balance |
| `:61:` | Statement line (a transaction) |
| `:62F:` / `:62M:` | (Final / intermediate) closing balance |
| `:64:` | Available balance |
| `:65:` | Forward available balance |
| `:86:` | Transaction details |
| `:90C:` / `:90D:` | Number and sum of credit / debit entries |
| `:NS:` | Bank-specific non-SWIFT extensions |

## Contributing

Help is greatly appreciated. Please clone the **develop** branch and run `tox`
Expand Down
Loading