painfree 0.1.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.
- painfree-0.1.0/LICENSE +21 -0
- painfree-0.1.0/NOTICE +49 -0
- painfree-0.1.0/PKG-INFO +208 -0
- painfree-0.1.0/README.md +167 -0
- painfree-0.1.0/hatch_build.py +32 -0
- painfree-0.1.0/painfree/__init__.py +15 -0
- painfree-0.1.0/painfree/__main__.py +451 -0
- painfree-0.1.0/painfree/access.py +531 -0
- painfree-0.1.0/painfree/access_api.py +437 -0
- painfree-0.1.0/painfree/accounts.py +732 -0
- painfree-0.1.0/painfree/api.py +823 -0
- painfree-0.1.0/painfree/app.py +468 -0
- painfree-0.1.0/painfree/attempts.py +246 -0
- painfree-0.1.0/painfree/audit.py +289 -0
- painfree-0.1.0/painfree/authn.py +878 -0
- painfree-0.1.0/painfree/camt.py +331 -0
- painfree-0.1.0/painfree/config.py +823 -0
- painfree-0.1.0/painfree/connections.py +336 -0
- painfree-0.1.0/painfree/custody.py +124 -0
- painfree-0.1.0/painfree/db.py +170 -0
- painfree-0.1.0/painfree/dispatcher.py +461 -0
- painfree-0.1.0/painfree/downloader.py +360 -0
- painfree-0.1.0/painfree/ebics3/__init__.py +231 -0
- painfree-0.1.0/painfree/ebics3/btf.py +213 -0
- painfree-0.1.0/painfree/ebics3/canon.py +239 -0
- painfree-0.1.0/painfree/ebics3/certificates.py +142 -0
- painfree-0.1.0/painfree/ebics3/crypto.py +342 -0
- painfree-0.1.0/painfree/ebics3/errors.py +142 -0
- painfree-0.1.0/painfree/ebics3/initialisation.py +520 -0
- painfree-0.1.0/painfree/ebics3/keys.py +181 -0
- painfree-0.1.0/painfree/ebics3/orderdata.py +198 -0
- painfree-0.1.0/painfree/ebics3/pipeline.py +287 -0
- painfree-0.1.0/painfree/ebics3/requests.py +637 -0
- painfree-0.1.0/painfree/ebics3/responses.py +245 -0
- painfree-0.1.0/painfree/ebics3/returncodes.py +265 -0
- painfree-0.1.0/painfree/ebics3/signature.py +220 -0
- painfree-0.1.0/painfree/ebics3/transaction.py +356 -0
- painfree-0.1.0/painfree/ebics3/versions.py +82 -0
- painfree-0.1.0/painfree/errors.py +113 -0
- painfree-0.1.0/painfree/identity.py +603 -0
- painfree-0.1.0/painfree/initialiser.py +314 -0
- painfree-0.1.0/painfree/isoxml.py +191 -0
- painfree-0.1.0/painfree/keyjobs.py +394 -0
- painfree-0.1.0/painfree/keyring.py +692 -0
- painfree-0.1.0/painfree/logging.py +339 -0
- painfree-0.1.0/painfree/migrations/env.py +64 -0
- painfree-0.1.0/painfree/migrations/script.py.mako +24 -0
- painfree-0.1.0/painfree/migrations/versions/0001_audit_log.py +52 -0
- painfree-0.1.0/painfree/migrations/versions/0002_connections_and_keyring.py +89 -0
- painfree-0.1.0/painfree/migrations/versions/0003_payment_orders.py +69 -0
- painfree-0.1.0/painfree/migrations/versions/0004_upload_claim.py +54 -0
- painfree-0.1.0/painfree/migrations/versions/0005_downloads.py +132 -0
- painfree-0.1.0/painfree/migrations/versions/0006_webhooks.py +95 -0
- painfree-0.1.0/painfree/migrations/versions/0007_identity.py +61 -0
- painfree-0.1.0/painfree/migrations/versions/0008_key_jobs.py +55 -0
- painfree-0.1.0/painfree/migrations/versions/0009_status_reports.py +63 -0
- painfree-0.1.0/painfree/migrations/versions/0010_webhook_management.py +89 -0
- painfree-0.1.0/painfree/migrations/versions/0011_schedule_management.py +45 -0
- painfree-0.1.0/painfree/migrations/versions/0012_connection_grants.py +270 -0
- painfree-0.1.0/painfree/migrations/versions/0013_oversight_grants.py +63 -0
- painfree-0.1.0/painfree/migrations/versions/0014_basic_accounts.py +86 -0
- painfree-0.1.0/painfree/migrations/versions/0015_payment_schemes.py +97 -0
- painfree-0.1.0/painfree/oidc.py +437 -0
- painfree-0.1.0/painfree/orders.py +658 -0
- painfree-0.1.0/painfree/pain001.py +345 -0
- painfree-0.1.0/painfree/pain002.py +180 -0
- painfree-0.1.0/painfree/payments.py +202 -0
- painfree-0.1.0/painfree/queue.py +528 -0
- painfree-0.1.0/painfree/reconcile.py +530 -0
- painfree-0.1.0/painfree/rekey.py +316 -0
- painfree-0.1.0/painfree/schedule.py +915 -0
- painfree-0.1.0/painfree/schema.py +978 -0
- painfree-0.1.0/painfree/schemas/README.md +19 -0
- painfree-0.1.0/painfree/schemas/pain.001.001.09.xsd +1114 -0
- painfree-0.1.0/painfree/schemes.py +533 -0
- painfree-0.1.0/painfree/sealing.py +256 -0
- painfree-0.1.0/painfree/sps.py +375 -0
- painfree-0.1.0/painfree/statements.py +379 -0
- painfree-0.1.0/painfree/tokens.py +381 -0
- painfree-0.1.0/painfree/transport.py +123 -0
- painfree-0.1.0/painfree/ui/__init__.py +51 -0
- painfree-0.1.0/painfree/ui/access_views.py +255 -0
- painfree-0.1.0/painfree/ui/account_views.py +161 -0
- painfree-0.1.0/painfree/ui/i18n.py +428 -0
- painfree-0.1.0/painfree/ui/locales/de.json +860 -0
- painfree-0.1.0/painfree/ui/locales/en.json +860 -0
- painfree-0.1.0/painfree/ui/locales/es.json +860 -0
- painfree-0.1.0/painfree/ui/locales/fr.json +860 -0
- painfree-0.1.0/painfree/ui/locales/it.json +860 -0
- painfree-0.1.0/painfree/ui/locales/pl.json +888 -0
- painfree-0.1.0/painfree/ui/locales/review.json +77 -0
- painfree-0.1.0/painfree/ui/notifications.py +191 -0
- painfree-0.1.0/painfree/ui/reference.py +272 -0
- painfree-0.1.0/painfree/ui/reference_views.py +142 -0
- painfree-0.1.0/painfree/ui/rendering.py +312 -0
- painfree-0.1.0/painfree/ui/scheme_forms.py +72 -0
- painfree-0.1.0/painfree/ui/static/01-tokens.css +207 -0
- painfree-0.1.0/painfree/ui/static/02-base.css +125 -0
- painfree-0.1.0/painfree/ui/static/03-chrome.css +427 -0
- painfree-0.1.0/painfree/ui/static/04-components.css +416 -0
- painfree-0.1.0/painfree/ui/static/theme.js +78 -0
- painfree-0.1.0/painfree/ui/templates/_bank.html +27 -0
- painfree-0.1.0/painfree/ui/templates/_icons.html +90 -0
- painfree-0.1.0/painfree/ui/templates/_jobs.html +46 -0
- painfree-0.1.0/painfree/ui/templates/access.html +84 -0
- painfree-0.1.0/painfree/ui/templates/access_subject.html +90 -0
- painfree-0.1.0/painfree/ui/templates/accounts.html +125 -0
- painfree-0.1.0/painfree/ui/templates/api.html +201 -0
- painfree-0.1.0/painfree/ui/templates/audit.html +107 -0
- painfree-0.1.0/painfree/ui/templates/bank_keys.html +67 -0
- painfree-0.1.0/painfree/ui/templates/base.html +266 -0
- painfree-0.1.0/painfree/ui/templates/connection.html +119 -0
- painfree-0.1.0/painfree/ui/templates/connection_access.html +64 -0
- painfree-0.1.0/painfree/ui/templates/connection_edit.html +83 -0
- painfree-0.1.0/painfree/ui/templates/connection_new.html +41 -0
- painfree-0.1.0/painfree/ui/templates/connections.html +47 -0
- painfree-0.1.0/painfree/ui/templates/error.html +20 -0
- painfree-0.1.0/painfree/ui/templates/keys.html +146 -0
- painfree-0.1.0/painfree/ui/templates/letter.html +38 -0
- painfree-0.1.0/painfree/ui/templates/order.html +151 -0
- painfree-0.1.0/painfree/ui/templates/order_replay.html +33 -0
- painfree-0.1.0/painfree/ui/templates/orders.html +56 -0
- painfree-0.1.0/painfree/ui/templates/schedule.html +247 -0
- painfree-0.1.0/painfree/ui/templates/schedule_new.html +88 -0
- painfree-0.1.0/painfree/ui/templates/schedules.html +106 -0
- painfree-0.1.0/painfree/ui/templates/sign_in.html +33 -0
- painfree-0.1.0/painfree/ui/templates/signed_out.html +19 -0
- painfree-0.1.0/painfree/ui/templates/statement.html +33 -0
- painfree-0.1.0/painfree/ui/templates/statements.html +49 -0
- painfree-0.1.0/painfree/ui/templates/status_codes.html +37 -0
- painfree-0.1.0/painfree/ui/templates/webhook.html +144 -0
- painfree-0.1.0/painfree/ui/templates/webhook_new.html +56 -0
- painfree-0.1.0/painfree/ui/templates/webhook_secret.html +49 -0
- painfree-0.1.0/painfree/ui/templates/webhooks.html +69 -0
- painfree-0.1.0/painfree/ui/views.py +939 -0
- painfree-0.1.0/painfree/webhooks.py +966 -0
- painfree-0.1.0/painfree/worker.py +558 -0
- painfree-0.1.0/painfree/wrapping.py +267 -0
- painfree-0.1.0/pyproject.toml +119 -0
- painfree-0.1.0/tests/basic_world.py +153 -0
- painfree-0.1.0/tests/conftest.py +808 -0
- painfree-0.1.0/tests/fixtures/camt.052.001.08.xml +106 -0
- painfree-0.1.0/tests/fixtures/camt.053.001.08.xml +241 -0
- painfree-0.1.0/tests/fixtures/camt.054.001.09.xml +119 -0
- painfree-0.1.0/tests/fixtures/pain.002.001.10.xml +76 -0
- painfree-0.1.0/tests/idp.py +226 -0
- painfree-0.1.0/tests/schemas/README.md +38 -0
- painfree-0.1.0/tests/schemas/camt.052.001.08.xsd +2070 -0
- painfree-0.1.0/tests/schemas/camt.053.001.08.xsd +2070 -0
- painfree-0.1.0/tests/schemas/camt.054.001.09.xsd +2012 -0
- painfree-0.1.0/tests/schemas/pain.002.001.10.xsd +1185 -0
- painfree-0.1.0/tests/test_ebics3_initialisation.py +333 -0
- painfree-0.1.0/tests/test_ebics3_keys.py +236 -0
- painfree-0.1.0/tests/test_ebics3_pipeline.py +202 -0
- painfree-0.1.0/tests/test_ebics3_requests.py +471 -0
- painfree-0.1.0/tests/test_ebics3_returncodes.py +204 -0
- painfree-0.1.0/tests/test_ebics3_signature.py +223 -0
- painfree-0.1.0/tests/test_ebics3_transaction.py +366 -0
- painfree-0.1.0/tests/test_packaging.py +216 -0
- painfree-0.1.0/tests/test_service_access.py +791 -0
- painfree-0.1.0/tests/test_service_api_page.py +316 -0
- painfree-0.1.0/tests/test_service_app.py +305 -0
- painfree-0.1.0/tests/test_service_audit.py +139 -0
- painfree-0.1.0/tests/test_service_audit_actions.py +504 -0
- painfree-0.1.0/tests/test_service_audit_page.py +259 -0
- painfree-0.1.0/tests/test_service_authz.py +746 -0
- painfree-0.1.0/tests/test_service_basic_access.py +225 -0
- painfree-0.1.0/tests/test_service_basic_auth.py +571 -0
- painfree-0.1.0/tests/test_service_basic_deployment.py +486 -0
- painfree-0.1.0/tests/test_service_camt.py +288 -0
- painfree-0.1.0/tests/test_service_config.py +212 -0
- painfree-0.1.0/tests/test_service_connections.py +185 -0
- painfree-0.1.0/tests/test_service_custody.py +322 -0
- painfree-0.1.0/tests/test_service_db.py +278 -0
- painfree-0.1.0/tests/test_service_dispatcher.py +578 -0
- painfree-0.1.0/tests/test_service_downloader.py +323 -0
- painfree-0.1.0/tests/test_service_i18n.py +689 -0
- painfree-0.1.0/tests/test_service_keyjobs.py +384 -0
- painfree-0.1.0/tests/test_service_keyring.py +349 -0
- painfree-0.1.0/tests/test_service_logging.py +242 -0
- painfree-0.1.0/tests/test_service_orders.py +399 -0
- painfree-0.1.0/tests/test_service_oversight.py +698 -0
- painfree-0.1.0/tests/test_service_pain001.py +159 -0
- painfree-0.1.0/tests/test_service_payments_api.py +241 -0
- painfree-0.1.0/tests/test_service_queue.py +306 -0
- painfree-0.1.0/tests/test_service_reconcile.py +549 -0
- painfree-0.1.0/tests/test_service_rekey.py +360 -0
- painfree-0.1.0/tests/test_service_schedule.py +388 -0
- painfree-0.1.0/tests/test_service_schedule_admin.py +753 -0
- painfree-0.1.0/tests/test_service_scheme_fallback.py +825 -0
- painfree-0.1.0/tests/test_service_schemes.py +514 -0
- painfree-0.1.0/tests/test_service_sealing.py +116 -0
- painfree-0.1.0/tests/test_service_sps.py +230 -0
- painfree-0.1.0/tests/test_service_statements.py +312 -0
- painfree-0.1.0/tests/test_service_tokens.py +341 -0
- painfree-0.1.0/tests/test_service_ui.py +476 -0
- painfree-0.1.0/tests/test_service_ui_chrome.py +476 -0
- painfree-0.1.0/tests/test_service_webhook_admin.py +565 -0
- painfree-0.1.0/tests/test_service_webhooks.py +467 -0
- painfree-0.1.0/tests/test_service_worker.py +459 -0
painfree-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 reyemb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
painfree-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
painfree
|
|
2
|
+
Copyright (c) 2026 reyemb
|
|
3
|
+
|
|
4
|
+
This product is licensed under the MIT License. The full text is in LICENSE,
|
|
5
|
+
and that notice covers everything in this distribution.
|
|
6
|
+
|
|
7
|
+
--------------------------------------------------------------------------------
|
|
8
|
+
Third-party notices
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
ebics-api/ebics-client-php
|
|
12
|
+
https://github.com/ebics-api/ebics-client-php
|
|
13
|
+
|
|
14
|
+
The EBICS 3.0 protocol engine in `painfree/ebics3/` is a Python port of
|
|
15
|
+
`ebics-api/ebics-client-php`. It is not a copy: the code was rewritten in
|
|
16
|
+
Python against a different XML and cryptography stack, and it does not
|
|
17
|
+
reproduce the PHP source. It does follow that project's structure and its
|
|
18
|
+
solutions to the parts of EBICS the specification leaves ambiguous, which is
|
|
19
|
+
enough that its licence travels with this distribution rather than being left
|
|
20
|
+
behind in a commit message.
|
|
21
|
+
|
|
22
|
+
`ebics-client-php` is MIT licensed. Its notice, reproduced in full:
|
|
23
|
+
|
|
24
|
+
MIT License
|
|
25
|
+
|
|
26
|
+
Copyright (c) 2019-2020 EBICS API
|
|
27
|
+
|
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
29
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
30
|
+
in the Software without restriction, including without limitation the rights
|
|
31
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
32
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
33
|
+
furnished to do so, subject to the following conditions:
|
|
34
|
+
|
|
35
|
+
The above copyright notice and this permission notice shall be included in all
|
|
36
|
+
copies or substantial portions of the Software.
|
|
37
|
+
|
|
38
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
39
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
40
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
41
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
42
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
43
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
44
|
+
SOFTWARE.
|
|
45
|
+
|
|
46
|
+
No other project's source was read or adapted into `painfree/ebics3/`. The
|
|
47
|
+
Ruby `epics` and the Python `fintech` implementations are used only as
|
|
48
|
+
black-box oracles: they are executed and their output is compared, and neither
|
|
49
|
+
contributes code to this distribution.
|
painfree-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: painfree
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-hosted middleware that accepts JSON payment instructions and submits them to banks over EBICS 3.0
|
|
5
|
+
Project-URL: Homepage, https://github.com/reyemb/painfree-ebics-middleware-python
|
|
6
|
+
Project-URL: Source, https://github.com/reyemb/painfree-ebics-middleware-python
|
|
7
|
+
Project-URL: Issues, https://github.com/reyemb/painfree-ebics-middleware-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/reyemb/painfree-ebics-middleware-python/releases
|
|
9
|
+
Author-email: reyemb <m3y3r.bastian@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: banking,camt,ebics,iso20022,pain.001,payments,sepa
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: alembic>=1.13
|
|
27
|
+
Requires-Dist: argon2-cffi>=23.1
|
|
28
|
+
Requires-Dist: cryptography>=42
|
|
29
|
+
Requires-Dist: fastapi>=0.115
|
|
30
|
+
Requires-Dist: jinja2>=3.1
|
|
31
|
+
Requires-Dist: lxml>=5.2
|
|
32
|
+
Requires-Dist: psycopg[binary]>=3.2
|
|
33
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
34
|
+
Requires-Dist: pyjwt==2.11.0
|
|
35
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
36
|
+
Requires-Dist: uvicorn>=0.30
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# painfree
|
|
43
|
+
|
|
44
|
+
**JSON in, EBICS out.** Self-hosted middleware that accepts authenticated HTTP
|
|
45
|
+
requests and submits them to your banks over EBICS 3.0, so payment files stop
|
|
46
|
+
being something a human uploads by hand.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
your system ──JSON/HTTPS──▶ painfree ──EBICS 3.0 (H005)──▶ bank A
|
|
50
|
+
(OIDC) │ bank B
|
|
51
|
+
│ bank C
|
|
52
|
+
└──webhook──▶ your system
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
POST a payment as JSON. painfree validates it, builds a compliant `pain.001`,
|
|
56
|
+
signs and encrypts it per the EBICS protocol, uploads it to the right bank
|
|
57
|
+
connection, tracks the order to acknowledgement, and calls your webhook when the
|
|
58
|
+
status changes. It runs the other direction too: scheduled downloads of
|
|
59
|
+
`camt.052/053/054` and `pain.002`, normalised to JSON.
|
|
60
|
+
|
|
61
|
+
A console at `/ui` handles everything that is not a payment: bank connections,
|
|
62
|
+
EBICS initialisation, key management, order history, replay, audit.
|
|
63
|
+
|
|
64
|
+
## Status
|
|
65
|
+
|
|
66
|
+
**Early.** Not in production. Not suitable for anyone else's money.
|
|
67
|
+
|
|
68
|
+
## Run it
|
|
69
|
+
|
|
70
|
+
Four lines, against the published image. Nothing is built.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cp deploy/production.env.example .env # pinned to ghcr.io/reyemb/painfree
|
|
74
|
+
deploy/init-secrets.sh # four secret files, generated once
|
|
75
|
+
docker compose up -d # db, api, worker, TLS proxy
|
|
76
|
+
docker compose exec api python -m painfree create-admin you # no OIDC only
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open the address in `.env`. Set
|
|
80
|
+
`PAINFREE_SITE_ADDRESS=https://painfree.localhost` to run it on a machine with
|
|
81
|
+
no DNS: the proxy issues that certificate from its own local CA.
|
|
82
|
+
`podman-compose` works identically, and `deploy/build-image.sh` builds the
|
|
83
|
+
image yourself if you would rather not run someone else's.
|
|
84
|
+
|
|
85
|
+
Everything that is not secret lives in `.env`. Every secret is a file under
|
|
86
|
+
`deploy/secrets/`, generated once, mounted at runtime, and never baked into the
|
|
87
|
+
image or committed.
|
|
88
|
+
|
|
89
|
+
## Submitting a payment
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -X POST https://painfree.localhost/v1/connections/acme-ubs/payments \
|
|
93
|
+
-u you:the-password-you-set \
|
|
94
|
+
-H 'Idempotency-Key: caller-idem-0001' \
|
|
95
|
+
-H 'Content-Type: application/json' -d @payment.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`202 Accepted` with an `order_id` to poll at `/v1/orders/{order_id}`. Accepted
|
|
99
|
+
means accepted **for processing**, not that the bank has it. The instruction is
|
|
100
|
+
validated against the ISO 20022 schema *and* the Swiss Payment Standards rules
|
|
101
|
+
before anything is signed, so a malformed QR reference comes back immediately
|
|
102
|
+
naming the field, rather than from the bank two hours later.
|
|
103
|
+
|
|
104
|
+
One idempotency key stays one order, and an order can end accepted at most once.
|
|
105
|
+
|
|
106
|
+
**Payment schemes.** A payment carries `normal`, `instant`, or
|
|
107
|
+
`instant_or_normal`, which tries instant and sends an ordinary transfer if the
|
|
108
|
+
bank definitively refuses. A timeout, a dropped connection or an answer that
|
|
109
|
+
will not parse is an unknown outcome rather than a refusal: the order is
|
|
110
|
+
retried carrying the message it already had, so nothing is sent twice.
|
|
111
|
+
|
|
112
|
+
`/ui/api` renders the request body, every endpoint and the privilege it demands,
|
|
113
|
+
generated from the router rather than written down, and links the OpenAPI
|
|
114
|
+
documents beside it.
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
**Two processes, one image.** `api` serves HTTP and is refused the custody
|
|
119
|
+
secret. `worker` holds it, and does every upload, download and key operation. An
|
|
120
|
+
`api` process handed the secret does not start, and production refuses to run
|
|
121
|
+
both halves in one process. That is a security boundary rather than a scaling
|
|
122
|
+
one: the process answering requests has nothing to decrypt a private key with.
|
|
123
|
+
|
|
124
|
+
**Authentication.** OIDC where you have a provider (browser SSO with PKCE, JWT
|
|
125
|
+
bearer for machines), and HTTP Basic against local accounts where you do not.
|
|
126
|
+
Production refuses to start in development mode. A process accepts one kind of
|
|
127
|
+
credential and never two.
|
|
128
|
+
|
|
129
|
+
**Authorisation.** Two roles: `admin`, which may grant access, and `member`,
|
|
130
|
+
which may not. Everything a member holds is a grant naming a subject, a bank
|
|
131
|
+
connection and a level. `viewer` reads; `operator` also submits payments,
|
|
132
|
+
replays orders and manages schedules. Grants are read from the database on
|
|
133
|
+
every request, so revoking somebody takes effect on their next request. A
|
|
134
|
+
connection a caller holds no grant on answers `404`, exactly like one that was
|
|
135
|
+
never registered.
|
|
136
|
+
|
|
137
|
+
**Webhooks.** At-least-once. The event is written in the same transaction as
|
|
138
|
+
the fact it reports, so a crash cannot lose it, and a redelivery carries the
|
|
139
|
+
same `event_id`: deduplicate on that. Each request is signed
|
|
140
|
+
`X-Painfree-Signature: v1=<hex>`, an HMAC-SHA256 over `"<timestamp>.<raw
|
|
141
|
+
body>"` with the subscription's own secret. Verify over the bytes you received,
|
|
142
|
+
not over a re-serialisation.
|
|
143
|
+
|
|
144
|
+
**Keys.** INI, HIA and HPB from the console, with a printable INI letter,
|
|
145
|
+
renewal and suspension. The bank's keys are trusted only once you have compared
|
|
146
|
+
their fingerprints against the letter the bank sent, on a page where nothing is
|
|
147
|
+
pre-filled and declining is as easy as accepting.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
Every setting is an environment variable, and any of them can be read from a
|
|
152
|
+
file instead by setting `PAINFREE_X_FILE`. The ones a deployment sets are in
|
|
153
|
+
`deploy/production.env.example`; the rest are documented where they are
|
|
154
|
+
defined, in `painfree/config.py`.
|
|
155
|
+
|
|
156
|
+
| Variable | Default | What |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `PAINFREE_ENVIRONMENT` | `development` | `production` refuses SQLite, `combined`, and development auth |
|
|
159
|
+
| `PAINFREE_ROLE` | `combined` | `api`, `worker` or `combined` |
|
|
160
|
+
| `PAINFREE_DATABASE_URL` | SQLite file | PostgreSQL in production |
|
|
161
|
+
| `PAINFREE_KEY_ENCRYPTION_SECRET` | none | seals every stored private key; required of a worker, refused for an `api` |
|
|
162
|
+
| `PAINFREE_AUTH_MODE` | derived | `oidc`, `basic`, or `development`, which production refuses |
|
|
163
|
+
| `PAINFREE_TLS_TERMINATED_UPSTREAM` | `false` | `basic` in production will not start without it |
|
|
164
|
+
| `PAINFREE_OIDC_ISSUER` / `_CLIENT_ID` / `_REDIRECT_URI` | none | all three required for `oidc` |
|
|
165
|
+
|
|
166
|
+
Logs are one JSON object per line on stdout. No token, authorization code,
|
|
167
|
+
session id or client secret ever reaches the stream.
|
|
168
|
+
|
|
169
|
+
## Backup, and the one thing no backup covers
|
|
170
|
+
|
|
171
|
+
`deploy/backup.sh` dumps the database and `deploy/restore.sh` restores it. That
|
|
172
|
+
covers the sealed keys, the order history, the idempotency ledger and the audit
|
|
173
|
+
trail.
|
|
174
|
+
|
|
175
|
+
It does not cover `PAINFREE_KEY_ENCRYPTION_SECRET`, which is in no backup and
|
|
176
|
+
which nothing recovers. Lose it and every sealed key is unreadable by anyone,
|
|
177
|
+
and each bank connection has to be re-keyed on paper. Rotating it is `python -m
|
|
178
|
+
painfree rekey`, run with both secrets present, which re-seals every row and
|
|
179
|
+
fails loudly on any it could not move.
|
|
180
|
+
|
|
181
|
+
## Develop
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
uv sync --extra dev
|
|
185
|
+
uv run python -m painfree # SQLite, migrated at startup, on :8000
|
|
186
|
+
uv run pytest
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Development mode authenticates from `X-Painfree-Dev-Principal` and
|
|
190
|
+
`X-Painfree-Dev-Roles`, so a checkout runs and the suite passes with no provider
|
|
191
|
+
to point at. It is still an authentication step, and production refuses to start
|
|
192
|
+
in it.
|
|
193
|
+
|
|
194
|
+
There is one importable package, `painfree`; the EBICS 3.0 engine is the
|
|
195
|
+
subpackage `painfree/ebics3/`, which imports nothing from the service around it
|
|
196
|
+
and needs only `lxml` and `cryptography`.
|
|
197
|
+
|
|
198
|
+
## Licence
|
|
199
|
+
|
|
200
|
+
**MIT.** Copyright (c) 2026 reyemb. The full text is in
|
|
201
|
+
[LICENSE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/LICENSE).
|
|
202
|
+
|
|
203
|
+
The EBICS 3.0 engine in `painfree/ebics3/` is a Python port of
|
|
204
|
+
[`ebics-api/ebics-client-php`](https://github.com/ebics-api/ebics-client-php),
|
|
205
|
+
which is MIT too. Its notice is reproduced in full in
|
|
206
|
+
[NOTICE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/NOTICE),
|
|
207
|
+
which every copy has to keep, and both files ship inside the wheel, the
|
|
208
|
+
source distribution and the container image.
|
painfree-0.1.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# painfree
|
|
2
|
+
|
|
3
|
+
**JSON in, EBICS out.** Self-hosted middleware that accepts authenticated HTTP
|
|
4
|
+
requests and submits them to your banks over EBICS 3.0, so payment files stop
|
|
5
|
+
being something a human uploads by hand.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
your system ──JSON/HTTPS──▶ painfree ──EBICS 3.0 (H005)──▶ bank A
|
|
9
|
+
(OIDC) │ bank B
|
|
10
|
+
│ bank C
|
|
11
|
+
└──webhook──▶ your system
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
POST a payment as JSON. painfree validates it, builds a compliant `pain.001`,
|
|
15
|
+
signs and encrypts it per the EBICS protocol, uploads it to the right bank
|
|
16
|
+
connection, tracks the order to acknowledgement, and calls your webhook when the
|
|
17
|
+
status changes. It runs the other direction too: scheduled downloads of
|
|
18
|
+
`camt.052/053/054` and `pain.002`, normalised to JSON.
|
|
19
|
+
|
|
20
|
+
A console at `/ui` handles everything that is not a payment: bank connections,
|
|
21
|
+
EBICS initialisation, key management, order history, replay, audit.
|
|
22
|
+
|
|
23
|
+
## Status
|
|
24
|
+
|
|
25
|
+
**Early.** Not in production. Not suitable for anyone else's money.
|
|
26
|
+
|
|
27
|
+
## Run it
|
|
28
|
+
|
|
29
|
+
Four lines, against the published image. Nothing is built.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cp deploy/production.env.example .env # pinned to ghcr.io/reyemb/painfree
|
|
33
|
+
deploy/init-secrets.sh # four secret files, generated once
|
|
34
|
+
docker compose up -d # db, api, worker, TLS proxy
|
|
35
|
+
docker compose exec api python -m painfree create-admin you # no OIDC only
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Open the address in `.env`. Set
|
|
39
|
+
`PAINFREE_SITE_ADDRESS=https://painfree.localhost` to run it on a machine with
|
|
40
|
+
no DNS: the proxy issues that certificate from its own local CA.
|
|
41
|
+
`podman-compose` works identically, and `deploy/build-image.sh` builds the
|
|
42
|
+
image yourself if you would rather not run someone else's.
|
|
43
|
+
|
|
44
|
+
Everything that is not secret lives in `.env`. Every secret is a file under
|
|
45
|
+
`deploy/secrets/`, generated once, mounted at runtime, and never baked into the
|
|
46
|
+
image or committed.
|
|
47
|
+
|
|
48
|
+
## Submitting a payment
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
curl -X POST https://painfree.localhost/v1/connections/acme-ubs/payments \
|
|
52
|
+
-u you:the-password-you-set \
|
|
53
|
+
-H 'Idempotency-Key: caller-idem-0001' \
|
|
54
|
+
-H 'Content-Type: application/json' -d @payment.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`202 Accepted` with an `order_id` to poll at `/v1/orders/{order_id}`. Accepted
|
|
58
|
+
means accepted **for processing**, not that the bank has it. The instruction is
|
|
59
|
+
validated against the ISO 20022 schema *and* the Swiss Payment Standards rules
|
|
60
|
+
before anything is signed, so a malformed QR reference comes back immediately
|
|
61
|
+
naming the field, rather than from the bank two hours later.
|
|
62
|
+
|
|
63
|
+
One idempotency key stays one order, and an order can end accepted at most once.
|
|
64
|
+
|
|
65
|
+
**Payment schemes.** A payment carries `normal`, `instant`, or
|
|
66
|
+
`instant_or_normal`, which tries instant and sends an ordinary transfer if the
|
|
67
|
+
bank definitively refuses. A timeout, a dropped connection or an answer that
|
|
68
|
+
will not parse is an unknown outcome rather than a refusal: the order is
|
|
69
|
+
retried carrying the message it already had, so nothing is sent twice.
|
|
70
|
+
|
|
71
|
+
`/ui/api` renders the request body, every endpoint and the privilege it demands,
|
|
72
|
+
generated from the router rather than written down, and links the OpenAPI
|
|
73
|
+
documents beside it.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
**Two processes, one image.** `api` serves HTTP and is refused the custody
|
|
78
|
+
secret. `worker` holds it, and does every upload, download and key operation. An
|
|
79
|
+
`api` process handed the secret does not start, and production refuses to run
|
|
80
|
+
both halves in one process. That is a security boundary rather than a scaling
|
|
81
|
+
one: the process answering requests has nothing to decrypt a private key with.
|
|
82
|
+
|
|
83
|
+
**Authentication.** OIDC where you have a provider (browser SSO with PKCE, JWT
|
|
84
|
+
bearer for machines), and HTTP Basic against local accounts where you do not.
|
|
85
|
+
Production refuses to start in development mode. A process accepts one kind of
|
|
86
|
+
credential and never two.
|
|
87
|
+
|
|
88
|
+
**Authorisation.** Two roles: `admin`, which may grant access, and `member`,
|
|
89
|
+
which may not. Everything a member holds is a grant naming a subject, a bank
|
|
90
|
+
connection and a level. `viewer` reads; `operator` also submits payments,
|
|
91
|
+
replays orders and manages schedules. Grants are read from the database on
|
|
92
|
+
every request, so revoking somebody takes effect on their next request. A
|
|
93
|
+
connection a caller holds no grant on answers `404`, exactly like one that was
|
|
94
|
+
never registered.
|
|
95
|
+
|
|
96
|
+
**Webhooks.** At-least-once. The event is written in the same transaction as
|
|
97
|
+
the fact it reports, so a crash cannot lose it, and a redelivery carries the
|
|
98
|
+
same `event_id`: deduplicate on that. Each request is signed
|
|
99
|
+
`X-Painfree-Signature: v1=<hex>`, an HMAC-SHA256 over `"<timestamp>.<raw
|
|
100
|
+
body>"` with the subscription's own secret. Verify over the bytes you received,
|
|
101
|
+
not over a re-serialisation.
|
|
102
|
+
|
|
103
|
+
**Keys.** INI, HIA and HPB from the console, with a printable INI letter,
|
|
104
|
+
renewal and suspension. The bank's keys are trusted only once you have compared
|
|
105
|
+
their fingerprints against the letter the bank sent, on a page where nothing is
|
|
106
|
+
pre-filled and declining is as easy as accepting.
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
Every setting is an environment variable, and any of them can be read from a
|
|
111
|
+
file instead by setting `PAINFREE_X_FILE`. The ones a deployment sets are in
|
|
112
|
+
`deploy/production.env.example`; the rest are documented where they are
|
|
113
|
+
defined, in `painfree/config.py`.
|
|
114
|
+
|
|
115
|
+
| Variable | Default | What |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `PAINFREE_ENVIRONMENT` | `development` | `production` refuses SQLite, `combined`, and development auth |
|
|
118
|
+
| `PAINFREE_ROLE` | `combined` | `api`, `worker` or `combined` |
|
|
119
|
+
| `PAINFREE_DATABASE_URL` | SQLite file | PostgreSQL in production |
|
|
120
|
+
| `PAINFREE_KEY_ENCRYPTION_SECRET` | none | seals every stored private key; required of a worker, refused for an `api` |
|
|
121
|
+
| `PAINFREE_AUTH_MODE` | derived | `oidc`, `basic`, or `development`, which production refuses |
|
|
122
|
+
| `PAINFREE_TLS_TERMINATED_UPSTREAM` | `false` | `basic` in production will not start without it |
|
|
123
|
+
| `PAINFREE_OIDC_ISSUER` / `_CLIENT_ID` / `_REDIRECT_URI` | none | all three required for `oidc` |
|
|
124
|
+
|
|
125
|
+
Logs are one JSON object per line on stdout. No token, authorization code,
|
|
126
|
+
session id or client secret ever reaches the stream.
|
|
127
|
+
|
|
128
|
+
## Backup, and the one thing no backup covers
|
|
129
|
+
|
|
130
|
+
`deploy/backup.sh` dumps the database and `deploy/restore.sh` restores it. That
|
|
131
|
+
covers the sealed keys, the order history, the idempotency ledger and the audit
|
|
132
|
+
trail.
|
|
133
|
+
|
|
134
|
+
It does not cover `PAINFREE_KEY_ENCRYPTION_SECRET`, which is in no backup and
|
|
135
|
+
which nothing recovers. Lose it and every sealed key is unreadable by anyone,
|
|
136
|
+
and each bank connection has to be re-keyed on paper. Rotating it is `python -m
|
|
137
|
+
painfree rekey`, run with both secrets present, which re-seals every row and
|
|
138
|
+
fails loudly on any it could not move.
|
|
139
|
+
|
|
140
|
+
## Develop
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv sync --extra dev
|
|
144
|
+
uv run python -m painfree # SQLite, migrated at startup, on :8000
|
|
145
|
+
uv run pytest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Development mode authenticates from `X-Painfree-Dev-Principal` and
|
|
149
|
+
`X-Painfree-Dev-Roles`, so a checkout runs and the suite passes with no provider
|
|
150
|
+
to point at. It is still an authentication step, and production refuses to start
|
|
151
|
+
in it.
|
|
152
|
+
|
|
153
|
+
There is one importable package, `painfree`; the EBICS 3.0 engine is the
|
|
154
|
+
subpackage `painfree/ebics3/`, which imports nothing from the service around it
|
|
155
|
+
and needs only `lxml` and `cryptography`.
|
|
156
|
+
|
|
157
|
+
## Licence
|
|
158
|
+
|
|
159
|
+
**MIT.** Copyright (c) 2026 reyemb. The full text is in
|
|
160
|
+
[LICENSE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/LICENSE).
|
|
161
|
+
|
|
162
|
+
The EBICS 3.0 engine in `painfree/ebics3/` is a Python port of
|
|
163
|
+
[`ebics-api/ebics-client-php`](https://github.com/ebics-api/ebics-client-php),
|
|
164
|
+
which is MIT too. Its notice is reproduced in full in
|
|
165
|
+
[NOTICE](https://github.com/reyemb/painfree-ebics-middleware-python/blob/main/NOTICE),
|
|
166
|
+
which every copy has to keep, and both files ship inside the wheel, the
|
|
167
|
+
source distribution and the container image.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Keep `.gitignore` out of the source distribution.
|
|
2
|
+
|
|
3
|
+
The build backend force-includes every VCS exclusion file in an sdist, and a
|
|
4
|
+
force-include is not subject to the target's `exclude` patterns, so this is the
|
|
5
|
+
only place it can be dropped.
|
|
6
|
+
|
|
7
|
+
It matters because `.gitignore` in this repository is not just build noise: it
|
|
8
|
+
names the working paths that are deliberately kept local, and a source
|
|
9
|
+
distribution published to an index is exactly where that list should not
|
|
10
|
+
appear. The sdist is meant to be the source a rebuild needs, and a rebuild
|
|
11
|
+
needs none of it.
|
|
12
|
+
|
|
13
|
+
Scoped to the sdist target, so the wheel build -- which is what the container
|
|
14
|
+
image runs, from a directory that holds only `pyproject.toml` and the package
|
|
15
|
+
-- never has to find this file.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GitignoreOutOfSdistHook(BuildHookInterface):
|
|
27
|
+
PLUGIN_NAME = "gitignore-out-of-sdist"
|
|
28
|
+
|
|
29
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
30
|
+
forced = build_data.get("force_include", {})
|
|
31
|
+
for source in [p for p in forced if os.path.basename(p) == ".gitignore"]:
|
|
32
|
+
del forced[source]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""painfree -- JSON in, EBICS out.
|
|
2
|
+
|
|
3
|
+
The service layer lives directly under this package; the EBICS 3.0 protocol
|
|
4
|
+
engine is the subpackage :mod:`painfree.ebics3`, which must not import from any
|
|
5
|
+
other part of ``painfree``. This module is deliberately empty apart from the
|
|
6
|
+
version, so that importing the engine does not drag FastAPI, SQLAlchemy or a
|
|
7
|
+
database driver in with it -- that is what keeps the engine releasable on its
|
|
8
|
+
own.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|