ebicsclient 0.2.0__tar.gz → 1.3.0__tar.gz
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.
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.github/workflows/ci.yml +2 -2
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.github/workflows/release.yml +4 -4
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/CLAUDE.md +6 -3
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/PKG-INFO +45 -9
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/README.md +43 -7
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/examples/zkb_handshake.py +34 -1
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/pyproject.toml +2 -2
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/__init__.py +6 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/client.py +88 -12
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/errors.py +9 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/models.py +44 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/protocol/h005.py +188 -3
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/crypto_helpers.py +21 -4
- ebicsclient-1.3.0/tests/data/htd_zkb_sample.xml +707 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_client.py +121 -32
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_h005.py +109 -1
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_schema_validation.py +10 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/uv.lock +1 -1
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/.gitignore +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/CONTRIBUTING.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/LICENSE.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/01-protocol-and-formats.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/02-licensing-strategy.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/03-library-landscape.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/04-implementation-plan.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/05-zkb-onboarding.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/06-engineering-conventions.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/07-handshake-testing.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/08-parity-and-xsd-findings.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/09-zkb-test-platform-settings.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/10-btf-order-types.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/docs/11-certificate-profiles.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/certificates.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/crypto.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/__init__.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt052.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt053.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/camt054.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/container.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/formats/pain002.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/keys.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/letter.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/protocol/__init__.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/py.typed +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/src/ebicsclient/transport.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt052_zkb_sample.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt053_zkb_sample.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_collective_zkb_sample.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_credit_zkb_sample.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/camt054_debit_zkb_sample.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_accp.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_actc.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_part.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/data/pain002_rjct.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/hia.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/hpb.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/ini.xml +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/fixtures/parity/meta.json +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_c14n_vectors.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_camt052_054.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_camt053.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_certificates.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_crypto.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_errors.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_keys.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_letter.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_pain002.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_php_parity.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tests/test_transport.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/fetch-schemas.py +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/README.md +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/composer.json +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/composer.lock +0 -0
- {ebicsclient-0.2.0 → ebicsclient-1.3.0}/tools/php-parity/generate.php +0 -0
|
@@ -13,10 +13,10 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
python-version: ["3.11", "3.12", "3.13"]
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
17
|
|
|
18
18
|
- name: Install uv
|
|
19
|
-
uses: astral-sh/setup-uv@
|
|
19
|
+
uses: astral-sh/setup-uv@v7
|
|
20
20
|
with:
|
|
21
21
|
python-version: ${{ matrix.python-version }}
|
|
22
22
|
|
|
@@ -8,10 +8,10 @@ jobs:
|
|
|
8
8
|
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
11
|
+
- uses: actions/checkout@v6
|
|
12
12
|
|
|
13
13
|
- name: Install uv
|
|
14
|
-
uses: astral-sh/setup-uv@
|
|
14
|
+
uses: astral-sh/setup-uv@v7
|
|
15
15
|
with:
|
|
16
16
|
python-version: "3.12"
|
|
17
17
|
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
- name: Build sdist and wheel
|
|
43
43
|
run: uv build
|
|
44
44
|
|
|
45
|
-
- uses: actions/upload-artifact@
|
|
45
|
+
- uses: actions/upload-artifact@v5
|
|
46
46
|
with:
|
|
47
47
|
name: dist
|
|
48
48
|
path: dist/
|
|
@@ -58,7 +58,7 @@ jobs:
|
|
|
58
58
|
# anywhere; PyPI trusts this repository + workflow + environment combination.
|
|
59
59
|
id-token: write
|
|
60
60
|
steps:
|
|
61
|
-
- uses: actions/download-artifact@
|
|
61
|
+
- uses: actions/download-artifact@v6
|
|
62
62
|
with:
|
|
63
63
|
name: dist
|
|
64
64
|
path: dist/
|
|
@@ -78,10 +78,13 @@ MVP = read **and** write, validated live against ZKB: key init (INI/HIA/HPB) →
|
|
|
78
78
|
`EOP/camt.053.001.08` and parse balances → upload `MCT/pain.001.001.09` with the A006 electronic
|
|
79
79
|
signature. All of this is built and bank-accepted.
|
|
80
80
|
|
|
81
|
+
The pain.002/camt.052/camt.054 parsers are built (M5, validated on genuine ZKB messages).
|
|
81
82
|
Still deliberately **out of scope** — do not build until there is a concrete need and a way to
|
|
82
|
-
validate it: distributed signatures (EDS
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
validate it: distributed signatures (EDS — the ZKB test subscriber has ``NumSigRequired=0``, so
|
|
84
|
+
EDS cannot be validated there) and multi-bank quirk handling. **H004 and earlier EBICS versions
|
|
85
|
+
will never be supported** (owner decision; see docs/04) — the `protocol/` seam exists for a
|
|
86
|
+
future EBICS version, not for the past. Scope creep is what turns a stable standard into a
|
|
87
|
+
maintenance burden.
|
|
85
88
|
|
|
86
89
|
## Workflow
|
|
87
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ebicsclient
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Pure-Python client for the EBICS 3.0 (H005) banking protocol
|
|
5
5
|
Project-URL: Homepage, https://github.com/schulluk/ebicsclient
|
|
6
6
|
Project-URL: Repository, https://github.com/schulluk/ebicsclient
|
|
@@ -95,7 +95,7 @@ License: Copyright © 2026 Lukas Schulze
|
|
|
95
95
|
**Use** means anything you do with the software requiring one of your licenses.
|
|
96
96
|
License-File: LICENSE.md
|
|
97
97
|
Keywords: banking,camt,ebics,fintech,h005,iso20022
|
|
98
|
-
Classifier: Development Status ::
|
|
98
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
99
99
|
Classifier: Intended Audience :: Developers
|
|
100
100
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
101
101
|
Classifier: License :: Other/Proprietary License
|
|
@@ -118,9 +118,25 @@ Description-Content-Type: text/markdown
|
|
|
118
118
|
|
|
119
119
|
# ebicsclient — a pure-Python EBICS 3.0 (H005) client
|
|
120
120
|
|
|
121
|
-
A from-scratch, pure-Python client for the **EBICS** banking protocol.
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
A from-scratch, pure-Python client for the **EBICS** banking protocol (EBICS 3.0 / H005),
|
|
122
|
+
validated live against Zürcher Kantonalbank (ZKB).
|
|
123
|
+
|
|
124
|
+
**What it provides today:**
|
|
125
|
+
|
|
126
|
+
- **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
|
|
127
|
+
CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
|
|
128
|
+
**pinning** across sessions
|
|
129
|
+
- **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
|
|
130
|
+
statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
|
|
131
|
+
and **pain.002** payment status reports
|
|
132
|
+
- **Write** — **pain.001** payment submission (BTU) with the A006 electronic signature
|
|
133
|
+
- **Security throughout** — the bank's `AuthSignature` is verified on **every** response, unknown
|
|
134
|
+
return codes are never masked, and everything fails closed
|
|
135
|
+
- **Self-inspection** — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
|
|
136
|
+
|
|
137
|
+
**What is missing:** distributed signatures (EDS/VEU — multi-person payment approval; see
|
|
138
|
+
milestone 7 below), and validation against banks beyond ZKB. Legacy EBICS versions (H004 and
|
|
139
|
+
earlier) are deliberately unsupported.
|
|
124
140
|
|
|
125
141
|
- **Stack:** Python 3.11+, just two runtime deps — `cryptography` (RSA/AES) and `lxml` (XML /
|
|
126
142
|
inclusive Canonical XML 1.0); everything else stdlib. No PHP/Java sidecar. (Rationale:
|
|
@@ -238,11 +254,31 @@ a real bank statement.
|
|
|
238
254
|
- [x] camt.052 intraday reports
|
|
239
255
|
- [x] camt.054 booking advices (incl. the QRR / SCOR / LSV variants via `service_option`)
|
|
240
256
|
|
|
241
|
-
**Milestone 6 — Protocol
|
|
257
|
+
**Milestone 6 — Protocol hardening & conveniences**
|
|
258
|
+
|
|
259
|
+
- [x] Verify the bank's `AuthSignature` on every response — validated live on ZKB
|
|
260
|
+
- [x] Subscriber self-inspection — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
|
|
261
|
+
|
|
262
|
+
**Milestone 7 — Distributed signatures (EDS/VEU)** (next; parked until validatable)
|
|
263
|
+
|
|
264
|
+
EDS (*Elektronische Verteilte Unterschrift*) is EBICS's workflow for orders that need
|
|
265
|
+
**multiple people to sign before the bank executes them** — dual control on payments. An order
|
|
266
|
+
is uploaded with `requestEDS` and parks in the bank's VEU queue; further signatories list the
|
|
267
|
+
queue, inspect the order, and deliver their A006 signatures until the configured quorum (e.g.
|
|
268
|
+
first + second signature) is reached. The building blocks (A006 signing, the `SignatureFlag`,
|
|
269
|
+
the admin-download transaction pattern) are in place; what is missing is a **validatable
|
|
270
|
+
multi-user setup** — the ZKB test subscriber is single-user with `NumSigRequired=0`, so the
|
|
271
|
+
workflow cannot be exercised there. This milestone starts when a downstream setup with a real
|
|
272
|
+
multi-signature profile exists.
|
|
273
|
+
|
|
274
|
+
- [ ] Upload into the VEU queue (`SignatureFlag` with `requestEDS`)
|
|
275
|
+
- [ ] List pending orders (HVU/HVZ) and fetch order details (HVD) and transactions (HVT)
|
|
276
|
+
- [ ] Deliver an additional signature (HVE) and cancel a pending order (HVS)
|
|
277
|
+
- [ ] Validate the full quorum workflow against a multi-signature bank profile
|
|
242
278
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
279
|
+
This client is **EBICS 3.0 (H005) only** — legacy versions (H004 and earlier) will not be
|
|
280
|
+
supported (see [docs/04](docs/04-implementation-plan.md)); the `protocol/` seam exists for a
|
|
281
|
+
future EBICS version, not for the past.
|
|
246
282
|
|
|
247
283
|
## License
|
|
248
284
|
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
# ebicsclient — a pure-Python EBICS 3.0 (H005) client
|
|
2
2
|
|
|
3
|
-
A from-scratch, pure-Python client for the **EBICS** banking protocol.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A from-scratch, pure-Python client for the **EBICS** banking protocol (EBICS 3.0 / H005),
|
|
4
|
+
validated live against Zürcher Kantonalbank (ZKB).
|
|
5
|
+
|
|
6
|
+
**What it provides today:**
|
|
7
|
+
|
|
8
|
+
- **Key ceremony** — INI/HIA/HPB with X.509 key transmission (self-signed *mit Schlüsseln* or
|
|
9
|
+
CA-issued *mit Zertifikaten*), the printable initialisation letter (HTML/PDF), and bank-key
|
|
10
|
+
**pinning** across sessions
|
|
11
|
+
- **Read** — statement/report downloads with parsers into typed models: **camt.053** end-of-day
|
|
12
|
+
statements, **camt.052** intraday reports, **camt.054** booking advices (incl. QRR/SCOR/LSV),
|
|
13
|
+
and **pain.002** payment status reports
|
|
14
|
+
- **Write** — **pain.001** payment submission (BTU) with the A006 electronic signature
|
|
15
|
+
- **Security throughout** — the bank's `AuthSignature` is verified on **every** response, unknown
|
|
16
|
+
return codes are never masked, and everything fails closed
|
|
17
|
+
- **Self-inspection** — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
|
|
18
|
+
|
|
19
|
+
**What is missing:** distributed signatures (EDS/VEU — multi-person payment approval; see
|
|
20
|
+
milestone 7 below), and validation against banks beyond ZKB. Legacy EBICS versions (H004 and
|
|
21
|
+
earlier) are deliberately unsupported.
|
|
6
22
|
|
|
7
23
|
- **Stack:** Python 3.11+, just two runtime deps — `cryptography` (RSA/AES) and `lxml` (XML /
|
|
8
24
|
inclusive Canonical XML 1.0); everything else stdlib. No PHP/Java sidecar. (Rationale:
|
|
@@ -120,11 +136,31 @@ a real bank statement.
|
|
|
120
136
|
- [x] camt.052 intraday reports
|
|
121
137
|
- [x] camt.054 booking advices (incl. the QRR / SCOR / LSV variants via `service_option`)
|
|
122
138
|
|
|
123
|
-
**Milestone 6 — Protocol
|
|
139
|
+
**Milestone 6 — Protocol hardening & conveniences**
|
|
124
140
|
|
|
125
|
-
- [
|
|
126
|
-
- [
|
|
127
|
-
|
|
141
|
+
- [x] Verify the bank's `AuthSignature` on every response — validated live on ZKB
|
|
142
|
+
- [x] Subscriber self-inspection — `available_order_types()` (HAA) and `subscriber_info()` (HTD)
|
|
143
|
+
|
|
144
|
+
**Milestone 7 — Distributed signatures (EDS/VEU)** (next; parked until validatable)
|
|
145
|
+
|
|
146
|
+
EDS (*Elektronische Verteilte Unterschrift*) is EBICS's workflow for orders that need
|
|
147
|
+
**multiple people to sign before the bank executes them** — dual control on payments. An order
|
|
148
|
+
is uploaded with `requestEDS` and parks in the bank's VEU queue; further signatories list the
|
|
149
|
+
queue, inspect the order, and deliver their A006 signatures until the configured quorum (e.g.
|
|
150
|
+
first + second signature) is reached. The building blocks (A006 signing, the `SignatureFlag`,
|
|
151
|
+
the admin-download transaction pattern) are in place; what is missing is a **validatable
|
|
152
|
+
multi-user setup** — the ZKB test subscriber is single-user with `NumSigRequired=0`, so the
|
|
153
|
+
workflow cannot be exercised there. This milestone starts when a downstream setup with a real
|
|
154
|
+
multi-signature profile exists.
|
|
155
|
+
|
|
156
|
+
- [ ] Upload into the VEU queue (`SignatureFlag` with `requestEDS`)
|
|
157
|
+
- [ ] List pending orders (HVU/HVZ) and fetch order details (HVD) and transactions (HVT)
|
|
158
|
+
- [ ] Deliver an additional signature (HVE) and cancel a pending order (HVS)
|
|
159
|
+
- [ ] Validate the full quorum workflow against a multi-signature bank profile
|
|
160
|
+
|
|
161
|
+
This client is **EBICS 3.0 (H005) only** — legacy versions (H004 and earlier) will not be
|
|
162
|
+
supported (see [docs/04](docs/04-implementation-plan.md)); the `protocol/` seam exists for a
|
|
163
|
+
future EBICS version, not for the past.
|
|
128
164
|
|
|
129
165
|
## License
|
|
130
166
|
|
|
@@ -15,6 +15,8 @@ Steps, matching the EBICS initialisation ceremony (see docs/05, docs/07):
|
|
|
15
15
|
download fetch the EOP/camt.053 statements and print their closing balances
|
|
16
16
|
upload upload a pain.001 payment file (path in EBICS_UPLOAD_PATH)
|
|
17
17
|
pain002 fetch the pain.002 payment status report(s) and print them
|
|
18
|
+
haa list the order types with data available at the bank
|
|
19
|
+
htd show the subscriber data registered at the bank
|
|
18
20
|
|
|
19
21
|
Run each step in order, e.g.::
|
|
20
22
|
|
|
@@ -83,7 +85,8 @@ def main() -> int:
|
|
|
83
85
|
parser.add_argument(
|
|
84
86
|
"step",
|
|
85
87
|
choices=[
|
|
86
|
-
"generate", "ini", "hia", "letter", "hpb", "hashes", "download", "upload",
|
|
88
|
+
"generate", "ini", "hia", "letter", "hpb", "hashes", "download", "upload",
|
|
89
|
+
"pain002", "haa", "htd",
|
|
87
90
|
],
|
|
88
91
|
)
|
|
89
92
|
step = parser.parse_args().step
|
|
@@ -219,6 +222,34 @@ def _pain002() -> int:
|
|
|
219
222
|
return 0
|
|
220
223
|
|
|
221
224
|
|
|
225
|
+
def _haa() -> int:
|
|
226
|
+
client = _build_client()
|
|
227
|
+
_fetch_bank_keys(client)
|
|
228
|
+
order_types = client.available_order_types()
|
|
229
|
+
if not order_types:
|
|
230
|
+
print("HAA: no order types currently have data available.")
|
|
231
|
+
return 0
|
|
232
|
+
print(f"HAA: {len(order_types)} order type(s) with data available:")
|
|
233
|
+
for btf in order_types:
|
|
234
|
+
print(f" {btf.service_name}/{btf.scope or '-'}/{btf.message_name}"
|
|
235
|
+
f" v{btf.message_version or '-'} container={btf.container or '-'}")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _htd() -> int:
|
|
240
|
+
client = _build_client()
|
|
241
|
+
_fetch_bank_keys(client)
|
|
242
|
+
info = client.subscriber_info()
|
|
243
|
+
print(f"HTD: user {info.user_id} (status {info.user_status}), name: {info.name}")
|
|
244
|
+
print(f" permissions: {len(info.permissions)}, partner order types: {len(info.order_types)}")
|
|
245
|
+
for entry in info.order_types:
|
|
246
|
+
service = entry.service
|
|
247
|
+
btf = (f"{service.service_name}/{service.scope or '-'}/{service.message_name}"
|
|
248
|
+
f" v{service.message_version or '-'}") if service else "-"
|
|
249
|
+
print(f" {entry.admin_order_type:<4} {btf:<40} {entry.description or ''}")
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
|
|
222
253
|
def _build_client() -> Client:
|
|
223
254
|
return Client(_bank(), _user(), _load_keyring())
|
|
224
255
|
|
|
@@ -284,6 +315,8 @@ _STEPS = {
|
|
|
284
315
|
"download": _download,
|
|
285
316
|
"upload": _upload,
|
|
286
317
|
"pain002": _pain002,
|
|
318
|
+
"haa": _haa,
|
|
319
|
+
"htd": _htd,
|
|
287
320
|
}
|
|
288
321
|
|
|
289
322
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ebicsclient"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Pure-Python client for the EBICS 3.0 (H005) banking protocol"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ license = { file = "LICENSE.md" }
|
|
|
12
12
|
authors = [{ name = "Lukas Schulze" }]
|
|
13
13
|
keywords = ["ebics", "banking", "iso20022", "camt", "h005", "fintech"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"Intended Audience :: Financial and Insurance Industry",
|
|
18
18
|
"License :: Other/Proprietary License",
|
|
@@ -30,6 +30,7 @@ from ebicsclient.errors import (
|
|
|
30
30
|
MessageFormatError,
|
|
31
31
|
MissingDependencyError,
|
|
32
32
|
ProtocolError,
|
|
33
|
+
ResponseAuthenticationError,
|
|
33
34
|
Retryability,
|
|
34
35
|
ReturnCodeError,
|
|
35
36
|
TransportError,
|
|
@@ -63,11 +64,13 @@ from ebicsclient.models import (
|
|
|
63
64
|
Keyring,
|
|
64
65
|
Letter,
|
|
65
66
|
Notification,
|
|
67
|
+
OrderTypeInfo,
|
|
66
68
|
OutputFormat,
|
|
67
69
|
PaymentStatus,
|
|
68
70
|
PaymentStatusReport,
|
|
69
71
|
Statement,
|
|
70
72
|
StatusReason,
|
|
73
|
+
SubscriberInfo,
|
|
71
74
|
TransactionStatus,
|
|
72
75
|
UploadPayload,
|
|
73
76
|
User,
|
|
@@ -116,15 +119,18 @@ __all__ = [
|
|
|
116
119
|
"MessageFormatError",
|
|
117
120
|
"MissingDependencyError",
|
|
118
121
|
"Notification",
|
|
122
|
+
"OrderTypeInfo",
|
|
119
123
|
"OutputFormat",
|
|
120
124
|
"PaymentStatus",
|
|
121
125
|
"PaymentStatusReport",
|
|
122
126
|
"ProtocolError",
|
|
127
|
+
"ResponseAuthenticationError",
|
|
123
128
|
"Retryability",
|
|
124
129
|
"ReturnCodeError",
|
|
125
130
|
"SelfSignedCertificateProvider",
|
|
126
131
|
"Statement",
|
|
127
132
|
"StatusReason",
|
|
133
|
+
"SubscriberInfo",
|
|
128
134
|
"TransactionStatus",
|
|
129
135
|
"TransportError",
|
|
130
136
|
"TrustAnchorVerifier",
|
|
@@ -32,6 +32,7 @@ from ebicsclient.models import (
|
|
|
32
32
|
OutputFormat,
|
|
33
33
|
PaymentStatusReport,
|
|
34
34
|
Statement,
|
|
35
|
+
SubscriberInfo,
|
|
35
36
|
User,
|
|
36
37
|
)
|
|
37
38
|
from ebicsclient.protocol import h005
|
|
@@ -240,13 +241,31 @@ class Client:
|
|
|
240
241
|
ReturnCodeError: the bank reported a non-OK return code (e.g. no data available).
|
|
241
242
|
CryptoError: the order data could not be decrypted.
|
|
242
243
|
"""
|
|
243
|
-
|
|
244
|
-
raise ClientStateError("Download requires the bank's keys; call hpb() first")
|
|
244
|
+
bank_keys = self._require_bank_keys("Download")
|
|
245
245
|
logger.info("Download: opening a %s/%s transaction", btf.service_name, btf.message_name)
|
|
246
246
|
request = h005.build_download_initialisation_request(
|
|
247
|
-
self._bank, self._user, self._keyring,
|
|
247
|
+
self._bank, self._user, self._keyring, bank_keys, btf
|
|
248
|
+
)
|
|
249
|
+
return self._run_download_transaction(request, bank_keys)
|
|
250
|
+
|
|
251
|
+
def _require_bank_keys(self, operation: str) -> BankKeys:
|
|
252
|
+
if self._bank_keys is None:
|
|
253
|
+
raise ClientStateError(f"{operation} requires the bank's keys; call hpb() first")
|
|
254
|
+
return self._bank_keys
|
|
255
|
+
|
|
256
|
+
def _post_verified(self, request: bytes, bank_keys: BankKeys) -> bytes:
|
|
257
|
+
# Every ebicsResponse is authenticated with the bank's X002 signature; verify it
|
|
258
|
+
# BEFORE trusting anything in the response — including its return code.
|
|
259
|
+
response = self._transport.post(request)
|
|
260
|
+
h005.verify_response_signature(response, bank_keys.authentication)
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
def _run_download_transaction(
|
|
264
|
+
self, initialisation_request: bytes, bank_keys: BankKeys
|
|
265
|
+
) -> bytes:
|
|
266
|
+
initialisation = h005.parse_download_initialisation_response(
|
|
267
|
+
self._post_verified(initialisation_request, bank_keys)
|
|
248
268
|
)
|
|
249
|
-
initialisation = h005.parse_download_initialisation_response(self._transport.post(request))
|
|
250
269
|
|
|
251
270
|
# Order data is one base64 stream split into NumSegments pieces: fetch the rest, in
|
|
252
271
|
# order, driven by the authoritative segment count from the initialisation response.
|
|
@@ -259,7 +278,9 @@ class Client:
|
|
|
259
278
|
number,
|
|
260
279
|
last_segment=number == initialisation.num_segments,
|
|
261
280
|
)
|
|
262
|
-
segment = h005.parse_download_segment_response(
|
|
281
|
+
segment = h005.parse_download_segment_response(
|
|
282
|
+
self._post_verified(transfer, bank_keys)
|
|
283
|
+
)
|
|
263
284
|
segments.append(segment.order_data_segment)
|
|
264
285
|
|
|
265
286
|
# Acknowledge the completed transfer so the bank marks the download as delivered.
|
|
@@ -267,7 +288,7 @@ class Client:
|
|
|
267
288
|
receipt = h005.build_download_receipt_request(
|
|
268
289
|
self._bank, self._keyring, initialisation.transaction_id
|
|
269
290
|
)
|
|
270
|
-
h005.parse_download_receipt_response(self.
|
|
291
|
+
h005.parse_download_receipt_response(self._post_verified(receipt, bank_keys))
|
|
271
292
|
logger.info(
|
|
272
293
|
"Download: received %d segment(s) for transaction %s",
|
|
273
294
|
initialisation.num_segments,
|
|
@@ -281,6 +302,60 @@ class Client:
|
|
|
281
302
|
self._keyring.encryption, initialisation.transaction_key, encrypted_order_data
|
|
282
303
|
)
|
|
283
304
|
|
|
305
|
+
def available_order_types(self) -> list[BusinessTransactionFormat]:
|
|
306
|
+
"""Download the order types for which the bank currently holds data (HAA).
|
|
307
|
+
|
|
308
|
+
Runs the administrative ``HAA`` download and returns the Business Transaction
|
|
309
|
+
Formats with data waiting — an authoritative "is there anything to fetch?" check.
|
|
310
|
+
The bank's keys must already be available (call :meth:`hpb` first).
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
The order types with data available (empty when nothing is waiting).
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
ClientStateError: the bank's keys have not been fetched yet (run HPB first).
|
|
317
|
+
TransportError: a request could not be delivered.
|
|
318
|
+
ProtocolError: a response could not be parsed.
|
|
319
|
+
ResponseAuthenticationError: a response failed signature verification.
|
|
320
|
+
ReturnCodeError: the bank rejected the request.
|
|
321
|
+
CryptoError: the order data could not be decrypted.
|
|
322
|
+
"""
|
|
323
|
+
bank_keys = self._require_bank_keys("HAA")
|
|
324
|
+
logger.info("HAA: requesting the order types with data available")
|
|
325
|
+
request = h005.build_admin_download_initialisation_request(
|
|
326
|
+
self._bank, self._user, self._keyring, bank_keys, "HAA"
|
|
327
|
+
)
|
|
328
|
+
return h005.parse_available_order_types(
|
|
329
|
+
self._run_download_transaction(request, bank_keys)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def subscriber_info(self) -> SubscriberInfo:
|
|
333
|
+
"""Download the bank's registered data for this subscriber (HTD).
|
|
334
|
+
|
|
335
|
+
Runs the administrative ``HTD`` download and returns the bank's authoritative
|
|
336
|
+
view of the subscriber: the user's status and name, the permissions granted, and
|
|
337
|
+
every order type registered for the partner — the ground truth for diagnosing
|
|
338
|
+
rejected order types. The bank's keys must already be available (call :meth:`hpb`
|
|
339
|
+
first).
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
The subscriber's registered data.
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
ClientStateError: the bank's keys have not been fetched yet (run HPB first).
|
|
346
|
+
TransportError: a request could not be delivered.
|
|
347
|
+
ProtocolError: a response could not be parsed.
|
|
348
|
+
ResponseAuthenticationError: a response failed signature verification.
|
|
349
|
+
ReturnCodeError: the bank rejected the request.
|
|
350
|
+
CryptoError: the order data could not be decrypted.
|
|
351
|
+
"""
|
|
352
|
+
bank_keys = self._require_bank_keys("HTD")
|
|
353
|
+
logger.info("HTD: requesting the subscriber data registered at the bank")
|
|
354
|
+
request = h005.build_admin_download_initialisation_request(
|
|
355
|
+
self._bank, self._user, self._keyring, bank_keys, "HTD"
|
|
356
|
+
)
|
|
357
|
+
return h005.parse_subscriber_info(self._run_download_transaction(request, bank_keys))
|
|
358
|
+
|
|
284
359
|
def download_statements(self) -> list[Statement]:
|
|
285
360
|
"""Download the end-of-period camt.053 statements and parse them.
|
|
286
361
|
|
|
@@ -390,14 +465,15 @@ class Client:
|
|
|
390
465
|
ReturnCodeError: the bank rejected the upload (e.g. a bad signature or order data).
|
|
391
466
|
CryptoError: the order data could not be signed or encrypted.
|
|
392
467
|
"""
|
|
393
|
-
|
|
394
|
-
raise ClientStateError("Upload requires the bank's keys; call hpb() first")
|
|
468
|
+
bank_keys = self._require_bank_keys("Upload")
|
|
395
469
|
logger.info("Upload: opening a %s/%s transaction", btf.service_name, btf.message_name)
|
|
396
|
-
payload = h005.prepare_upload(self._user, self._keyring,
|
|
470
|
+
payload = h005.prepare_upload(self._user, self._keyring, bank_keys, order_data)
|
|
397
471
|
request = h005.build_upload_initialisation_request(
|
|
398
|
-
self._bank, self._user, self._keyring,
|
|
472
|
+
self._bank, self._user, self._keyring, bank_keys, btf, payload
|
|
473
|
+
)
|
|
474
|
+
transaction_id = h005.parse_upload_initialisation_response(
|
|
475
|
+
self._post_verified(request, bank_keys)
|
|
399
476
|
)
|
|
400
|
-
transaction_id = h005.parse_upload_initialisation_response(self._transport.post(request))
|
|
401
477
|
|
|
402
478
|
segments = payload.order_data_segments
|
|
403
479
|
for number, segment_data in enumerate(segments, start=1):
|
|
@@ -409,7 +485,7 @@ class Client:
|
|
|
409
485
|
segment_data,
|
|
410
486
|
last_segment=number == len(segments),
|
|
411
487
|
)
|
|
412
|
-
h005.parse_upload_transfer_response(self.
|
|
488
|
+
h005.parse_upload_transfer_response(self._post_verified(transfer, bank_keys))
|
|
413
489
|
logger.info(
|
|
414
490
|
"Upload: sent %d segment(s) for transaction %s", len(segments), transaction_id
|
|
415
491
|
)
|
|
@@ -125,6 +125,15 @@ class ProtocolError(EbicsError):
|
|
|
125
125
|
"""The bank's response violated the expected EBICS protocol or could not be parsed."""
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
class ResponseAuthenticationError(ProtocolError):
|
|
129
|
+
"""The bank's response failed authentication-signature verification.
|
|
130
|
+
|
|
131
|
+
Every ``ebicsResponse`` carries the bank's ``AuthSignature`` (X002) over its
|
|
132
|
+
``authenticate="true"`` nodes. A missing or invalid signature means the response
|
|
133
|
+
cannot be attributed to the bank — do not trust its contents. Fail closed.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
128
137
|
class BankKeyMismatchError(ProtocolError):
|
|
129
138
|
"""The bank's HPB public keys did not match the pinned values.
|
|
130
139
|
|
|
@@ -450,6 +450,50 @@ class PaymentStatusReport:
|
|
|
450
450
|
)
|
|
451
451
|
|
|
452
452
|
|
|
453
|
+
@dataclass(frozen=True, slots=True)
|
|
454
|
+
class OrderTypeInfo:
|
|
455
|
+
"""One order type entry from the bank's subscriber data (HTD ``OrderInfo``/``Permission``).
|
|
456
|
+
|
|
457
|
+
Attributes:
|
|
458
|
+
admin_order_type: The administrative order type (``"BTD"``, ``"BTU"``, ``"HPB"``, …).
|
|
459
|
+
service: For ``BTD``/``BTU`` entries, the Business Transaction Format the entry
|
|
460
|
+
refers to; ``None`` for plain administrative order types.
|
|
461
|
+
description: The bank's human-readable description, or ``None`` (permissions
|
|
462
|
+
carry none).
|
|
463
|
+
number_of_signatures_required: How many electronic signatures the order type
|
|
464
|
+
requires (upload entries), or ``None`` if not stated.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
admin_order_type: str
|
|
468
|
+
service: BusinessTransactionFormat | None
|
|
469
|
+
description: str | None
|
|
470
|
+
number_of_signatures_required: int | None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@dataclass(frozen=True, slots=True)
|
|
474
|
+
class SubscriberInfo:
|
|
475
|
+
"""The bank's registered data for this subscriber (the HTD download).
|
|
476
|
+
|
|
477
|
+
The authoritative view of what the bank has configured: the order types offered to
|
|
478
|
+
the partner and the permissions granted to this user. Useful for diagnosing
|
|
479
|
+
``091005``-style rejections — an order type missing here is not registered, whatever
|
|
480
|
+
a catalogue says.
|
|
481
|
+
|
|
482
|
+
Attributes:
|
|
483
|
+
user_id: The subscriber's User ID as registered at the bank.
|
|
484
|
+
user_status: The bank's user-status attribute (a bank-defined code), or ``None``.
|
|
485
|
+
name: The subscriber's registered name, or ``None``.
|
|
486
|
+
permissions: The order types this user may use, in document order.
|
|
487
|
+
order_types: Every order type registered for the partner, in document order.
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
user_id: str
|
|
491
|
+
user_status: str | None
|
|
492
|
+
name: str | None
|
|
493
|
+
permissions: tuple[OrderTypeInfo, ...]
|
|
494
|
+
order_types: tuple[OrderTypeInfo, ...]
|
|
495
|
+
|
|
496
|
+
|
|
453
497
|
class OutputFormat(StrEnum):
|
|
454
498
|
"""The rendering format for the initialisation letter.
|
|
455
499
|
|