pax_global_header 0000666 0000000 0000000 00000000064 14421014606 0014510 g ustar 00root root 0000000 0000000 52 comment=5989160fb74ffee3f87119aa4db16391f644267d
python-fints-4.0.0/ 0000775 0000000 0000000 00000000000 14421014606 0014153 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/.github/ 0000775 0000000 0000000 00000000000 14421014606 0015513 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14421014606 0017676 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001203 14421014606 0022364 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: General bug report
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Bank I tested this with*
Name of the bank:
FinTS URL:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Code required to reproduce**
```
Your sample code
```
**Log output / error message**
**Additional context**
Add any other context about the problem here.
python-fints-4.0.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14421014606 0017550 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/.github/workflows/tests.yml 0000664 0000000 0000000 00000001473 14421014606 0021442 0 ustar 00root root 0000000 0000000 name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
name: Tests
strategy:
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: python -m pip install -U pip wheel coverage codecov
- name: Install Dependencies
run: python -m pip install -Ur requirements.txt pytest pytest-mock
- name: Run tests
run: coverage run -m pytest tests
- name: Upload coverage
run: codecov python-fints-4.0.0/.gitignore 0000664 0000000 0000000 00000000141 14421014606 0016137 0 ustar 00root root 0000000 0000000 __pycache__/
build/
dist/
*.egg-info
env
.idea/
test*.py
!tests/*
tests/messages/private_*
*.pyc
python-fints-4.0.0/LICENSE.txt 0000664 0000000 0000000 00000016743 14421014606 0016011 0 ustar 00root root 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
python-fints-4.0.0/MANIFEST.in 0000664 0000000 0000000 00000000067 14421014606 0015714 0 ustar 00root root 0000000 0000000 recursive-include tests *.py *.bin
include LICENSE.txt
python-fints-4.0.0/README.md 0000664 0000000 0000000 00000003026 14421014606 0015433 0 ustar 00root root 0000000 0000000 PyFinTS
=======
This is a pure-python implementation of FinTS (formerly known as HBCI), a
online-banking protocol commonly supported by German banks.
[Read our documentation for more info](https://python-fints.readthedocs.io)
Maintenance Status
------------------
This project is maintained, but with limited capacity. Working on this is takes a lot of time and testing since all banks do things differently and once you move a part here, you break an unexpected one over there. Therefore: Bugs will only be fixed by me if they occur with a bank where I have an account. New features will only be developed if I need them. PRs will be merged if they either have a very low risk of breaking things elsewhere (e.g. purely adding new commands) or if I can test them. In any case, things might take a little time until I have the bandwidth to focus on them. Sorry about that :(
Limitations
-----------
* Only FinTS 3.0 is supported
* Only PIN/TAN authentication is supported, no signature cards
* Only the following operations are supported:
* Fetching bank statements
* Fetching balances
* Fetching holdings
* SEPA transfers and debits (only with required TAN and with specific TAN methods)
* Supports Python 3.6+
Credits and License
-------------------
This library is maintained by Raphael Michel
and features major contributions by Henryk Plötz.
Further thanks for improving this library go out to:
Daniel Nowak, Patrick Braune, Mathias Dalheimer, Christopher Grebs, Markus Schindler, and many more.
License: LGPL
python-fints-4.0.0/docs/ 0000775 0000000 0000000 00000000000 14421014606 0015103 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/docs/.gitignore 0000664 0000000 0000000 00000000010 14421014606 0017062 0 ustar 00root root 0000000 0000000 _build/
python-fints-4.0.0/docs/Makefile 0000664 0000000 0000000 00000017323 14421014606 0016551 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-i18nfield.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-i18nfield.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-i18nfield"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-i18nfield"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
python-fints-4.0.0/docs/client.rst 0000664 0000000 0000000 00000006073 14421014606 0017121 0 ustar 00root root 0000000 0000000 .. _client:
The client object
=================
.. _client-state:
Storing and restoring client state
----------------------------------
The :class:`~fints.client.FinTS3Client` object keeps some internal state that's beneficial to keep
across invocations. This includes
* A system identifier that uniquely identifies this particular FinTS endpoint
* The Bank Parameter Data (BPD) with information about the bank and its advertised capabilities
* The User Parameter Data (UPD) with information about the user account and allowed actions
.. autoclass:: fints.client.FinTS3Client
:members: deconstruct, set_data
:noindex:
:undoc-members:
Using the :func:`~fints.client.FinTS3Client.deconstruct`/:func:`~fints.client.FinTS3Client.set_data`
facility is purely optional for reading operations, but may speed up the process because the BPD/UPD
can be cached and need not be transmitted again.
It may be required to use the facility for transaction operations if both parts of a two-step transaction
cannot be completed with the same :class:`~fints.client.FinTS3Client` object.
The :func:`~fints.client.FinTS3Client.deconstruct` parameter `include_private` (defaults to `False`) enables
including the User Parameter Data in the datablob. Set this to `True` if you can sufficiently ensure the
privacy of the returned datablob (mostly: user name and account numbers).
If your system manages multiple users/identity contexts, you SHOULD keep distinct datablobs per
user or context.
You SHOULD NOT call any other methods on the :class:`~fints.client.FinTS3Client` object
after calling :func:`~fints.client.FinTS3Client.deconstruct`.
Keeping the dialog open
-----------------------
All FinTS operations happen in the context of a so-called "dialog". The simple reading operations of this
library will automatically open and close the dialog when necessary, but each opening and each closing
takes one FinTS roundtrip.
For the case where multiple operations are to be performed one after the other you can indicate to the library
that you want to open a standing dialog and keep it open explicitly by entering the
:class:`~fints.client.FinTS3Client` as a context handler.
This can, and should be, complemented with the client state facility as follows:
.. code-block:: python
datablob = ... # get from backend storage, or set to None
client = FinTS3PinTanClient(..., from_data=datablob)
with client:
accounts = client.get_sepa_accounts()
balance = client.get_balance(accounts[0])
transactions = client.get_transactions(accounts[0])
datablob = client.deconstruct()
# Store datablob to backend storage
For transactions involving TANs it may be required by the bank to issue both steps for one transaction
within the same dialog. In this case it's mandatory to use a standing dialog, because otherwise each
step would be issued in its own, implicit, dialog.
.. _client-dialog-state:
Storing and restoring dialog state
----------------------------------
.. autoclass:: fints.client.FinTS3Client
:members: pause_dialog, resume_dialog
:noindex:
:undoc-members:
python-fints-4.0.0/docs/conf.py 0000664 0000000 0000000 00000030201 14421014606 0016376 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# python-fints documentation build configuration file, created by
# sphinx-quickstart on Sun Apr 3 00:09:59 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
import sys
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
try:
from fints import version
except ImportError:
version = '?'
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.doctest',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'python-fints'
copyright = '2018, Raphael Michel'
author = 'Raphael Michel'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '.'.join(version.split('.')[:2])
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'description': 'FinTS client library for Python',
'github_user': 'raphaelm',
'github_repo': 'python-fints',
'github_button': True,
'github_banner': False,
'travis_button': True,
'pre_bg': '#FFF6E5',
'note_bg': '#E5ECD1',
'note_border': '#BFCF8C',
'body_text': '#482C0A',
'sidebar_text': '#49443E',
'sidebar_header': '#4B4032',
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# " v documentation" by default.
# html_title = 'python-fints v0.0.1'
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'**': [
'about.html', 'navigation.html', 'searchbox.html',
]
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-fintsdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pythonfints.tex', 'python-fints Documentation',
'Raphael Michel', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'python-fints', 'python-fints Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'python-fints', 'python-fonts Documentation',
author, 'python-fints', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False
# -- Options for Epub output ----------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright
# The basename for the epub file. It defaults to the project name.
# epub_basename = project
# The HTML theme for the epub output. Since the default themes are not
# optimized for small screen space, using the same theme for HTML and epub
# output is usually not wise. This defaults to 'epub', a theme designed to save
# visual space.
# epub_theme = 'epub'
# The language of the text. It defaults to the language option
# or 'en' if the language is not set.
# epub_language = ''
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
# epub_identifier = ''
# A unique identification for the text.
# epub_uid = ''
# A tuple containing the cover image and cover page html template filenames.
# epub_cover = ()
# A sequence of (type, uri, title) tuples for the guide element of content.opf.
# epub_guide = ()
# HTML files that should be inserted before the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_pre_files = []
# HTML files that should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_post_files = []
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# The depth of the table of contents in toc.ncx.
# epub_tocdepth = 3
# Allow duplicate toc entries.
# epub_tocdup = True
# Choose between 'default' and 'includehidden'.
# epub_tocscope = 'default'
# Fix unsupported image types using the Pillow.
# epub_fix_images = False
# Scale large images.
# epub_max_image_width = 0
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# epub_show_urls = 'inline'
# If false, no index is generated.
# epub_use_index = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None, 'https://mt940.readthedocs.io/en/latest/': None, }
python-fints-4.0.0/docs/debits.rst 0000664 0000000 0000000 00000004550 14421014606 0017113 0 ustar 00root root 0000000 0000000 .. _debits:
Creating SEPA debits
====================
You can submit a SEPA debit XML file to the bank with the ``sepa_debit`` method:
.. autoclass:: fints.client.FinTS3Client
:members: sepa_debit
:noindex:
You should then enter a TAN, read our chapter :ref:`tans` to find out more.
Full example
------------
You can easily generate XML using the ``sepaxml`` python library:
.. code-block:: python
from sepaxml import SepaDD
config = {
"name": "Test Company",
"IBAN": "DE12345",
"BIC": "BIC12345",
"batch": False,
"creditor_id": "TESTCORPID",
"currency": "EUR",
}
sepa = SepaDD(config, schema="pain.008.002.02")
sepa.add_payment({
"name": "Customer",
"IBAN": "DE12345",
"BIC": "BIC12345",
"amount": 100,
"type": "OOFF", # FRST, RCUR, OOFF, FNAL
"collection_date": datetime.date.today() + datetime.timedelta(days=3),
"mandate_id": "FINTSTEST1",
"mandate_date": datetime.date(2018, 7, 26),
"description": "FinTS Test transaction",
})
pain_message = sepa.export().decode()
client = FinTS3PinTanClient(...)
minimal_interactive_cli_bootstrap(client)
with client:
if client.init_tan_response:
print("A TAN is required", client.init_tan_response.challenge)
if getattr(client.init_tan_response, 'challenge_hhduc', None):
try:
terminal_flicker_unix(client.init_tan_response.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
client.send_tan(client.init_tan_response, tan)
res = client.sepa_debit(
account=accounts[0],
data=pain_message,
multiple=False,
control_sum=Decimal('1.00'),
pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.008.002.02'
)
if isinstance(res, NeedTANResponse):
print("A TAN is required", res.challenge)
if getattr(res, 'challenge_hhduc', None):
try:
terminal_flicker_unix(res.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
res = client.send_tan(res, tan)
print(res.status)
print(res.responses)
python-fints-4.0.0/docs/developer/ 0000775 0000000 0000000 00000000000 14421014606 0017070 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/docs/developer/index.rst 0000664 0000000 0000000 00000000461 14421014606 0020732 0 ustar 00root root 0000000 0000000 Developer documentation/API
===========================
This part of the documentation is for you if you want to improve python-fints, but also if you just want to look behind the curtain.
.. toctree::
:maxdepth: 2
:caption: Contents:
parsing
segments/index
sequence
working
types
python-fints-4.0.0/docs/developer/parsing.rst 0000664 0000000 0000000 00000010276 14421014606 0021273 0 ustar 00root root 0000000 0000000 Parsing and serialization
-------------------------
.. autoclass:: fints.parser.FinTS3Parser
:members:
.. autoclass:: fints.parser.FinTS3Serializer
:members:
Example usage:
.. code-block:: python
>>> message = (b'HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043'
... b"999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+"
... b"2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:"
... b'4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16'
... b"+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100:"
... b":Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'")
>>> from fints.parser import FinTS3Parser
>>> s = FinTS3Parser().parse_message(message)
>>> s
SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3), message_size='000000000428', hbci_version=300, dialog_id='430711670077=043999659571CN9D=', message_number=2, reference_message=fints.formals.ReferenceMessage(dialog_id='430711670077=043999659571CN9D=', message_number=2)), fints.segments.HNVSK3(header=fints.formals.SegmentHeader('HNVSK', 998, 3), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='998', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), encryption_algorithm=fints.formals.EncryptionAlgorithm(usage_encryption='2', operation_mode='2', encryption_algorithm='13', algorithm_parameter_value=b'00000000', algorithm_parameter_name='5', algorithm_parameter_iv_name='1'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0), compression_function='0'), fints.segments.HNVSD1(header=fints.formals.SegmentHeader('HNVSD', 999, 1), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader('HNSHK', 2, 4), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='999', security_reference='9166926', security_application_area='1', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1'), signature_algorithm=fints.formals.SignatureAlgorithm(usage_signature='6', signature_algorithm='10', operation_mode='16'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0)), fints.segments.HIRMG2(header=fints.formals.SegmentHeader('HIRMG', 3, 2), responses=[fints.formals.Response(code='0010', reference_element=None, text='Nachricht entgegengenommen.'), fints.formals.Response(code='0100', reference_element=None, text='Dialog beendet.')]), fints.segments.HNSHA2(header=fints.formals.SegmentHeader('HNSHA', 4, 2), security_reference='9166926')])), fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', 5, 1), message_number=2)])
>>> from fints.parser import FinTS3Serializer
>>> FinTS3Serializer().serialize_message(s)
b"HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100::Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'"
.. note::
In general parsing followed by serialization is not idempotent: A message may contain empty list elements at the end, but our serializer will never generate them.
python-fints-4.0.0/docs/developer/segments/ 0000775 0000000 0000000 00000000000 14421014606 0020715 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/docs/developer/segments/all.rst 0000664 0000000 0000000 00000005772 14421014606 0022232 0 ustar 00root root 0000000 0000000 All Segments
____________
fints.segments.accounts module
------------------------------
.. automodule:: fints.segments.accounts
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.auth module
--------------------------
.. automodule:: fints.segments.auth
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.bank module
--------------------------
.. automodule:: fints.segments.bank
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.base module
--------------------------
.. automodule:: fints.segments.base
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset, FinTS3Segment
fints.segments.debit module
---------------------------
.. automodule:: fints.segments.debit
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.depot module
---------------------------
.. automodule:: fints.segments.depot
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.dialog module
----------------------------
.. automodule:: fints.segments.dialog
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.journal module
-----------------------------
.. automodule:: fints.segments.journal
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.message module
-----------------------------
.. automodule:: fints.segments.message
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.saldo module
---------------------------
.. automodule:: fints.segments.saldo
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.statement module
-------------------------------
.. automodule:: fints.segments.statement
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
fints.segments.transfer module
------------------------------
.. automodule:: fints.segments.transfer
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset
python-fints-4.0.0/docs/developer/segments/index.rst 0000664 0000000 0000000 00000004326 14421014606 0022563 0 ustar 00root root 0000000 0000000 FinTS Segments
--------------
A segment is the core communication workhorse in FinTS. Each segment has a header of fixed format, which includes the segment type ("Segmentkennung"), number within the message, version, and, optionally, the number of the segment of another message it is in response or relation to ("Bezugssegment").
The header is followed by a nested structure of fields and groups of fields, the exact specification of which depends on the segment type and version.
All segment classes derive from :class:`~fints.segments.base.FinTS3Segment`, which specifies the ``header`` attribute of :class:`~fints.formals.SegmentHeader` type.
.. autoclass:: fints.segments.base.FinTS3Segment
:members:
:inherited-members: print_nested
:member-order: bysource
.. attribute:: TYPE
Segment type. Will be determined from the class name in subclasses, if the class name consists only of uppercase characters followed by decimal digits. Subclasses may explicitly set a class attribute instead.
.. attribute:: VERSION
Segment version. Will be determined from the class name in subclasses, if the class name consists only of uppercase characters followed by decimal digits. Subclasses may explicitly set a class attribute instead.
.. classmethod:: find_subclass(segment: list)
Parse the given ``segment`` parameter as a :class:`~fints.formals.SegmentHeader` and return a subclass with matching type and version class attributes.
The :class:`~fints.segments.base.FinTS3Segment` class and its base classes employ a number of dynamic programming techniques so that derived classes need only specify the name, order and type of fields. All type conversion, construction etc. will take place automatically. All derived classes basically should behave "as expected", returning only native Python datatypes.
Consider this example segment class:
.. code-block:: python
class HNHBS1(FinTS3Segment):
message_number = DataElementField(type='num', max_length=4)
Calling ``print_nested`` on an instance of this class might output:
.. code-block:: python
fints.segments.HNHBS1(
header = fints.formals.SegmentHeader('HNHBS', 4, 1),
message_number = 1,
)
.. toctree::
:maxdepth: 2
all
python-fints-4.0.0/docs/developer/sequence.rst 0000664 0000000 0000000 00000000500 14421014606 0021425 0 ustar 00root root 0000000 0000000 FinTS Segment Sequence
----------------------
A message is a sequence of segments. The :class:`~fints.formals.SegmentSequence` object allows searching for segments by type and version, by default recursing into nested sequences.
.. autoclass:: fints.types.SegmentSequence
:members:
:undoc-members: print_nested
python-fints-4.0.0/docs/developer/types.rst 0000664 0000000 0000000 00000001106 14421014606 0020764 0 ustar 00root root 0000000 0000000 Defining new Segment classes
----------------------------
Base types
~~~~~~~~~~~
.. automodule:: fints.types
:members:
:undoc-members:
:exclude-members: print_nested, SegmentSequence
:member-order: bysource
Field types
~~~~~~~~~~~
.. automodule:: fints.fields
:members:
:undoc-members:
:exclude-members: print_nested
:member-order: bysource
Constructed and helper types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: fints.formals
:members:
:undoc-members:
:exclude-members: print_nested SegmentHeader
:member-order: bysource
python-fints-4.0.0/docs/developer/working.rst 0000664 0000000 0000000 00000007373 14421014606 0021314 0 ustar 00root root 0000000 0000000 Working with Segments
~~~~~~~~~~~~~~~~~~~~~
Objects of :class:`~fints.segments.base.FinTS3Segment` or a subclass can be created by calling their constructor. The constructor takes optional arguments for all fields of the class. Setting and getting fields and subfields works, and consumes and returns Python objects as appropriate:
.. code-block:: python
>>> from fints.segments import HNHBS1
>>> s = HNHBS1()
>>> s
fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', None, 1), message_number=None)
>>> s.header.number = 3
>>> s.header
fints.formals.SegmentHeader('HNHBS', 3, 1)
When setting a value, format and length restrictions will be checked, if possible:
.. code-block:: python
>>> s.message_number = 'abc'
ValueError: invalid literal for int() with base 10: 'abc'
>>> s.message_number = 12345
ValueError: Value '12345' cannot be rendered: max_length=4 exceeded
The only exception is: Every field can be set to ``None`` in order to clear the field and make it unset, recursively. No checking is performed whether all fields that are required (or conditionally required) by the specification are set. For convenience, an unset constructed field will still be filled with an instance of the field's value type, so that subfield accessing will always work, without encountering ``None`` values on the way.
.. code-block:: python
>>> s.header = None
>>> s
fints.segments.HNHBS1(header=fints.formals.SegmentHeader(None, None, None), message_number=None)
When calling the constructor with non-keyword arguments, fields are assigned in order, with the exception of ``header`` in :class:`~fints.segments.base.FinTS3Segment` subclasses, which can only be given as a keyword argument. When no ``header`` argument is present, a :class:`~fints.formals.SegmentHeader` is automatically constructed with default values (and no ``number``). It's generally not required to construct the ``header`` parameter manually.
.. code-block:: python
>>> HNHBS1(42)
fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', None, 1), message_number=42)
>>> HNHBS1(42, header=SegmentHeader('FOO'))
fints.segments.HNHBS1(header=fints.formals.SegmentHeader('FOO', None, None), message_number=42)
Some segment fields have a variable number of values. These are always treated as a list, and minimum/maximum list length is obeyed. Setting a value beyond the end of the list results in an exception. Empty values are added to maintain the correct minimum number of values.
.. code-block:: python
>>> from fints.segments import HIRMG2
>>> s = HIRMG2()
>>> s
fints.segments.HIRMG2(header=fints.formals.SegmentHeader('HIRMG', None, 2), responses=[fints.formals.Response(code=None, reference_element=None, text=None)])
>>> s.responses[0].code = '0010'
>>> s.responses[1].code = '0100'
>>> s.print_nested()
fints.segments.HIRMG2(
header = fints.formals.SegmentHeader('HIRMG', None, 2),
responses = [
fints.formals.Response(
code = '0010',
reference_element = None,
text = None,
),
fints.formals.Response(
code = '0100',
reference_element = None,
text = None,
),
],
)
>>> HIRMG2(responses=[fints.formals.Response('2342')]).print_nested()
fints.segments.HIRMG2(
header = fints.formals.SegmentHeader('HIRMG', None, 2),
responses = [
fints.formals.Response(
code = '2342',
reference_element = None,
text = None,
),
],
)
python-fints-4.0.0/docs/index.rst 0000664 0000000 0000000 00000001207 14421014606 0016744 0 ustar 00root root 0000000 0000000 FinTS client library
====================
.. image:: https://img.shields.io/pypi/v/fints.svg
:target: https://pypi.python.org/pypi/fints
This is a pure-python implementation of FinTS (formerly known as HBCI), a
online-banking protocol commonly supported by German banks.
Library user documentation content
----------------------------------
.. toctree::
:maxdepth: 2
quickstart
reading
client
tans
transfers
debits
tested
upgrading_3_4
upgrading_2_3
upgrading_1_2
trouble
Library developer documentation content
---------------------------------------
.. toctree::
:maxdepth: 2
developer/index
python-fints-4.0.0/docs/make.bat 0000664 0000000 0000000 00000016514 14421014606 0016517 0 ustar 00root root 0000000 0000000 @ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. epub3 to make an epub3
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 1>NUL 2>NUL
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-i18nfield.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-i18nfield.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "epub3" (
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end
python-fints-4.0.0/docs/quickstart.rst 0000664 0000000 0000000 00000004636 14421014606 0020040 0 ustar 00root root 0000000 0000000 Getting started
===============
Register for a product ID
-------------------------
As of September 14th, 2019, all FinTS client programs need to be registered with the ZKA.
You need to fill out a PDF form and will be assigned a product ID that you can pass to this library.
It can take up to two weeks for the product ID to be assigned.
The reason for this requirement is compliance with the European Unions 2nd Payment Services Directive (PSD2)
which mandates that end-users can transparently see which applications are accessing their bank account.
You cna find more information as well as the registration form on the `ZKA Website`_ (only available in German).
Start coding
------------
First of all, you need to install the library::
$ pip3 install fints
Then, you can initialize a FinTS client by providing your bank's BLZ, your username and PIN as well as the HBCI endpoint
of your bank. Logging in with a signature file or chip card is currently not supported. For example:
.. code-block:: python
import logging
from datetime import date
import getpass
from fints.client import FinTS3PinTanClient
logging.basicConfig(level=logging.DEBUG)
f = FinTS3PinTanClient(
'123456789', # Your bank's BLZ
'myusername', # Your login name
getpass.getpass('PIN:'), # Your banking PIN
'https://hbci-pintan.gad.de/cgi-bin/hbciservlet',
product_id='Your product ID' # see above
)
Since the implementation of PSD2, you will in almost all cases need to be ready to deal with TANs. For a quick start,
we included a minimal command-line utility to help choose a TAN method:
.. code-block:: python
from fints.utils import minimal_interactive_cli_bootstrap
minimal_interactive_cli_bootstrap(f)
You can then open up a real communication dialog to the bank with a ``with`` statement and issue commands:
commands using the client instance:
.. code-block:: python
with f:
# Since PSD2, a TAN might be needed for dialog initialization. Let's check if there is one required
if f.init_tan_response:
print("A TAN is required", f.init_tan_response.challenge)
tan = input('Please enter TAN:')
f.send_tan(f.init_tan_response, tan)
# Fetch accounts
accounts = f.get_sepa_accounts()
Go on to the next pages to find out what commands are supported!
.. _ZKA Website: https://www.hbci-zka.de/register/prod_register.htm python-fints-4.0.0/docs/reading.rst 0000664 0000000 0000000 00000004321 14421014606 0017246 0 ustar 00root root 0000000 0000000 Reading operations
==================
.. note::
Starting from version 3, **all of the methods on this page** can return a ``NeedTANResponse`` instead of actual
data if your bank requires a TAN. You should then enter a TAN, read our chapter :ref:`tans` to find out more.
Fetching your bank accounts
---------------------------
The most simple method allows you to get all bank accounts that your user has access to:
.. autoclass:: fints.client.FinTS3Client
:noindex:
:members: get_sepa_accounts
This method will return a list of named tuples of the following type:
.. autoclass:: fints.models.SEPAAccount
You will need this account object for many further operations to show which account you want to operate on.
.. _information:
Fetching bank information
-------------------------
During the first interaction with the bank some meta information about the bank and your user is transmitted
from the bank.
.. autoclass:: fints.client.FinTS3Client
:members: get_information
:noindex:
Fetching account balances
-------------------------
You can fetch the current balance of an account with the ``get_balance`` operation.
.. autoclass:: fints.client.FinTS3Client
:members: get_balance
:noindex:
This method will return a list of ``Balance`` objects from the ``mt-940`` library. You can find more information
in `their documentation `_.
.. _transactions:
Reading account transactions
----------------------------
You can fetch the banking statement of an account within a certain timeframe with the ``get_transactions``
operation.
.. autoclass:: fints.client.FinTS3Client
:members: get_transactions, get_transactions_xml
:noindex:
This method will return a list of ``Transaction`` objects from the ``mt-940`` library. You can find more information
in `their documentation `_.
Fetching holdings
-----------------
You can fetch the holdings of an account with the ``get_holdings`` method:
.. autoclass:: fints.client.FinTS3Client
:members: get_holdings
:noindex:
This method will return a list of ``Holding`` objects:
.. autoclass:: fints.models.Holding
python-fints-4.0.0/docs/tans.rst 0000664 0000000 0000000 00000013600 14421014606 0016602 0 ustar 00root root 0000000 0000000 .. _tans:
Working with TANs
=================
Many operations in FinTS will require a form of two-step authentication, called TANs. TANs are
mostly required for operations that move money or change details of a bank account. TANs can be
generated with a multitude of methods, including paper lists, smartcard readers, SMS messages, and
smartphone apps.
TAN methods
-----------
Before doing any operations involving TANs, you should get a list of supported TAN mechanisms:
.. code-block:: python
mechanisms = client.get_tan_mechanisms()
The returned dictionary maps identifiers (generally: three-digit numerals) to instances of a
:func:`~fints.formals.TwoStepParametersCommon` subclass with varying fields, depending on the
version of the two-step process and the bank.
The `name` field of these objects provides a user-friendly name of the TAN mechanism that you
can display to the user to choose from. To select a TAN mechanism, you can use
:func:`~fints.client.FinTS3PinTanClient.set_tan_mechanism`, which takes the identifier used as
key in the :func:`~fints.client.FinTS3PinTanClient.get_tan_mechanisms` return value.
If the ``description_required`` attribute for the TAN mechanism is :attr:`~fints.formals.DescriptionRequired.MUST`,
you will need to get a list of TAN media with :func:`~fints.client.FinTS3PinTanClient.get_tan_media` and select the
appropriate one with :func:`~fints.client.FinTS3PinTanClient.set_tan_medium`.
Have a look at the source code of :func:`~fints.utils.minimal_interactive_cli_bootstrap` for an example on how to
ask the user for these properties.
You may not change the active TAN mechanism or TAN medium within a standing dialog (see :ref:`client-dialog-state`).
The selection of the active TAN mechanism/medium is stored with the persistent client data (see :ref:`client-state`).
.. autoclass:: fints.client.FinTS3PinTanClient
:members: get_tan_mechanisms, set_tan_mechanism, get_current_tan_mechanism, get_tan_media, set_tan_medium
:noindex:
:undoc-members:
TAN challenges
--------------
When you try to perform an operation that requires a TAN to proceed, you will receive an object containing
the bank's challenge (and some internal data to continue the operation once the TAN has been processed):
.. autoclass:: fints.client.NeedTANResponse
:undoc-members:
:members:
The ``challenge`` attribute will contain human-readable instructions on how to proceed.
The ``challenge_html`` attribute will possibly contain a nicer, formatted, HTML version of the challenge text
that you should prefer if your primary interface can render HTML. The contents are guaranteed to be proper and
clean (by using the `bleach` library): They can be used with `mark_safe` in Django.
The ``challenge_hhduc`` attribute will contain the challenge to be used with a TAN generator device using the
Hand Held Device Unidirectional Coupling specification (such as a Flicker-Code).
Flicker-Code / optiTAN
----------------------
If you want to use chipTAN with an optical TAN device, we provide utilities to print the flicker code on
a unix terminal. Just pass the ``challenge_hhd_uc`` value to this method:
.. autofunction:: fints.hhd.flicker.terminal_flicker_unix
You should probably catch for ``KeyboardInterrupts`` to allow the user to abort the displaying and to continue
with the TAN:
.. code-block:: python
try:
terminal_flicker_unix(result.challenge_hhduc)
except KeyboardInterrupt:
pass
photoTAN
--------
If you want to use photoTAN, use the ``challenge_matrix`` attribute to access the image file, e.g. by writing it to
a file:
.. code-block:: python
with open("tan.png", "wb") as writer:
writer.write(result.challenge_matrix[1])
writer.close()
Sending the TAN
---------------
Once obtained the TAN, you can send it with the ``send_tan`` client method:
.. autoclass:: fints.client.FinTS3PinTanClient
:members: send_tan
:noindex:
For example:
.. code-block:: python
tan = input('Please enter the TAN code: ')
result = client.send_tan(result, tan)
Storing and restoring TAN state
-------------------------------
The :func:`~fints.client.NeedTANResponse.get_data` method and
:func:`~fints.client.NeedRetryResponse.from_data` factory method can be used to store and restore
a TAN state object between steps.
.. autoclass:: fints.client.NeedRetryResponse
:undoc-members:
:members: from_data
You SHOULD use this facility together with the client and dialog state restoration facilities:
.. code-block:: python
:caption: First step
client = FinTS3PinTanClient(...)
# Optionally: choose a tan mechanism with
# client.set_tan_mechanism(…)
with client:
response = client.sepa_transfer(...)
dialog_data = client.pause_dialog()
client_data = client.deconstruct()
tan_data = response.get_data()
.. code-block:: python
:caption: Second step
tan_request = NeedRetryResponse.from_data(tan_data)
print("TAN request: {}".format(tan_request.challenge))
tan = input('Enter TAN: ')
.. code-block:: python
:caption: Third step
tan_request = NeedRetryResponse.from_data(tan_data)
client = FinTS3PinTanClient(..., from_data=client_data)
with client.resume_dialog(dialog_data):
response = client.send_tan(tan_request, tan)
print(response.status)
print(response.responses)
Reference
---------
.. autoclass:: fints.formals.TwoStepParameters2
:noindex:
:undoc-members:
:members:
:inherited-members:
:member-order: bysource
:exclude-members: is_unset, naive_parse, print_nested
.. autoclass:: fints.formals.TwoStepParameters3
:noindex:
:undoc-members:
:members:
:inherited-members:
:member-order: bysource
:exclude-members: is_unset, naive_parse, print_nested
.. autoclass:: fints.formals.TwoStepParameters5
:noindex:
:undoc-members:
:members:
:inherited-members:
:member-order: bysource
:exclude-members: is_unset, naive_parse, print_nested
python-fints-4.0.0/docs/tested.rst 0000664 0000000 0000000 00000005560 14421014606 0017133 0 ustar 00root root 0000000 0000000 Tested banks
============
The following banks have been tested with version 3.x of this library:
======================================== ============ ======== ======== ======
Bank Transactions Holdings Transfer Debits
and Balance
======================================== ============ ======== ======== ======
Postbank Yes
BBBank eG Yes Yes
Sparkasse Heidelberg Yes
comdirect Yes
======================================== ============ ======== ======== ======
Tested security functions
-------------------------
* ``902`` "photoTAN"
* ``921`` "pushTAN"
* ``930`` "mobile TAN"
* ``942`` "mobile TAN"
* ``962`` "Smart-TAN plus manuell"
* ``972`` "Smart-TAN plus optisch"
Legacy results
---------------
The following banks have been tested with the old version 1.x of this library:
======================================== ============ ======== ======== ======
Bank Statements Holdings Transfer Debits
======================================== ============ ======== ======== ======
BBBank eG Yes Yes
CortalConsors Yes Yes
comdirect Yes
GLS Bank eG Yes Yes Yes
DKB Yes
ING DiBa Yes
netbank Yes
NIBC Direct Yes
Postbank Yes
Sparkasse Yes
Triodos Bank Yes
Volksbank (Fiducia) Yes
Wüstenrot Yes
1822direkt Yes Yes
======================================== ============ ======== ======== ======
The following banks have been tested with the old version 2.x of this library:
======================================== ============ ======== ======== ======
Bank Transactions Holdings Transfer Debits
and Balance
======================================== ============ ======== ======== ======
GLS Bank eG Yes Yes Yes
Postbank Yes
Triodos Bank Yes Yes
Volksbank Darmstadt-Südhessen Yes Yes
Deutsche Skatbank Yes Yes
BBBank eG Yes Yes
MLP Banking AG Yes
======================================== ============ ======== ======== ======
python-fints-4.0.0/docs/transfers.rst 0000664 0000000 0000000 00000003560 14421014606 0017650 0 ustar 00root root 0000000 0000000 .. _transfers:
Sending SEPA transfers
======================
Simple mode
-----------
You can create a simple SEPA transfer using this convenient client method:
.. autoclass:: fints.client.FinTS3Client
:members: simple_sepa_transfer
:noindex:
You should then enter a TAN, read our chapter :ref:`tans` to find out more.
Advanced mode
-------------
If you want to use advanced methods, you can supply your own SEPA XML:
.. autoclass:: fints.client.FinTS3Client
:members: sepa_transfer
:noindex:
Full example
------------
.. code-block:: python
client = FinTS3PinTanClient(...)
minimal_interactive_cli_bootstrap(client)
with client:
if client.init_tan_response:
print("A TAN is required", client.init_tan_response.challenge)
if getattr(client.init_tan_response, 'challenge_hhduc', None):
try:
terminal_flicker_unix(client.init_tan_response.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
client.send_tan(client.init_tan_response, tan)
res = client.simple_sepa_transfer(
account=accounts[0],
iban='DE12345',
bic='BIC12345',
amount=Decimal('7.00'),
recipient_name='Foo',
account_name='Test',
reason='Birthday gift',
endtoend_id='NOTPROVIDED',
)
if isinstance(res, NeedTANResponse):
print("A TAN is required", res.challenge)
if getattr(res, 'challenge_hhduc', None):
try:
terminal_flicker_unix(res.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
res = client.send_tan(res, tan)
print(res.status)
print(res.responses)
python-fints-4.0.0/docs/trouble.rst 0000664 0000000 0000000 00000017552 14421014606 0017323 0 ustar 00root root 0000000 0000000 Troubleshooting and bug reporting
=================================
The FinTS specification is long and complicated and in many parts leaves things open to interpretation -- or sometimes
implementors interpret things differently even though they're not really open to interpretation. This is valid for us,
but also for the banks. Making the library work with many different banks is hard, and often impossible without access
to a test account. Therefore, we ask you for patience when reporting issues with different banks -- and you need to be
ready that we might not be able to help you because we do not have the time or bank account required to dig deeper.
Therefore, if you run into trouble with this library, you first need to ask yourself a very important question: **Is it
me or the library?** To answer this question for most cases, we have attached a script below, that we ask you to use
to try the affected feature of the library in a well-documented way. Apart from changing the arguments (i.e. your bank's
parameters and your credentials) at the top, we ask you **not to make any modifications**. Pasting this bit by bit into
a Jupyter notebook **is a modification**. If your issue does not include information as to whether the script below works
or does not work for your bank, **we will close your issue without further comment.**
**If the script below does not work for you**, there is probably a compatibility issue between this library and your
bank. Feel free to open an issue, but make sure the issue title includes the name of the bank and the text includes
what operations specifically fail.
**If the script below does work for you**, there is probably something wrong with your usage of the library or our
documentation. Feel free to open an issue, but **include full working example code** that is necessary to reproduce
the problem.
.. note:: Before posting anything on GitHub, make sure it does not contain your username, PIN, IBAN, or similarly sensitive data.
.. code-block:: python
import datetime
import getpass
import logging
import sys
from decimal import Decimal
from fints.client import FinTS3PinTanClient, NeedTANResponse, FinTSUnsupportedOperation
from fints.hhd.flicker import terminal_flicker_unix
from fints.utils import minimal_interactive_cli_bootstrap
logging.basicConfig(level=logging.DEBUG)
client_args = (
'REPLACEME', # BLZ
'REPLACEME', # USER
getpass.getpass('PIN: '),
'REPLACEME' # ENDPOINT
)
f = FinTS3PinTanClient(*client_args)
minimal_interactive_cli_bootstrap(f)
def ask_for_tan(response):
print("A TAN is required")
print(response.challenge)
if getattr(response, 'challenge_hhduc', None):
try:
terminal_flicker_unix(response.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
return f.send_tan(response, tan)
# Open the actual dialog
with f:
# Since PSD2, a TAN might be needed for dialog initialization. Let's check if there is one required
if f.init_tan_response:
ask_for_tan(f.init_tan_response)
# Fetch accounts
accounts = f.get_sepa_accounts()
if isinstance(accounts, NeedTANResponse):
accounts = ask_for_tan(accounts)
if len(accounts) == 1:
account = accounts[0]
else:
print("Multiple accounts available, choose one")
for i, mm in enumerate(accounts):
print(i, mm.iban)
choice = input("Choice: ").strip()
account = accounts[int(choice)]
# Test pausing and resuming the dialog
dialog_data = f.pause_dialog()
client_data = f.deconstruct(including_private=True)
f = FinTS3PinTanClient(*client_args, from_data=client_data)
with f.resume_dialog(dialog_data):
while True:
operations = [
"End dialog",
"Fetch transactions of the last 30 days",
"Fetch transactions of the last 120 days",
"Fetch transactions XML of the last 30 days",
"Fetch transactions XML of the last 120 days",
"Fetch information",
"Fetch balance",
"Fetch holdings",
"Fetch scheduled debits",
"Fetch status protocol",
"Make a simple transfer"
]
print("Choose an operation")
for i, o in enumerate(operations):
print(i, o)
choice = int(input("Choice: ").strip())
try:
if choice == 0:
break
elif choice == 1:
res = f.get_transactions(account, datetime.date.today() - datetime.timedelta(days=30),
datetime.date.today())
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print("Found", len(res), "transactions")
elif choice == 2:
res = f.get_transactions(account, datetime.date.today() - datetime.timedelta(days=120),
datetime.date.today())
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print("Found", len(res), "transactions")
elif choice == 3:
res = f.get_transactions_xml(account, datetime.date.today() - datetime.timedelta(days=30),
datetime.date.today())
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print("Found", len(res[0]) + len(res[1]), "XML documents")
elif choice == 4:
res = f.get_transactions_xml(account, datetime.date.today() - datetime.timedelta(days=120),
datetime.date.today())
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print("Found", len(res[0]) + len(res[1]), "XML documents")
elif choice == 5:
print(f.get_information())
elif choice == 6:
res = f.get_balance(account)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print(res)
elif choice == 7:
res = f.get_holdings(account)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print(res)
elif choice == 8:
res = f.get_scheduled_debits(account)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print(res)
elif choice == 9:
res = f.get_status_protocol()
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print(res)
elif choice == 10:
res = f.simple_sepa_transfer(
account=accounts[0],
iban=input('Target IBAN:'),
bic=input('Target BIC:'),
amount=Decimal(input('Amount:')),
recipient_name=input('Recipient name:'),
account_name=input('Your name:'),
reason=input('Reason:'),
endtoend_id='NOTPROVIDED',
)
if isinstance(res, NeedTANResponse):
ask_for_tan(res)
except FinTSUnsupportedOperation as e:
print("This operation is not supported by this bank:", e) python-fints-4.0.0/docs/upgrading_1_2.rst 0000664 0000000 0000000 00000004005 14421014606 0020255 0 ustar 00root root 0000000 0000000 Upgrading from python-fints 1.x to 2.x
======================================
This library has seen a major rewrite in version 2.0 and the API has changed in a lot of places. These are the most
important changes to know:
* The ``get_statement`` method was renamed to ``get_transactions``. → :ref:`transactions`
* The ``start_simple_sepa_transfer`` method was renamed to ``simple_sepa_transfer`` and no longer takes a TAN method
and TAN medium description as an argument. → :ref:`transfers`
* The ``start_sepa_transfer`` method was renamed to ``sepa_transfer`` and no longer takes a TAN method and TAN
medium description as an argument. The new parameter ``pain_descriptor`` should be passed with the version of the
PAIN format, e.g. ``urn:iso:std:iso:20022:tech:xsd:pain.001.001.03``. → :ref:`transfers`
* The ``start_sepa_debit`` method was renamed to ``sepa_debit`` and no longer takes a TAN method and TAN
medium description as an argument. The new parameter ``pain_descriptor`` should be passed with the version of the
PAIN format, e.g. ``urn:iso:std:iso:20022:tech:xsd:pain.008.003.01``. Also, a new parameter ``cor1`` is optionally
available. → :ref:`debits`
* Working with TANs has changed a lot. ``get_tan_methos`` has been renamed to ``get_tan_mechanisms`` and has a new
return data type. The chosen TAN method is now set on a client level with ``set_tan_mechanism`` and
``set_tan_medium``. You can find more information in the chapter :ref:`tans` and a full example in the chapter
:ref:`transfers`.
* Debug logging output now contains parsed syntax structures instead of data blobs and is much easier to read.
* A new parser for FinTS has been added that is more robust and performs more validation.
In exchange, you get a couple of great new features:
* A new method :func:`fints.client.FinTS3Client.get_information` was added. → :ref:`information`
* It is now possible to serialize and store the state of the client to enable multi-step operations in a stateless
environment. → :ref:`client-state`
python-fints-4.0.0/docs/upgrading_2_3.rst 0000664 0000000 0000000 00000001603 14421014606 0020260 0 ustar 00root root 0000000 0000000 Upgrading from python-fints 2.x to 3.x
======================================
Release 3.0 of this library was made to adjust to changes made by the banks as part of their PSD2 implementation
in 2019. Here's what you should know when porting your code:
* A TAN can now be required for dialog initialization. In this case, ``client.init_tan_response`` will contain a
``NeedTANResponse``.
* Basically every method of the client class can now return a ``NeedTANResponse``, so you should always expect this
case and handle it gracefully.
* Since everything can require a TAN, everything requires a standing dialog. Issuing interactive commands outside of a
``with client:`` statement is now deprecated. It still might work in very few cases, so we didn't disable it, but we
do not support it any longer. This affects you mostly when you work with this on a Python REPL or e.g. in a Notebook.
python-fints-4.0.0/docs/upgrading_3_4.rst 0000664 0000000 0000000 00000001230 14421014606 0020256 0 ustar 00root root 0000000 0000000 Upgrading from python-fints 3.x to 4.x
======================================
Release 4.0 of this library was made to introduce a breaking change:
* You now need to register your application with the Deutsche Kreditwirtschaft (German banking association) and supply
your assigned product IT when initializing the library.
The library used to have a built-in product ID that was used as a default if you didn't. This was very useful, but
Deutsche Kreditwirtschaft asked us to stop doing this, since it undermindes the whole point of the product registration.
The ID included in prior versions of the library will be deactivated at some point and stop working.
python-fints-4.0.0/fints/ 0000775 0000000 0000000 00000000000 14421014606 0015276 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/fints/__init__.py 0000664 0000000 0000000 00000000022 14421014606 0017401 0 ustar 00root root 0000000 0000000 version = '4.0.0'
python-fints-4.0.0/fints/client.py 0000664 0000000 0000000 00000160611 14421014606 0017133 0 ustar 00root root 0000000 0000000 import datetime
import logging
from abc import ABCMeta, abstractmethod
from base64 import b64decode
from collections import OrderedDict
from contextlib import contextmanager
from decimal import Decimal
from enum import Enum
import bleach
from sepaxml import SepaTransfer
from . import version
from .connection import FinTSHTTPSConnection
from .dialog import FinTSDialog
from .exceptions import *
from .formals import (
CUSTOMER_ID_ANONYMOUS, KTI1, BankIdentifier, DescriptionRequired,
SynchronizationMode, TANMediaClass4, TANMediaType2,
SupportedMessageTypes)
from .message import FinTSInstituteMessage
from .models import SEPAAccount
from .parser import FinTS3Serializer
from .security import (
PinTanDummyEncryptionMechanism, PinTanOneStepAuthenticationMechanism,
PinTanTwoStepAuthenticationMechanism,
)
from .segments.accounts import HISPA1, HKSPA1
from .segments.auth import HIPINS1, HKTAB4, HKTAB5, HKTAN2, HKTAN3, HKTAN5, HKTAN6
from .segments.bank import HIBPA3, HIUPA4, HKKOM4
from .segments.debit import (
HKDBS1, HKDBS2, HKDMB1, HKDMC1, HKDME1, HKDME2,
HKDSC1, HKDSE1, HKDSE2, DebitResponseBase,
)
from .segments.depot import HKWPD5, HKWPD6
from .segments.dialog import HIRMG2, HIRMS2, HISYN4, HKSYN3
from .segments.journal import HKPRO3, HKPRO4
from .segments.saldo import HKSAL5, HKSAL6, HKSAL7
from .segments.statement import DKKKU2, HKKAZ5, HKKAZ6, HKKAZ7, HKCAZ1
from .segments.transfer import HKCCM1, HKCCS1, HKIPZ1, HKIPM1
from .types import SegmentSequence
from .utils import (
MT535_Miniparser, Password, SubclassesMixin,
compress_datablob, decompress_datablob, mt940_to_array,
)
logger = logging.getLogger(__name__)
SYSTEM_ID_UNASSIGNED = '0'
DATA_BLOB_MAGIC = b'python-fints_DATABLOB'
DATA_BLOB_MAGIC_RETRY = b'python-fints_RETRY_DATABLOB'
class FinTSOperations(Enum):
"""This enum is used as keys in the 'supported_operations' member of the get_information() response.
The enum value is a tuple of transaction types ("Geschäftsvorfälle"). The operation is supported if
any of the listed transaction types is present/allowed.
"""
GET_BALANCE = ("HKSAL", )
GET_TRANSACTIONS = ("HKKAZ", )
GET_TRANSACTIONS_XML = ("HKCAZ", )
GET_CREDIT_CARD_TRANSACTIONS = ("DKKKU", )
GET_STATEMENT = ("HKEKA", )
GET_STATEMENT_PDF = ("HKEKP", )
GET_HOLDINGS = ("HKWPD", )
GET_SEPA_ACCOUNTS = ("HKSPA", )
GET_SCHEDULED_DEBITS_SINGLE = ("HKDBS", )
GET_SCHEDULED_DEBITS_MULTIPLE = ("HKDMB", )
GET_STATUS_PROTOCOL = ("HKPRO", )
SEPA_TRANSFER_SINGLE = ("HKCCS", )
SEPA_TRANSFER_MULTIPLE = ("HKCCM", )
SEPA_DEBIT_SINGLE = ("HKDSE", )
SEPA_DEBIT_MULTIPLE = ("HKDME", )
SEPA_DEBIT_SINGLE_COR1 = ("HKDSC", )
SEPA_DEBIT_MULTIPLE_COR1 = ("HKDMC", )
SEPA_STANDING_DEBIT_SINGLE_CREATE = ("HKDDE", )
GET_SEPA_STANDING_DEBITS_SINGLE = ("HKDDB", )
SEPA_STANDING_DEBIT_SINGLE_DELETE = ("HKDDL", )
class NeedRetryResponse(SubclassesMixin, metaclass=ABCMeta):
"""Base class for Responses that need the operation to be externally retried.
A concrete subclass of this class is returned, if an operation cannot be completed and needs a retry/completion.
Typical (and only) example: Requiring a TAN to be provided."""
@abstractmethod
def get_data(self) -> bytes:
"""Return a compressed datablob representing this object.
To restore the object, use :func:`fints.client.NeedRetryResponse.from_data`.
"""
raise NotImplementedError
@classmethod
def from_data(cls, blob):
"""Restore an object instance from a compressed datablob.
Returns an instance of a concrete subclass."""
version, data = decompress_datablob(DATA_BLOB_MAGIC_RETRY, blob)
if version == 1:
for clazz in cls._all_subclasses():
if clazz.__name__ == data["_class_name"]:
return clazz._from_data_v1(data)
raise Exception("Invalid data blob data or version")
class ResponseStatus(Enum):
"""Error status of the response"""
UNKNOWN = 0
SUCCESS = 1 #: Response indicates Success
WARNING = 2 #: Response indicates a Warning
ERROR = 3 #: Response indicates an Error
_RESPONSE_STATUS_MAPPING = {
'0': ResponseStatus.SUCCESS,
'3': ResponseStatus.WARNING,
'9': ResponseStatus.ERROR,
}
class TransactionResponse:
"""Result of a FinTS operation.
The status member indicates the highest type of errors included in this Response object.
The responses member lists all individual response lines/messages, there may be multiple (e.g. 'Message accepted' and 'Order executed').
The data member may contain further data appropriate to the operation that was executed."""
status = ResponseStatus
responses = list
data = dict
def __init__(self, response_message):
self.status = ResponseStatus.UNKNOWN
self.responses = []
self.data = {}
for hirms in response_message.find_segments(HIRMS2):
for resp in hirms.responses:
self.set_status_if_higher(_RESPONSE_STATUS_MAPPING.get(resp.code[0], ResponseStatus.UNKNOWN))
def set_status_if_higher(self, status):
if status.value > self.status.value:
self.status = status
def __repr__(self):
return "<{o.__class__.__name__}(status={o.status!r}, responses={o.responses!r}, data={o.data!r})>".format(o=self)
class FinTSClientMode(Enum):
OFFLINE = 'offline'
INTERACTIVE = 'interactive'
class FinTS3Client:
def __init__(self,
bank_identifier, user_id, customer_id=None,
from_data: bytes=None,
product_id=None, product_version=version[:5],
mode=FinTSClientMode.INTERACTIVE):
self.accounts = []
if isinstance(bank_identifier, BankIdentifier):
self.bank_identifier = bank_identifier
elif isinstance(bank_identifier, str):
self.bank_identifier = BankIdentifier(BankIdentifier.COUNTRY_ALPHA_TO_NUMERIC['DE'], bank_identifier)
else:
raise TypeError("bank_identifier must be BankIdentifier or str (BLZ)")
self.system_id = SYSTEM_ID_UNASSIGNED
if not product_id:
raise TypeError("The product_id keyword argument is mandatory starting with python-fints version 4. See "
"https://python-fints.readthedocs.io/en/latest/upgrading_3_4.html for more information.")
self.user_id = user_id
self.customer_id = customer_id or user_id
self.bpd_version = 0
self.bpa = None
self.bpd = SegmentSequence()
self.upd_version = 0
self.upa = None
self.upd = SegmentSequence()
self.product_name = product_id
self.product_version = product_version
self.response_callbacks = []
self.mode = mode
self.init_tan_response = None
self._standing_dialog = None
if from_data:
self.set_data(bytes(from_data))
def _new_dialog(self, lazy_init=False):
raise NotImplemented()
def _ensure_system_id(self):
raise NotImplemented()
def _process_response(self, dialog, segment, response):
pass
def process_response_message(self, dialog, message: FinTSInstituteMessage, internal_send=True):
bpa = message.find_segment_first(HIBPA3)
if bpa:
self.bpa = bpa
self.bpd_version = bpa.bpd_version
self.bpd = SegmentSequence(
message.find_segments(
callback=lambda m: len(m.header.type) == 6 and m.header.type[1] == 'I' and m.header.type[5] == 'S'
)
)
upa = message.find_segment_first(HIUPA4)
if upa:
self.upa = upa
self.upd_version = upa.upd_version
self.upd = SegmentSequence(
message.find_segments('HIUPD')
)
for seg in message.find_segments(HIRMG2):
for response in seg.responses:
if not internal_send:
self._log_response(None, response)
self._call_callbacks(None, response)
self._process_response(dialog, None, response)
for seg in message.find_segments(HIRMS2):
for response in seg.responses:
segment = None # FIXME: Provide segment
if not internal_send:
self._log_response(segment, response)
self._call_callbacks(segment, response)
self._process_response(dialog, segment, response)
def _send_with_possible_retry(self, dialog, command_seg, resume_func):
response = dialog._send(command_seg)
return resume_func(command_seg, response)
def __enter__(self):
if self._standing_dialog:
raise Exception("Cannot double __enter__() {}".format(self))
self._standing_dialog = self._get_dialog()
self._standing_dialog.__enter__()
def __exit__(self, exc_type, exc_value, traceback):
if self._standing_dialog:
if exc_type is not None and issubclass(exc_type, FinTSSCARequiredError):
# In case of SCARequiredError, the dialog has already been closed by the bank
self._standing_dialog.open = False
else:
self._standing_dialog.__exit__(exc_type, exc_value, traceback)
else:
raise Exception("Cannot double __exit__() {}".format(self))
self._standing_dialog = None
def _get_dialog(self, lazy_init=False):
if lazy_init and self._standing_dialog:
raise Exception("Cannot _get_dialog(lazy_init=True) with _standing_dialog")
if self._standing_dialog:
return self._standing_dialog
if not lazy_init:
self._ensure_system_id()
return self._new_dialog(lazy_init=lazy_init)
def _set_data_v1(self, data):
self.system_id = data.get('system_id', self.system_id)
if all(x in data for x in ('bpd_bin', 'bpa_bin', 'bpd_version')):
if data['bpd_version'] >= self.bpd_version and data['bpa_bin']:
self.bpd = SegmentSequence(data['bpd_bin'])
self.bpa = SegmentSequence(data['bpa_bin']).segments[0]
self.bpd_version = data['bpd_version']
if all(x in data for x in ('upd_bin', 'upa_bin', 'upd_version')):
if data['upd_version'] >= self.upd_version and data['upa_bin']:
self.upd = SegmentSequence(data['upd_bin'])
self.upa = SegmentSequence(data['upa_bin']).segments[0]
self.upd_version = data['upd_version']
def _deconstruct_v1(self, including_private=False):
data = {
"system_id": self.system_id,
"bpd_bin": self.bpd.render_bytes(),
"bpa_bin": FinTS3Serializer().serialize_message(self.bpa) if self.bpa else None,
"bpd_version": self.bpd_version,
}
if including_private:
data.update({
"upd_bin": self.upd.render_bytes(),
"upa_bin": FinTS3Serializer().serialize_message(self.upa) if self.upa else None,
"upd_version": self.upd_version,
})
return data
def deconstruct(self, including_private: bool=False) -> bytes:
"""Return state of this FinTSClient instance as an opaque datablob. You should not
use this object after calling this method.
Information about the connection is implicitly retrieved from the bank and
cached in the FinTSClient. This includes: system identifier, bank parameter
data, user parameter data. It's not strictly required to retain this information
across sessions, but beneficial. If possible, an API user SHOULD use this method
to serialize the client instance before destroying it, and provide the serialized
data next time an instance is constructed.
Parameter `including_private` should be set to True, if the storage is sufficiently
secure (with regards to confidentiality) to include private data, specifically,
account numbers and names. Most often this is the case.
Note: No connection information is stored in the datablob, neither is the PIN.
"""
data = self._deconstruct_v1(including_private=including_private)
return compress_datablob(DATA_BLOB_MAGIC, 1, data)
def set_data(self, blob: bytes):
"""Restore a datablob created with deconstruct().
You should only call this method once, and only immediately after constructing
the object and before calling any other method or functionality (e.g. __enter__()).
For convenience, you can pass the `from_data` parameter to __init__()."""
decompress_datablob(DATA_BLOB_MAGIC, blob, self)
def _log_response(self, segment, response):
if response.code[0] in ('0', '1'):
log_target = logger.info
elif response.code[0] in ('3',):
log_target = logger.warning
else:
log_target = logger.error
log_target("Dialog response: {} - {}{}".format(
response.code,
response.text,
" ({!r})".format(response.parameters) if response.parameters else ""),
extra={
'fints_response_code': response.code,
'fints_response_text': response.text,
'fints_response_parameters': response.parameters,
}
)
def get_information(self):
"""
Return information about the connected bank.
Note: Can only be filled after the first communication with the bank.
If in doubt, use a construction like::
f = FinTS3Client(...)
with f:
info = f.get_information()
Returns a nested dictionary::
bank:
name: Bank Name
supported_operations: dict(FinTSOperations -> boolean)
supported_formats: dict(FinTSOperation -> ['urn:iso:std:iso:20022:tech:xsd:pain.001.003.03', ...])
supported_sepa_formats: ['urn:iso:std:iso:20022:tech:xsd:pain.001.003.03', ...]
accounts:
- iban: IBAN
account_number: Account Number
subaccount_number: Sub-Account Number
bank_identifier: fints.formals.BankIdentifier(...)
customer_id: Customer ID
type: Account type
currency: Currency
owner_name: ['Owner Name 1', 'Owner Name 2 (optional)']
product_name: Account product name
supported_operations: dict(FinTSOperations -> boolean)
- ...
"""
retval = {
'bank': {},
'accounts': [],
'auth': {},
}
if self.bpa:
retval['bank']['name'] = self.bpa.bank_name
if self.bpd.segments:
retval['bank']['supported_operations'] = {
op: any(self.bpd.find_segment_first(cmd[0]+'I'+cmd[2:]+'S') for cmd in op.value)
for op in FinTSOperations
}
retval['bank']['supported_formats'] = {}
for op in FinTSOperations:
for segment in (self.bpd.find_segment_first(cmd[0] + 'I' + cmd[2:] + 'S') for cmd in op.value):
if not hasattr(segment, 'parameter'):
continue
formats = getattr(segment.parameter, 'supported_sepa_formats', [])
retval['bank']['supported_formats'][op] = list(
set(retval['bank']['supported_formats'].get(op, [])).union(set(formats))
)
hispas = self.bpd.find_segment_first('HISPAS')
if hispas:
retval['bank']['supported_sepa_formats'] = list(hispas.parameter.supported_sepa_formats)
else:
retval['bank']['supported_sepa_formats'] = []
if self.upd.segments:
for upd in self.upd.find_segments('HIUPD'):
acc = {}
acc['iban'] = upd.iban
acc['account_number'] = upd.account_information.account_number
acc['subaccount_number'] = upd.account_information.subaccount_number
acc['bank_identifier'] = upd.account_information.bank_identifier
acc['customer_id'] = upd.customer_id
acc['type'] = upd.account_type
acc['currency'] = upd.account_currency
acc['owner_name'] = []
if upd.name_account_owner_1:
acc['owner_name'].append(upd.name_account_owner_1)
if upd.name_account_owner_2:
acc['owner_name'].append(upd.name_account_owner_2)
acc['product_name'] = upd.account_product_name
acc['supported_operations'] = {
op: any(allowed_transaction.transaction in op.value for allowed_transaction in upd.allowed_transactions)
for op in FinTSOperations
}
retval['accounts'].append(acc)
return retval
def _get_sepa_accounts(self, command_seg, response):
self.accounts = []
for seg in response.find_segments(HISPA1, throw=True):
self.accounts.extend(seg.accounts)
return [a for a in [acc.as_sepa_account() for acc in self.accounts] if a]
def get_sepa_accounts(self):
"""
Returns a list of SEPA accounts
:return: List of SEPAAccount objects.
"""
seg = HKSPA1()
with self._get_dialog() as dialog:
return self._send_with_possible_retry(dialog, seg, self._get_sepa_accounts)
def _continue_fetch_with_touchdowns(self, command_seg, response):
for resp in response.response_segments(command_seg, *self._touchdown_args, **self._touchdown_kwargs):
self._touchdown_responses.append(resp)
touchdown = None
for response in response.responses(command_seg, '3040'):
touchdown = response.parameters[0]
break
if touchdown:
logger.info('Fetching more results ({})...'.format(self._touchdown_counter))
self._touchdown_counter += 1
if touchdown:
seg = self._touchdown_segment_factory(touchdown)
return self._send_with_possible_retry(self._touchdown_dialog, seg, self._continue_fetch_with_touchdowns)
else:
return self._touchdown_response_processor(self._touchdown_responses)
def _fetch_with_touchdowns(self, dialog, segment_factory, response_processor, *args, **kwargs):
"""Execute a sequence of fetch commands on dialog.
segment_factory must be a callable with one argument touchdown. Will be None for the
first call and contains the institute's touchdown point on subsequent calls.
segment_factory must return a command segment.
response_processor can be a callable that will be passed the return value of this function and can
return a new value instead.
Extra arguments will be passed to FinTSMessage.response_segments.
Return value is a concatenated list of the return values of FinTSMessage.response_segments().
"""
self._touchdown_responses = []
self._touchdown_counter = 1
self._touchdown = None
self._touchdown_dialog = dialog
self._touchdown_segment_factory = segment_factory
self._touchdown_response_processor = response_processor
self._touchdown_args = args
self._touchdown_kwargs = kwargs
seg = segment_factory(self._touchdown)
return self._send_with_possible_retry(dialog, seg, self._continue_fetch_with_touchdowns)
def _find_highest_supported_command(self, *segment_classes, **kwargs):
"""Search the BPD for the highest supported version of a segment."""
return_parameter_segment = kwargs.get("return_parameter_segment", False)
parameter_segment_name = "{}I{}S".format(segment_classes[0].TYPE[0], segment_classes[0].TYPE[2:])
version_map = dict((clazz.VERSION, clazz) for clazz in segment_classes)
max_version = self.bpd.find_segment_highest_version(parameter_segment_name, version_map.keys())
if not max_version:
raise FinTSUnsupportedOperation('No supported {} version found. I support {}, bank supports {}.'.format(
parameter_segment_name,
tuple(version_map.keys()),
tuple(v.header.version for v in self.bpd.find_segments(parameter_segment_name))
))
if return_parameter_segment:
return max_version, version_map.get(max_version.header.version)
else:
return version_map.get(max_version.header.version)
def get_transactions(self, account: SEPAAccount, start_date: datetime.date = None, end_date: datetime.date = None):
"""
Fetches the list of transactions of a bank account in a certain timeframe.
:param account: SEPA
:param start_date: First day to fetch
:param end_date: Last day to fetch
:return: A list of mt940.models.Transaction objects
"""
with self._get_dialog() as dialog:
hkkaz = self._find_highest_supported_command(HKKAZ5, HKKAZ6, HKKAZ7)
logger.info('Start fetching from {} to {}'.format(start_date, end_date))
response = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkkaz(
account=hkkaz._fields['account'].type.from_sepa_account(account),
all_accounts=False,
date_start=start_date,
date_end=end_date,
touchdown_point=touchdown,
),
lambda responses: mt940_to_array(''.join([seg.statement_booked.decode('iso-8859-1') for seg in responses])),
'HIKAZ',
# Note 1: Some banks send the HIKAZ data in arbitrary splits.
# So better concatenate them before MT940 parsing.
# Note 2: MT940 messages are encoded in the S.W.I.F.T character set,
# which is a subset of ISO 8859. There are no character in it that
# differ between ISO 8859 variants, so we'll arbitrarily chose 8859-1.
)
logger.info('Fetching done.')
return response
@staticmethod
def _response_handler_get_transactions_xml(responses):
booked_streams = []
pending_streams = []
for seg in responses:
booked_streams.extend(seg.statement_booked.camt_statements)
pending_streams.append(seg.statement_pending)
return booked_streams, pending_streams
def get_transactions_xml(self, account: SEPAAccount, start_date: datetime.date = None,
end_date: datetime.date = None) -> list:
"""
Fetches the list of transactions of a bank account in a certain timeframe as camt.052.001.02 XML files.
Returns both booked and pending transactions.
:param account: SEPA
:param start_date: First day to fetch
:param end_date: Last day to fetch
:return: Two lists of bytestrings containing XML documents, possibly empty: first one for booked transactions,
second for pending transactions
"""
with self._get_dialog() as dialog:
hkcaz = self._find_highest_supported_command(HKCAZ1)
logger.info('Start fetching from {} to {}'.format(start_date, end_date))
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkcaz(
account=hkcaz._fields['account'].type.from_sepa_account(account),
all_accounts=False,
date_start=start_date,
date_end=end_date,
touchdown_point=touchdown,
supported_camt_messages=SupportedMessageTypes(['urn:iso:std:iso:20022:tech:xsd:camt.052.001.02']),
),
FinTS3Client._response_handler_get_transactions_xml,
'HICAZ'
)
logger.info('Fetching done.')
return responses
def get_credit_card_transactions(self, account: SEPAAccount, credit_card_number: str, start_date: datetime.date = None, end_date: datetime.date = None):
# FIXME Reverse engineered, probably wrong
with self._get_dialog() as dialog:
dkkku = self._find_highest_supported_command(DKKKU2)
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: dkkku(
account=dkkku._fields['account'].type.from_sepa_account(account) if account else None,
credit_card_number=credit_card_number,
date_start=start_date,
date_end=end_date,
touchdown_point=touchdown,
),
lambda responses: responses,
'DIKKU'
)
return responses
def _get_balance(self, command_seg, response):
for resp in response.response_segments(command_seg, 'HISAL'):
return resp.balance_booked.as_mt940_Balance()
def get_balance(self, account: SEPAAccount):
"""
Fetches an accounts current balance.
:param account: SEPA account to fetch the balance
:return: A mt940.models.Balance object
"""
with self._get_dialog() as dialog:
hksal = self._find_highest_supported_command(HKSAL5, HKSAL6, HKSAL7)
seg = hksal(
account=hksal._fields['account'].type.from_sepa_account(account),
all_accounts=False,
)
response = self._send_with_possible_retry(dialog, seg, self._get_balance)
return response
def get_holdings(self, account: SEPAAccount):
"""
Retrieve holdings of an account.
:param account: SEPAAccount to retrieve holdings for.
:return: List of Holding objects
"""
# init dialog
with self._get_dialog() as dialog:
hkwpd = self._find_highest_supported_command(HKWPD5, HKWPD6)
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkwpd(
account=hkwpd._fields['account'].type.from_sepa_account(account),
touchdown_point=touchdown,
),
lambda responses: responses, # TODO
'HIWPD'
)
if isinstance(responses, NeedTANResponse):
return responses
holdings = []
for resp in responses:
if type(resp.holdings) == bytes:
holding_str = resp.holdings.decode()
else:
holding_str = resp.holdings
mt535_lines = str.splitlines(holding_str)
# The first line is empty - drop it.
del mt535_lines[0]
mt535 = MT535_Miniparser()
holdings.extend(mt535.parse(mt535_lines))
if not holdings:
logger.debug('No HIWPD response segment found - maybe account has no holdings?')
return holdings
def get_scheduled_debits(self, account: SEPAAccount, multiple=False):
with self._get_dialog() as dialog:
if multiple:
command_classes = (HKDMB1, )
response_type = "HIDMB"
else:
command_classes = (HKDBS1, HKDBS2)
response_type = "HKDBS"
hkdbs = self._find_highest_supported_command(*command_classes)
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkdbs(
account=hkdbs._fields['account'].type.from_sepa_account(account),
touchdown_point=touchdown,
),
lambda responses: responses,
response_type,
)
return responses
def get_status_protocol(self):
with self._get_dialog() as dialog:
hkpro = self._find_highest_supported_command(HKPRO3, HKPRO4)
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkpro(
touchdown_point=touchdown,
),
lambda responses: responses,
'HIPRO',
)
return responses
def get_communication_endpoints(self):
with self._get_dialog() as dialog:
hkkom = self._find_highest_supported_command(HKKOM4)
responses = self._fetch_with_touchdowns(
dialog,
lambda touchdown: hkkom(
touchdown_point=touchdown,
),
lambda responses: responses,
'HIKOM'
)
return responses
def _find_supported_sepa_version(self, candidate_versions):
hispas = self.bpd.find_segment_first('HISPAS')
if not hispas:
logger.warning("Could not determine supported SEPA versions, is the dialogue open? Defaulting to first candidate: %s.", candidate_versions[0])
return candidate_versions[0]
bank_supported = list(hispas.parameter.supported_sepa_formats)
for candidate in candidate_versions:
if "urn:iso:std:iso:20022:tech:xsd:{}".format(candidate) in bank_supported:
return candidate
if "urn:iso:std:iso:20022:tech:xsd:{}.xsd".format(candidate) in bank_supported:
return candidate
logger.warning("No common supported SEPA version. Defaulting to first candidate and hoping for the best: %s.", candidate_versions[0])
return candidate_versions[0]
def simple_sepa_transfer(self, account: SEPAAccount, iban: str, bic: str,
recipient_name: str, amount: Decimal, account_name: str, reason: str, instant_payment=False,
endtoend_id='NOTPROVIDED'):
"""
Simple SEPA transfer.
:param account: SEPAAccount to start the transfer from.
:param iban: Recipient's IBAN
:param bic: Recipient's BIC
:param recipient_name: Recipient name
:param amount: Amount as a ``Decimal``
:param account_name: Sender account name
:param reason: Transfer reason
:param instant_payment: Whether to use instant payment (defaults to ``False``)
:param endtoend_id: End-to-end-Id (defaults to ``NOTPROVIDED``)
:return: Returns either a NeedRetryResponse or TransactionResponse
"""
config = {
"name": account_name,
"IBAN": account.iban,
"BIC": account.bic,
"batch": False,
"currency": "EUR",
}
version = self._find_supported_sepa_version(['pain.001.001.03', 'pain.001.003.03'])
sepa = SepaTransfer(config, version)
payment = {
"name": recipient_name,
"IBAN": iban,
"BIC": bic,
"amount": round(Decimal(amount) * 100), # in cents
"execution_date": datetime.date(1999, 1, 1),
"description": reason,
"endtoend_id": endtoend_id,
}
sepa.add_payment(payment)
xml = sepa.export().decode()
return self.sepa_transfer(account, xml, pain_descriptor="urn:iso:std:iso:20022:tech:xsd:"+version, instant_payment=instant_payment)
def sepa_transfer(self, account: SEPAAccount, pain_message: str, multiple=False,
control_sum=None, currency='EUR', book_as_single=False,
pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.001.001.03', instant_payment=False):
"""
Custom SEPA transfer.
:param account: SEPAAccount to send the transfer from.
:param pain_message: SEPA PAIN message containing the transfer details.
:param multiple: Whether this message contains multiple transfers.
:param control_sum: Sum of all transfers (required if there are multiple)
:param currency: Transfer currency
:param book_as_single: Kindly ask the bank to put multiple transactions as separate lines on the bank statement (defaults to ``False``)
:param pain_descriptor: URN of the PAIN message schema used.
:param instant_payment: Whether this is an instant transfer (defaults to ``False``)
:return: Returns either a NeedRetryResponse or TransactionResponse
"""
with self._get_dialog() as dialog:
if multiple:
command_class = HKIPM1 if instant_payment else HKCCM1
else:
command_class = HKIPZ1 if instant_payment else HKCCS1
hiccxs, hkccx = self._find_highest_supported_command(
command_class,
return_parameter_segment=True
)
seg = hkccx(
account=hkccx._fields['account'].type.from_sepa_account(account),
sepa_descriptor=pain_descriptor,
sepa_pain_message=pain_message.encode(),
)
# if instant_payment:
# seg.allow_convert_sepa_transfer = True
if multiple:
if hiccxs.parameter.sum_amount_required and control_sum is None:
raise ValueError("Control sum required.")
if book_as_single and not hiccxs.parameter.single_booking_allowed:
raise FinTSUnsupportedOperation("Single booking not allowed by bank.")
if control_sum:
seg.sum_amount.amount = control_sum
seg.sum_amount.currency = currency
if book_as_single:
seg.request_single_booking = True
return self._send_with_possible_retry(dialog, seg, self._continue_sepa_transfer)
def _continue_sepa_transfer(self, command_seg, response):
retval = TransactionResponse(response)
for seg in response.find_segments(HIRMS2):
for resp in seg.responses:
retval.set_status_if_higher(_RESPONSE_STATUS_MAPPING.get(resp.code[0], ResponseStatus.UNKNOWN))
retval.responses.append(resp)
return retval
def _continue_dialog_initialization(self, command_seg, response):
return response
def sepa_debit(self, account: SEPAAccount, pain_message: str, multiple=False, cor1=False,
control_sum=None, currency='EUR', book_as_single=False,
pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.008.003.01'):
"""
Custom SEPA debit.
:param account: SEPAAccount to send the debit from.
:param pain_message: SEPA PAIN message containing the debit details.
:param multiple: Whether this message contains multiple debits.
:param cor1: Whether to use COR1 debit (lead time reduced to 1 day)
:param control_sum: Sum of all debits (required if there are multiple)
:param currency: Debit currency
:param book_as_single: Kindly ask the bank to put multiple transactions as separate lines on the bank statement (defaults to ``False``)
:param pain_descriptor: URN of the PAIN message schema used. Defaults to ``urn:iso:std:iso:20022:tech:xsd:pain.008.003.01``.
:return: Returns either a NeedRetryResponse or TransactionResponse (with data['task_id'] set, if available)
"""
with self._get_dialog() as dialog:
if multiple:
if cor1:
command_candidates = (HKDMC1, )
else:
command_candidates = (HKDME1, HKDME2)
else:
if cor1:
command_candidates = (HKDSC1, )
else:
command_candidates = (HKDSE1, HKDSE2)
hidxxs, hkdxx = self._find_highest_supported_command(
*command_candidates,
return_parameter_segment=True
)
seg = hkdxx(
account=hkdxx._fields['account'].type.from_sepa_account(account),
sepa_descriptor=pain_descriptor,
sepa_pain_message=pain_message.encode(),
)
if multiple:
if hidxxs.parameter.sum_amount_required and control_sum is None:
raise ValueError("Control sum required.")
if book_as_single and not hidxxs.parameter.single_booking_allowed:
raise FinTSUnsupportedOperation("Single booking not allowed by bank.")
if control_sum:
seg.sum_amount.amount = control_sum
seg.sum_amount.currency = currency
if book_as_single:
seg.request_single_booking = True
return self._send_with_possible_retry(dialog, seg, self._continue_sepa_debit)
def _continue_sepa_debit(self, command_seg, response):
retval = TransactionResponse(response)
for seg in response.find_segments(HIRMS2):
for resp in seg.responses:
retval.set_status_if_higher(_RESPONSE_STATUS_MAPPING.get(resp.code[0], ResponseStatus.UNKNOWN))
retval.responses.append(resp)
for seg in response.find_segments(DebitResponseBase):
if seg.task_id:
retval.data['task_id'] = seg.task_id
if not 'task_id' in retval.data:
for seg in response.find_segments('HITAN'):
if hasattr(seg, 'task_reference') and seg.task_reference:
retval.data['task_id'] = seg.task_reference
return retval
def add_response_callback(self, cb):
# FIXME document
self.response_callbacks.append(cb)
def remove_response_callback(self, cb):
# FIXME document
self.response_callbacks.remove(cb)
def set_product(self, product_name, product_version):
"""Set the product name and version that is transmitted as part of our identification
According to 'FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals',
version 3.0, section C.3.1.3, you should fill this with useful information about the
end-user product, *NOT* the FinTS library."""
self.product_name = product_name
self.product_version = product_version
def _call_callbacks(self, *cb_data):
for cb in self.response_callbacks:
cb(*cb_data)
def pause_dialog(self):
"""Pause a standing dialog and return the saved dialog state.
Sometimes, for example in a web app, it's not possible to keep a context open
during user input. In some cases, though, it's required to send a response
within the same dialog that issued the original task (f.e. TAN with TANTimeDialogAssociation.NOT_ALLOWED).
This method freezes the current standing dialog (started with FinTS3Client.__enter__()) and
returns the frozen state.
Commands MUST NOT be issued in the dialog after calling this method.
MUST be used in conjunction with deconstruct()/set_data().
Caller SHOULD ensure that the dialog is resumed (and properly ended) within a reasonable amount of time.
:Example:
::
client = FinTS3PinTanClient(..., from_data=None)
with client:
challenge = client.sepa_transfer(...)
dialog_data = client.pause_dialog()
# dialog is now frozen, no new commands may be issued
# exiting the context does not end the dialog
client_data = client.deconstruct()
# Store dialog_data and client_data out-of-band somewhere
# ... Some time passes ...
# Later, possibly in a different process, restore the state
client = FinTS3PinTanClient(..., from_data=client_data)
with client.resume_dialog(dialog_data):
client.send_tan(...)
# Exiting the context here ends the dialog, unless frozen with pause_dialog() again.
"""
if not self._standing_dialog:
raise Exception("Cannot pause dialog, no standing dialog exists")
return self._standing_dialog.pause()
@contextmanager
def resume_dialog(self, dialog_data):
# FIXME document, test, NOTE NO UNTRUSTED SOURCES
if self._standing_dialog:
raise Exception("Cannot resume dialog, existing standing dialog")
self._standing_dialog = FinTSDialog.create_resume(self, dialog_data)
with self._standing_dialog:
yield self
self._standing_dialog = None
class NeedTANResponse(NeedRetryResponse):
challenge_raw = None #: Raw challenge as received by the bank
challenge = None #: Textual challenge to be displayed to the user
challenge_html = None #: HTML-safe challenge text, possibly with formatting
challenge_hhduc = None #: HHD_UC challenge to be transmitted to the TAN generator
challenge_matrix = None #: Matrix code challenge: tuple(mime_type, data)
def __init__(self, command_seg, tan_request, resume_method=None, tan_request_structured=False):
self.command_seg = command_seg
self.tan_request = tan_request
self.tan_request_structured = tan_request_structured
if hasattr(resume_method, '__func__'):
self.resume_method = resume_method.__func__.__name__
else:
self.resume_method = resume_method
self._parse_tan_challenge()
def __repr__(self):
return ''.format(o=self)
@classmethod
def _from_data_v1(cls, data):
if data["version"] == 1:
segs = SegmentSequence(data['segments_bin']).segments
return cls(segs[0], segs[1], data['resume_method'], data['tan_request_structured'])
raise Exception("Wrong blob data version")
def get_data(self) -> bytes:
"""Return a compressed datablob representing this object.
To restore the object, use :func:`fints.client.NeedRetryResponse.from_data`.
"""
data = {
"_class_name": self.__class__.__name__,
"version": 1,
"segments_bin": SegmentSequence([self.command_seg, self.tan_request]).render_bytes(),
"resume_method": self.resume_method,
"tan_request_structured": self.tan_request_structured,
}
return compress_datablob(DATA_BLOB_MAGIC_RETRY, 1, data)
def _parse_tan_challenge(self):
self.challenge_raw = self.tan_request.challenge
self.challenge = self.challenge_raw
self.challenge_html = None
self.challenge_hhduc = None
self.challenge_matrix = None
if hasattr(self.tan_request, 'challenge_hhduc'):
if self.tan_request.challenge_hhduc:
if len(self.tan_request.challenge_hhduc) < 256:
self.challenge_hhduc = self.tan_request.challenge_hhduc.decode('us-ascii')
else:
data = self.tan_request.challenge_hhduc
type_len_field, data = data[:2], data[2:]
if len(type_len_field) == 2:
type_len = type_len_field[0]*256 + type_len_field[1]
type_data, data = data[:type_len], data[type_len:]
content_len_field, data = data[:2], data[2:]
if len(content_len_field) == 2:
content_len = content_len_field[0]*256 + content_len_field[1]
content_data, data = data[:content_len], data[content_len:]
self.challenge_matrix = (type_data.decode('us-ascii', 'replace'), content_data)
if self.challenge.startswith('CHLGUC '):
l = self.challenge[8:12]
if l.isdigit():
self.challenge_hhduc = self.challenge[12:(12+int(l,10))]
self.challenge = self.challenge[(12+int(l,10)):]
if self.challenge_hhduc.startswith('iVBO'):
self.challenge_matrix = ('image/png', b64decode(self.challenge_hhduc))
self.challenge_hhduc = None
if self.challenge.startswith('CHLGTEXT'):
self.challenge = self.challenge[12:]
if self.tan_request_structured:
self.challenge_html = bleach.clean(
self.challenge,
tags=['br', 'p', 'b', 'i', 'u', 'ul', 'ol', 'li'],
attributes={},
)
else:
self.challenge_html = bleach.clean(self.challenge, tags=[])
# Note: Implementing HKTAN#6 implies support for Strong Customer Authentication (SCA)
# which may require TANs for many more operations including dialog initialization.
# We do not currently support that.
IMPLEMENTED_HKTAN_VERSIONS = {
2: HKTAN2,
3: HKTAN3,
5: HKTAN5,
6: HKTAN6,
}
class FinTS3PinTanClient(FinTS3Client):
def __init__(self, bank_identifier, user_id, pin, server, customer_id=None, *args, **kwargs):
self.pin = Password(pin) if pin is not None else pin
self._pending_tan = None
self.connection = FinTSHTTPSConnection(server)
self.allowed_security_functions = []
self.selected_security_function = None
self.selected_tan_medium = None
self._bootstrap_mode = True
super().__init__(bank_identifier=bank_identifier, user_id=user_id, customer_id=customer_id, *args, **kwargs)
def _new_dialog(self, lazy_init=False):
if self.pin is None:
enc = None
auth = []
elif not self.selected_security_function or self.selected_security_function == '999':
enc = PinTanDummyEncryptionMechanism(1)
auth = [PinTanOneStepAuthenticationMechanism(self.pin)]
else:
enc = PinTanDummyEncryptionMechanism(2)
auth = [PinTanTwoStepAuthenticationMechanism(
self,
self.selected_security_function,
self.pin,
)]
return FinTSDialog(
self,
lazy_init=lazy_init,
enc_mechanism=enc,
auth_mechanisms=auth,
)
def fetch_tan_mechanisms(self):
self.set_tan_mechanism('999')
self._ensure_system_id()
if self.get_current_tan_mechanism():
# We already got a reply through _ensure_system_id
return self.get_current_tan_mechanism()
with self._new_dialog():
return self.get_current_tan_mechanism()
def _ensure_system_id(self):
if self.system_id != SYSTEM_ID_UNASSIGNED or self.user_id == CUSTOMER_ID_ANONYMOUS:
return
with self._get_dialog(lazy_init=True) as dialog:
response = dialog.init(
HKSYN3(SynchronizationMode.NEW_SYSTEM_ID),
)
self.process_response_message(dialog, response, internal_send=True)
seg = response.find_segment_first(HISYN4)
if not seg:
raise ValueError('Could not find system_id')
self.system_id = seg.system_id
def _set_data_v1(self, data):
super()._set_data_v1(data)
self.selected_tan_medium = data.get('selected_tan_medium', self.selected_tan_medium)
self.selected_security_function = data.get('selected_security_function', self.selected_security_function)
self.allowed_security_functions = data.get('allowed_security_functions', self.allowed_security_functions)
def _deconstruct_v1(self, including_private=False):
data = super()._deconstruct_v1(including_private=including_private)
data.update({
"selected_security_function": self.selected_security_function,
"selected_tan_medium": self.selected_tan_medium,
})
if including_private:
data.update({
"allowed_security_functions": self.allowed_security_functions,
})
return data
def is_tan_media_required(self):
tan_mechanism = self.get_tan_mechanisms()[self.get_current_tan_mechanism()]
return getattr(tan_mechanism, 'supported_media_number', None) is not None and \
tan_mechanism.supported_media_number > 1 and \
tan_mechanism.description_required == DescriptionRequired.MUST
def _get_tan_segment(self, orig_seg, tan_process, tan_seg=None):
tan_mechanism = self.get_tan_mechanisms()[self.get_current_tan_mechanism()]
hktan = IMPLEMENTED_HKTAN_VERSIONS.get(tan_mechanism.VERSION)
seg = hktan(tan_process=tan_process)
if tan_process == '1':
seg.segment_type = orig_seg.header.type
account_ = getattr(orig_seg, 'account', None)
if isinstance(account_, KTI1):
seg.account = account_
raise NotImplementedError("TAN-Process 1 not implemented")
if tan_process in ('1', '3', '4') and self.is_tan_media_required():
if self.selected_tan_medium:
seg.tan_medium_name = self.selected_tan_medium
else:
seg.tan_medium_name = 'DUMMY'
if tan_process == '4' and tan_mechanism.VERSION >= 6:
seg.segment_type = orig_seg.header.type
if tan_process in ('2', '3'):
seg.task_reference = tan_seg.task_reference
if tan_process in ('1', '2'):
seg.further_tan_follows = False
return seg
def _need_twostep_tan_for_segment(self, seg):
if not self.selected_security_function or self.selected_security_function == '999':
return False
else:
hipins = self.bpd.find_segment_first(HIPINS1)
if not hipins:
return False
else:
for requirement in hipins.parameter.transaction_tans_required:
if seg.header.type == requirement.transaction:
return requirement.tan_required
return False
def _send_with_possible_retry(self, dialog, command_seg, resume_func):
with dialog:
if self._need_twostep_tan_for_segment(command_seg):
tan_seg = self._get_tan_segment(command_seg, '4')
response = dialog.send(command_seg, tan_seg)
for resp in response.responses(tan_seg):
if resp.code == '0030':
return NeedTANResponse(command_seg, response.find_segment_first('HITAN'), resume_func, self.is_challenge_structured())
if resp.code.startswith('9'):
raise Exception("Error response: {!r}".format(response))
else:
response = dialog.send(command_seg)
return resume_func(command_seg, response)
def is_challenge_structured(self):
param = self.get_tan_mechanisms()[self.get_current_tan_mechanism()]
if hasattr(param, 'challenge_structured'):
return param.challenge_structured
return False
def send_tan(self, challenge: NeedTANResponse, tan: str):
"""
Sends a TAN to confirm a pending operation.
:param challenge: NeedTANResponse to respond to
:param tan: TAN value
:return: Currently no response
"""
with self._get_dialog() as dialog:
tan_seg = self._get_tan_segment(challenge.command_seg, '2', challenge.tan_request)
self._pending_tan = tan
response = dialog.send(tan_seg)
resume_func = getattr(self, challenge.resume_method)
return resume_func(challenge.command_seg, response)
def _process_response(self, dialog, segment, response):
if response.code == '3920':
self.allowed_security_functions = list(response.parameters)
if self.selected_security_function is None or not self.selected_security_function in self.allowed_security_functions:
# Select the first available twostep security_function that we support
for security_function, parameter in self.get_tan_mechanisms().items():
if security_function == '999':
# Skip onestep TAN
continue
if parameter.tan_process != '2':
# Only support process variant 2 for now
continue
try:
self.set_tan_mechanism(parameter.security_function)
break
except NotImplementedError:
pass
else:
# Fall back to onestep
self.set_tan_mechanism('999')
if response.code == '9010':
raise FinTSClientError("Error during dialog initialization, could not fetch BPD. Please check that you "
"passed the correct bank identifier to the HBCI URL of the correct bank.")
if ((not dialog.open and response.code.startswith('9')) and not self._bootstrap_mode) or response.code in ('9340', '9910', '9930', '9931', '9942'):
# Assume all 9xxx errors in a not-yet-open dialog refer to the PIN or authentication
# During a dialog also listen for the following codes which may explicitly indicate an
# incorrect pin: 9340, 9910, 9930, 9931, 9942
# Fail-safe block all further attempts with this PIN
if self.pin:
self.pin.block()
raise FinTSClientPINError("Error during dialog initialization, PIN wrong?")
if response.code == '3938':
# Account locked, e.g. after three wrong password attempts. Theoretically, the bank might allow us to
# send a HKPSA with a TAN to unlock, but since the library currently doesn't implement it and there's only
# one chance to get it right, let's rather error iout.
if self.pin:
self.pin.block()
raise FinTSClientTemporaryAuthError("Account is temporarily locked.")
if response.code == '9075':
if self._bootstrap_mode:
if self._standing_dialog:
self._standing_dialog.open = False
else:
raise FinTSSCARequiredError("This operation requires strong customer authentication.")
def get_tan_mechanisms(self):
"""
Get the available TAN mechanisms.
Note: Only checks for HITANS versions listed in IMPLEMENTED_HKTAN_VERSIONS.
:return: Dictionary of security_function: TwoStepParameters objects.
"""
retval = OrderedDict()
for version in sorted(IMPLEMENTED_HKTAN_VERSIONS.keys()):
for seg in self.bpd.find_segments('HITANS', version):
for parameter in seg.parameter.twostep_parameters:
if parameter.security_function in self.allowed_security_functions:
retval[parameter.security_function] = parameter
return retval
def get_current_tan_mechanism(self):
return self.selected_security_function
def set_tan_mechanism(self, security_function):
if self._standing_dialog:
raise Exception("Cannot change TAN mechanism with a standing dialog")
self.selected_security_function = security_function
def set_tan_medium(self, tan_medium):
if self._standing_dialog:
raise Exception("Cannot change TAN medium with a standing dialog")
self.selected_tan_medium = tan_medium.tan_medium_name
def get_tan_media(self, media_type = TANMediaType2.ALL, media_class = TANMediaClass4.ALL):
"""Get information about TAN lists/generators.
Returns tuple of fints.formals.TANUsageOption and a list of fints.formals.TANMedia4 or fints.formals.TANMedia5 objects."""
if self.connection.url == 'https://hbci.postbank.de/banking/hbci.do':
# see https://github.com/raphaelm/python-fints/issues/101#issuecomment-572486099
context = self._new_dialog(lazy_init=True)
method = lambda dialog: dialog.init
else:
context = self._get_dialog()
method = lambda dialog: dialog.send
with context as dialog:
hktab = self._find_highest_supported_command(HKTAB4, HKTAB5)
seg = hktab(
tan_media_type=media_type,
tan_media_class=str(media_class),
)
# The specification says we should send a dummy HKTAN object but apparently it seems to do more harm than
# good.
try:
self._bootstrap_mode = True
response = method(dialog)(seg)
finally:
self._bootstrap_mode = False
for resp in response.response_segments(seg, 'HITAB'):
return resp.tan_usage_option, list(resp.tan_media_list)
def get_information(self):
retval = super().get_information()
retval['auth'] = {
'current_tan_mechanism': self.get_current_tan_mechanism(),
'tan_mechanisms': self.get_tan_mechanisms(),
}
return retval
python-fints-4.0.0/fints/connection.py 0000664 0000000 0000000 00000002501 14421014606 0020005 0 ustar 00root root 0000000 0000000 import base64
import io
import logging
import requests
from fints.utils import Password
from .exceptions import *
from .message import FinTSInstituteMessage, FinTSMessage
logger = logging.getLogger(__name__)
class FinTSHTTPSConnection:
def __init__(self, url):
self.url = url
def send(self, msg: FinTSMessage):
log_out = io.StringIO()
with Password.protect():
msg.print_nested(stream=log_out, prefix="\t")
logger.debug("Sending >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n{}\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n".format(log_out.getvalue()))
log_out.truncate(0)
r = requests.post(
self.url, data=base64.b64encode(msg.render_bytes()),
headers={
'Content-Type': 'text/plain',
},
)
if r.status_code < 200 or r.status_code > 299:
raise FinTSConnectionError('Bad status code {}'.format(r.status_code))
response = base64.b64decode(r.content.decode('iso-8859-1'))
retval = FinTSInstituteMessage(segments=response)
with Password.protect():
retval.print_nested(stream=log_out, prefix="\t")
logger.debug("Received <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n{}\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n".format(log_out.getvalue()))
return retval
python-fints-4.0.0/fints/dialog.py 0000664 0000000 0000000 00000021202 14421014606 0017104 0 ustar 00root root 0000000 0000000 import io
import logging
import pickle
from .connection import FinTSConnectionError
from .exceptions import *
from .formals import CUSTOMER_ID_ANONYMOUS, Language2, SystemIDStatus
from .message import FinTSCustomerMessage, MessageDirection
from .segments.auth import HKIDN2, HKVVB3
from .segments.dialog import HKEND1
from .segments.message import HNHBK3, HNHBS1
from .utils import compress_datablob, decompress_datablob
logger = logging.getLogger(__name__)
DIALOG_ID_UNASSIGNED = '0'
DATA_BLOB_MAGIC = b'python-fints_DIALOG_DATABLOB'
class FinTSDialog:
def __init__(self, client=None, lazy_init=False, enc_mechanism=None, auth_mechanisms=None):
self.client = client
self.next_message_number = dict((v, 1) for v in MessageDirection)
self.messages = dict((v, {}) for v in MessageDirection)
self.auth_mechanisms = auth_mechanisms or []
self.enc_mechanism = enc_mechanism
self.open = False
self.need_init = True
self.lazy_init = lazy_init
self.dialog_id = DIALOG_ID_UNASSIGNED
self.paused = False
self._context_count = 0
def __enter__(self):
if self._context_count == 0:
if not self.lazy_init:
self.init()
self._context_count += 1
return self
def __exit__(self, exc_type, exc_value, traceback):
self._context_count -= 1
if not self.paused:
if self._context_count == 0:
self.end()
def init(self, *extra_segments):
if self.paused:
raise FinTSDialogStateError("Cannot init() a paused dialog")
from fints.client import FinTSClientMode, NeedTANResponse
if self.client.mode == FinTSClientMode.OFFLINE:
raise FinTSDialogOfflineError("Cannot open a dialog with mode=FinTSClientMode.OFFLINE. "
"This is a control flow error, no online functionality "
"should have been attempted with this FinTSClient object.")
if self.need_init and not self.open:
segments = [
HKIDN2(
self.client.bank_identifier,
self.client.customer_id,
self.client.system_id,
SystemIDStatus.ID_NECESSARY if self.client.customer_id != CUSTOMER_ID_ANONYMOUS else SystemIDStatus.ID_UNNECESSARY
),
HKVVB3(
self.client.bpd_version,
self.client.upd_version,
Language2.DE,
self.client.product_name,
self.client.product_version
),
]
if self.client.mode == FinTSClientMode.INTERACTIVE and self.client.get_tan_mechanisms():
tan_seg = self.client._get_tan_segment(segments[0], '4')
segments.append(tan_seg)
else:
tan_seg = None
for s in extra_segments:
segments.append(s)
try:
self.open = True
retval = self.send(*segments, internal_send=True)
if tan_seg:
for resp in retval.responses(tan_seg):
if resp.code == '0030':
self.client.init_tan_response = NeedTANResponse(
None,
retval.find_segment_first('HITAN'),
'_continue_dialog_initialization',
self.client.is_challenge_structured()
)
self.need_init = False
return retval
except Exception as e:
self.open = False
if isinstance(e, (FinTSConnectionError, FinTSClientError)):
raise
else:
raise FinTSDialogInitError("Couldn't establish dialog with bank, Authentication data wrong?") from e
finally:
self.lazy_init = False
def end(self):
if self.paused:
raise FinTSDialogStateError("Cannot end() on a paused dialog")
if self.open:
response = self.send(HKEND1(self.dialog_id), internal_send=True)
self.open = False
def send(self, *segments, **kwargs):
internal_send = kwargs.pop('internal_send', False)
if self.paused:
raise FinTSDialogStateError("Cannot send() on a paused dialog")
if not self.open:
if self.lazy_init and self.need_init:
self.init()
if not self.open:
raise FinTSDialogStateError("Cannot send on dialog that is not open")
message = self.new_customer_message()
for s in segments:
message += s
self.finish_message(message)
assert message.segments[0].message_number == self.next_message_number[message.DIRECTION]
self.messages[message.DIRECTION][message.segments[0].message_number] = message
self.next_message_number[message.DIRECTION] += 1
response = self.client.connection.send(message)
# assert response.segments[0].message_number == self.next_message_number[response.DIRECTION]
# FIXME Better handling of HKEND in exception case
self.messages[response.DIRECTION][response.segments[0].message_number] = response
self.next_message_number[response.DIRECTION] += 1
if self.enc_mechanism:
self.enc_mechanism.decrypt(message)
for auth_mech in self.auth_mechanisms:
auth_mech.verify(message)
if self.dialog_id == DIALOG_ID_UNASSIGNED:
seg = response.find_segment_first(HNHBK3)
if not seg:
raise FinTSDialogError('Could not find dialog_id')
self.dialog_id = seg.dialog_id
self.client.process_response_message(self, response, internal_send=internal_send)
return response
def new_customer_message(self):
if self.paused:
raise FinTSDialogStateError("Cannot call new_customer_message() on a paused dialog")
message = FinTSCustomerMessage(self)
message += HNHBK3(0, 300, self.dialog_id, self.next_message_number[message.DIRECTION])
for auth_mech in self.auth_mechanisms:
auth_mech.sign_prepare(message)
return message
def finish_message(self, message):
if self.paused:
raise FinTSDialogStateError("Cannot call finish_message() on a paused dialog")
# Create signature(s) in reverse order: from inner to outer
for auth_mech in reversed(self.auth_mechanisms):
auth_mech.sign_commit(message)
message += HNHBS1(message.segments[0].message_number)
if self.enc_mechanism:
self.enc_mechanism.encrypt(message)
message.segments[0].message_size = len(message.render_bytes())
def pause(self):
# FIXME Document, test
if self.paused:
raise FinTSDialogStateError("Cannot pause a paused dialog")
external_dialog = self
external_client = self.client
class SmartPickler(pickle.Pickler):
def persistent_id(self, obj):
if obj is external_dialog:
return "dialog"
if obj is external_client:
return "client"
return None
pickle_out = io.BytesIO()
SmartPickler(pickle_out, protocol=4).dump({
k: getattr(self, k) for k in [
'next_message_number',
'messages',
'auth_mechanisms',
'enc_mechanism',
'open',
'need_init',
'lazy_init',
'dialog_id',
]
})
data_pickled = pickle_out.getvalue()
self.paused = True
return compress_datablob(DATA_BLOB_MAGIC, 1, {'data_bin': data_pickled})
@classmethod
def create_resume(cls, client, blob):
retval = cls(client=client)
decompress_datablob(DATA_BLOB_MAGIC, blob, retval)
return retval
def _set_data_v1(self, data):
external_dialog = self
external_client = self.client
class SmartUnpickler(pickle.Unpickler):
def persistent_load(self, pid):
if pid == 'dialog':
return external_dialog
if pid == 'client':
return external_client
raise pickle.UnpicklingError("unsupported persistent object")
pickle_in = io.BytesIO(data['data_bin'])
data_unpickled = SmartUnpickler(pickle_in).load()
for k, v in data_unpickled.items():
setattr(self, k, v)
python-fints-4.0.0/fints/exceptions.py 0000664 0000000 0000000 00000001205 14421014606 0020027 0 ustar 00root root 0000000 0000000 class FinTSError(Exception):
pass
class FinTSClientError(FinTSError):
pass
class FinTSClientPINError(FinTSClientError):
pass
class FinTSClientTemporaryAuthError(FinTSClientError):
pass
class FinTSSCARequiredError(FinTSClientError):
pass
class FinTSDialogError(FinTSError):
pass
class FinTSDialogStateError(FinTSDialogError):
pass
class FinTSDialogOfflineError(FinTSDialogError):
pass
class FinTSDialogInitError(FinTSDialogError):
pass
class FinTSConnectionError(FinTSError):
pass
class FinTSUnsupportedOperation(FinTSError):
pass
class FinTSNoResponseError(FinTSError):
pass
python-fints-4.0.0/fints/fields.py 0000664 0000000 0000000 00000020515 14421014606 0017121 0 ustar 00root root 0000000 0000000 import datetime
import decimal
import re
import warnings
from fints.types import Container, SegmentSequence, TypedField
from fints.utils import (
DocTypeMixin, FieldRenderFormatStringMixin, FixedLengthMixin, Password,
)
class DataElementField(DocTypeMixin, TypedField):
pass
class ContainerField(TypedField):
def _check_value(self, value):
if self.type:
if not isinstance(value, self.type):
raise TypeError("Value {!r} is not of type {!r}".format(value, self.type))
super()._check_value(value)
def _default_value(self):
return self.type()
class DataElementGroupField(DocTypeMixin, ContainerField):
pass
class GenericField(FieldRenderFormatStringMixin, DataElementField):
type = None
_FORMAT_STRING = "{}"
def _parse_value(self, value):
warnings.warn("Generic field used for type {!r} value {!r}".format(self.type, value))
return value
class GenericGroupField(DataElementGroupField):
type = None
def _default_value(self):
if self.type is None:
return Container()
else:
return self.type()
def _parse_value(self, value):
if self.type is None:
warnings.warn("Generic field used for type {!r} value {!r}".format(self.type, value))
return value
class TextField(FieldRenderFormatStringMixin, DataElementField):
type = 'txt'
_DOC_TYPE = str
_FORMAT_STRING = "{}" # FIXME Restrict CRLF
def _parse_value(self, value): return str(value)
class AlphanumericField(TextField):
type = 'an'
class DTAUSField(DataElementField):
type = 'dta'
class NumericField(FieldRenderFormatStringMixin, DataElementField):
type = 'num'
_DOC_TYPE = int
_FORMAT_STRING = "{:d}"
def _parse_value(self, value):
_value = str(value)
if len(_value) > 1 and _value[0] == '0':
raise ValueError("Leading zeroes not allowed for value of type 'num': {!r}".format(value))
return int(_value, 10)
class ZeroPaddedNumericField(NumericField):
type = ''
_DOC_TYPE = int
def __init__(self, *args, **kwargs):
if not kwargs.get('length', None):
raise ValueError("ZeroPaddedNumericField needs length argument")
super().__init__(*args, **kwargs)
@property
def _FORMAT_STRING(self):
return "{:0" + str(self.length) + "d}"
def _parse_value(self, value):
_value = str(value)
return int(_value, 10)
class DigitsField(FieldRenderFormatStringMixin, DataElementField):
type = 'dig'
_DOC_TYPE = str
_FORMAT_STRING = "{}"
def _parse_value(self, value):
_value = str(value)
if not re.match(r'^\d*$', _value):
raise TypeError("Only digits allowed for value of type 'dig': {!r}".format(value))
return _value
class FloatField(DataElementField):
type = 'float'
_DOC_TYPE = float
_FORMAT_STRING = "{:.12f}" # Warning: Python's float is not exact!
# FIXME: Needs test
def _parse_value(self, value):
if isinstance(value, float):
return value
if isinstance(value, decimal.Decimal):
value = str(value.normalize()).replace(".", ",")
_value = str(value)
if not re.match(r'^(?:0|[1-9]\d*),(?:\d*[1-9]|)$', _value):
raise TypeError("Only digits and ',' allowed for value of type 'float', no superfluous leading or trailing zeroes allowed: {!r}".format(value))
return float(_value.replace(",", "."))
def _render_value(self, value):
retval = self._FORMAT_STRING.format(value)
retval = retval.replace('.', ',').rstrip('0')
self._check_value_length(retval)
return retval
class AmountField(FixedLengthMixin, DataElementField):
type = 'wrt'
_DOC_TYPE = decimal.Decimal
_FIXED_LENGTH = [None, None, 15]
# FIXME Needs test
def _parse_value(self, value):
if isinstance(value, float):
return value
if isinstance(value, decimal.Decimal):
return value
_value = str(value)
if not re.match(r'^(?:0|[1-9]\d*)(?:,)?(?:\d*[1-9]|)$', _value):
raise TypeError("Only digits and ',' allowed for value of type 'decimal', no superfluous leading or trailing zeroes allowed: {!r}".format(value))
return decimal.Decimal(_value.replace(",", "."))
def _render_value(self, value):
retval = str(value)
retval = retval.replace('.', ',').rstrip('0')
self._check_value_length(retval)
return retval
class BinaryField(DataElementField):
type = 'bin'
_DOC_TYPE = bytes
def _render_value(self, value):
retval = bytes(value)
self._check_value_length(retval)
return retval
def _parse_value(self, value): return bytes(value)
class IDField(FixedLengthMixin, AlphanumericField):
type = 'id'
_DOC_TYPE = str
_FIXED_LENGTH = [None, None, 30]
class BooleanField(FixedLengthMixin, AlphanumericField):
type = 'jn'
_DOC_TYPE = bool
_FIXED_LENGTH = [1]
def _render_value(self, value):
return "J" if value else "N"
def _parse_value(self, value):
if value is None:
return None
if value == "J" or value is True:
return True
elif value == "N" or value is False:
return False
else:
raise ValueError("Invalid value {!r} for BooleanField".format(value))
class CodeFieldMixin:
# FIXME Need tests
def __init__(self, enum=None, *args, **kwargs):
if enum:
self._DOC_TYPE = enum
self._enum = enum
else:
self._enum = None
super().__init__(*args, **kwargs)
def _parse_value(self, value):
retval = super()._parse_value(value)
if self._enum:
retval = self._enum(retval)
return retval
def _render_value(self, value):
retval = value
if self._enum:
retval = str(value.value)
return super()._render_value(retval)
def _inline_doc_comment(self, value):
retval = super()._inline_doc_comment(value)
if self._enum:
addendum = value.__doc__
if addendum and addendum is not value.__class__.__doc__:
if not retval:
retval = " # "
else:
retval = retval + ": "
retval = retval + addendum
return retval
class CodeField(CodeFieldMixin, AlphanumericField):
type = 'code'
_DOC_TYPE = str
class IntCodeField(CodeFieldMixin, NumericField):
type = ''
_DOC_TYPE = int
_FORMAT_STRING = "{}"
class CountryField(FixedLengthMixin, DigitsField):
type = 'ctr'
_FIXED_LENGTH = [3]
class CurrencyField(FixedLengthMixin, AlphanumericField):
type = 'cur'
_FIXED_LENGTH = [3]
class DateField(FixedLengthMixin, NumericField):
type = 'dat' # FIXME Need test
_DOC_TYPE = datetime.date
_FIXED_LENGTH = [8]
def _parse_value(self, value):
if isinstance(value, datetime.date):
return value
val = super()._parse_value(value)
val = str(val)
return datetime.date(int(val[0:4]), int(val[4:6]), int(val[6:8]))
def _render_value(self, value):
val = "{:04d}{:02d}{:02d}".format(value.year, value.month, value.day)
val = int(val)
return super()._render_value(val)
class TimeField(FixedLengthMixin, DigitsField):
type = 'tim' # FIXME Need test
_DOC_TYPE = datetime.time
_FIXED_LENGTH = [6]
def _parse_value(self, value):
if isinstance(value, datetime.time):
return value
val = super()._parse_value(value)
return datetime.time(int(val[0:2]), int(val[2:4]), int(val[4:6]))
def _render_value(self, value):
val = "{:02d}{:02d}{:02d}".format(value.hour, value.minute, value.second)
return super()._render_value(val)
class PasswordField(AlphanumericField):
type = ''
_DOC_TYPE = Password
def _parse_value(self, value):
return Password(value)
def _render_value(self, value):
return str(value)
class SegmentSequenceField(DataElementField):
type = 'sf'
def _parse_value(self, value):
if isinstance(value, SegmentSequence):
return value
else:
return SegmentSequence(value)
def _render_value(self, value):
return value.render_bytes()
python-fints-4.0.0/fints/formals.py 0000664 0000000 0000000 00000137547 14421014606 0017334 0 ustar 00root root 0000000 0000000 import re
from fints.fields import *
from fints.types import *
from fints.utils import RepresentableEnum, ShortReprMixin
CUSTOMER_ID_ANONYMOUS = '9999999999'
class DataElementGroup(Container):
pass
class SegmentHeader(ShortReprMixin, DataElementGroup):
"""Segmentkopf"""
type = AlphanumericField(max_length=6, _d='Segmentkennung')
number = NumericField(max_length=3, _d='Segmentnummer')
version = NumericField(max_length=3, _d='Segmentversion')
reference = NumericField(max_length=3, required=False, _d='Bezugssegment')
class ReferenceMessage(DataElementGroup):
dialog_id = DataElementField(type='id')
message_number = NumericField(max_length=4)
class SecurityMethod(RepresentableEnum):
DDV = 'DDV'
RAH = 'RAH'
RDH = 'RDH'
PIN = 'PIN'
class SecurityProfile(DataElementGroup):
"""Sicherheitsprofil"""
security_method = CodeField(enum=SecurityMethod, length=3, _d="Sicherheitsverfahren")
security_method_version = DataElementField(type='num', _d="Version des Sicherheitsverfahrens")
class IdentifiedRole(RepresentableEnum):
MS = '1' #: Message Sender
MR = '2' #: Message Receiver
class SecurityIdentificationDetails(DataElementGroup):
identified_role = CodeField(IdentifiedRole, max_length=3)
cid = DataElementField(type='bin', max_length=256)
identifier = DataElementField(type='id')
class DateTimeType(RepresentableEnum):
STS = '1' #: Sicherheitszeitstempel
CRT = '6' #: Certificate Revocation Time
class SecurityDateTime(DataElementGroup):
date_time_type = CodeField(DateTimeType, max_length=3)
date = DataElementField(type='dat', required=False)
time = DataElementField(type='tim', required=False)
class UsageEncryption(RepresentableEnum):
OSY = '2' #: Owner Symmetric
class OperationMode(RepresentableEnum):
CBC = '2' #: Cipher Block Chaining
ISO_9796_1 = '16' #: ISO 9796-1 (bei RDH)
ISO_9796_2_RANDOM = '17' #: ISO 9796-2 mit Zufallszahl (bei RDH)
PKCS1V15 = '18' #: RSASSA-PKCS#1 V1.5 (bei RDH); RSAES-PKCS#1 V1.5 (bei RAH, RDH)
PSS = '19' #: RSASSA-PSS (bei RAH, RDH)
ZZZ = '999' #: Gegenseitig vereinbart (DDV: Retail-MAC)
class EncryptionAlgorithmCoded(RepresentableEnum):
TWOKEY3DES = '13' #: 2-Key-Triple-DES
AES256 = '14' #: AES-256
class AlgorithmParameterName(RepresentableEnum):
KYE = '5' #: Symmetrischer Schlüssel, verschlüsselt mit symmetrischem Schlüssel
KYP = '6' #: Symmetrischer Schlüssel, verschlüsselt mit öffentlichem Schlüssel
class AlgorithmParameterIVName(RepresentableEnum):
IVC = '1' #: Initialization value, clear text
class EncryptionAlgorithm(DataElementGroup):
usage_encryption = CodeField(UsageEncryption, max_length=3)
operation_mode = CodeField(OperationMode, max_length=3)
encryption_algorithm = CodeField(EncryptionAlgorithmCoded, max_length=3)
algorithm_parameter_value = DataElementField(type='bin', max_length=512)
algorithm_parameter_name = CodeField(AlgorithmParameterName, max_length=3)
algorithm_parameter_iv_name = CodeField(AlgorithmParameterIVName, max_length=3)
algorithm_parameter_iv_value = DataElementField(type='bin', max_length=512, required=False)
class HashAlgorithm(DataElementGroup):
usage_hash = DataElementField(type='code', max_length=3)
hash_algorithm = DataElementField(type='code', max_length=3)
algorithm_parameter_name = DataElementField(type='code', max_length=3)
algorithm_parameter_value = DataElementField(type='bin', max_length=512, required=False)
class SignatureAlgorithm(DataElementGroup):
usage_signature = DataElementField(type='code', max_length=3)
signature_algorithm = DataElementField(type='code', max_length=3)
operation_mode = DataElementField(type='code', max_length=3)
class BankIdentifier(DataElementGroup):
COUNTRY_ALPHA_TO_NUMERIC = {
# Kapitel E.4 der SEPA-Geschäftsvorfälle
'BE': '056',
'BG': '100',
'DK': '208',
'DE': '280',
'FI': '246',
'FR': '250',
'GR': '300',
'GB': '826',
'IE': '372',
'IS': '352',
'IT': '380',
'JP': '392',
'CA': '124',
'HR': '191',
'LI': '438',
'LU': '442',
'NL': '528',
'AT': '040',
'PL': '616',
'PT': '620',
'RO': '642',
'RU': '643',
'SE': '752',
'CH': '756',
'SK': '703',
'SI': '705',
'ES': '724',
'CZ': '203',
'TR': '792',
'HU': '348',
'US': '840',
'EU': '978'
}
COUNTRY_NUMERIC_TO_ALPHA = {v: k for k, v in COUNTRY_ALPHA_TO_NUMERIC.items()}
COUNTRY_NUMERIC_TO_ALPHA['276'] = 'DE' # not yet in use by banks, but defined by ISO
country_identifier = DataElementField(type='ctr')
bank_code = DataElementField(type='an', max_length=30)
class KeyType(RepresentableEnum):
"""Schlüsselart"""
D = 'D' #: Schlüssel zur Erzeugung digitaler Signaturen
S = 'S' #: Signierschlüssel
V = 'V' #: Chiffrierschlüssel
class KeyName(DataElementGroup):
bank_identifier = DataElementGroupField(type=BankIdentifier)
user_id = DataElementField(type='id')
key_type = CodeField(KeyType, length=1, _d="Schlüsselart")
key_number = DataElementField(type='num', max_length=3)
key_version = DataElementField(type='num', max_length=3)
class Certificate(DataElementGroup):
certificate_type = DataElementField(type='code')
certificate_content = DataElementField(type='bin', max_length=4096)
class UserDefinedSignature(DataElementGroup):
pin = PasswordField(max_length=99)
tan = DataElementField(type='an', max_length=99, required=False)
class Response(DataElementGroup):
code = DataElementField(type='dig', length=4)
reference_element = DataElementField(type='an', max_length=7)
text = DataElementField(type='an', max_length=80)
parameters = DataElementField(type='an', max_length=35, max_count=10, required=False)
class Amount1(DataElementGroup):
"""Betrag
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
amount = DataElementField(type='wrt', _d="Wert")
currency = DataElementField(type='cur', _d="Währung")
class AccountInformation(DataElementGroup):
account_number = DataElementField(type='id')
subaccount_number = DataElementField(type='id')
bank_identifier = DataElementGroupField(type=BankIdentifier)
class AccountLimit(DataElementGroup):
limit_type = DataElementField(type='code', length=1)
limit_amount = DataElementGroupField(type=Amount1, required=False)
limit_days = DataElementField(type='num', max_length=3, required=False)
class AllowedTransaction(DataElementGroup):
transaction = DataElementField(type='an', max_length=6)
required_signatures = DataElementField(type='num', max_length=2)
limit_type = DataElementField(type='code', length=1, required=False)
limit_amount = DataElementGroupField(type=Amount1, required=False)
limit_days = DataElementField(type='num', max_length=3, required=False)
class TANTimeDialogAssociation(RepresentableEnum):
NOT_ALLOWED = '1' #: TAN nicht zeitversetzt / dialogübergreifend erlaubt
ALLOWED = '2' #: TAN zeitversetzt / dialogübergreifend erlaubt
BOTH = '3' #: beide Verfahren unterstützt
NOT_APPLICABLE = '4' #: nicht zutreffend
class AllowedFormat(RepresentableEnum):
NUMERIC = '1' #: numerisch
ALPHANUMERIC = '2' #: alfanumerisch
class TANListNumberRequired(RepresentableEnum):
NO = '0' #: Nein
YES = '2' #: Ja
class InitializationMode(RepresentableEnum):
CLEARTEXT_PIN_NO_TAN = '00' #: Initialisierungsverfahren mit Klartext-PIN und ohne TAN
ENCRYPTED_PIN_NO_TAN = '01' #: Schablone 01: Verschlüsselte PIN und ohne TAN
MASK_02 = '02' #: Schablone 02: Reserviert, bei FinTS zur Zeit nicht verwendet
class DescriptionRequired(RepresentableEnum):
MUST_NOT = '0' #: Bezeichnung des TAN-Mediums darf nicht angegeben werden
MAY = '1' #: Bezeichnung des TAN-Mediums kann angegeben werden
MUST = '2' #: Bezeichnung des TAN-Mediums muss angegeben werden
class SMSChargeAccountRequired(RepresentableEnum):
MUST_NOT = '0' #: SMS-Abbuchungskonto darf nicht angegeben werden
MAY = '1' #: SMS-Abbuchungskonto kann angegeben werden
MUST = '2' #: SMS-Abbuchungskonto muss angegeben werden
class PrincipalAccountRequired(RepresentableEnum):
MUST_NOT = '0' #: Auftraggeberkonto darf nicht angegeben werden
MUST = '2' #: Auftraggeberkonto muss angegeben werden, wenn im Geschäftsvorfall enthalten
class TaskHashAlgorithm(RepresentableEnum):
NONE = '0' #: Auftrags-Hashwert nicht unterstützt
RIPEMD_160 = '1' #: RIPEMD-160
SHA_1 = '2' #: SHA-1
class TwoStepParametersCommon(DataElementGroup):
@property
def VERSION(self):
"""TAN mechanism version"""
return int(re.match(r'^(\D+)(\d+)$', self.__class__.__name__).group(2))
security_function = DataElementField(type='code', max_length=3, _d="Sicherheitsfunktion kodiert")
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
tech_id = DataElementField(type='id', _d="Technische Identifikation TAN-Verfahren")
class TwoStepParameters1(TwoStepParametersCommon):
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=3, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
number_of_supported_lists = DataElementField(type='num', length=1, _d="Anzahl unterstützter aktiver TAN-Listen")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_delayed_allowed = DataElementField(type='jn', _d="TAN zeitversetzt/dialogübergreifend erlaubt")
class TwoStepParameters2(TwoStepParametersCommon):
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=3, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
number_of_supported_lists = DataElementField(type='num', length=1, _d="Anzahl unterstützter aktiver TAN-Listen")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
tan_list_number_required = CodeField(enum=TANListNumberRequired, length=1, _d="TAN-Listennummer erforderlich")
cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
challenge_value_required = DataElementField(type='jn', _d="Challenge-Betrag erforderlich")
class TwoStepParameters3(TwoStepParametersCommon):
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=3, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
number_of_supported_lists = DataElementField(type='num', length=1, _d="Anzahl unterstützter aktiver TAN-Listen")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
tan_list_number_required = CodeField(enum=TANListNumberRequired, length=1, _d="TAN-Listennummer erforderlich")
cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
challenge_value_required = DataElementField(type='jn', _d="Challenge-Betrag erforderlich")
initialization_mode = CodeField(enum=InitializationMode, _d="Initialisierungsmodus")
description_required = CodeField(enum=DescriptionRequired, length=1, _d="Bezeichnung des TAN-Medium erforderlich")
supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
class TwoStepParameters4(TwoStepParametersCommon):
zka_id = DataElementField(type='an', max_length=32, _d="ZKA TAN-Verfahren")
zka_version = DataElementField(type='an', max_length=10, _d="Version ZKA TAN-Verfahren")
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=3, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
number_of_supported_lists = DataElementField(type='num', length=1, _d="Anzahl unterstützter aktiver TAN-Listen")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
tan_list_number_required = CodeField(enum=TANListNumberRequired, length=1, _d="TAN-Listennummer erforderlich")
cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
sms_charge_account_required = DataElementField(type='jn', _d="SMS-Abbuchungskonto erforderlich")
challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
challenge_value_required = DataElementField(type='jn', _d="Challenge-Betrag erforderlich")
challenge_structured = DataElementField(type='jn', _d="Challenge strukturiert")
initialization_mode = CodeField(enum=InitializationMode, _d="Initialisierungsmodus")
description_required = CodeField(enum=DescriptionRequired, length=1, _d="Bezeichnung des TAN-Medium erforderlich")
supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
class TwoStepParameters5(TwoStepParametersCommon):
zka_id = DataElementField(type='an', max_length=32, _d="ZKA TAN-Verfahren")
zka_version = DataElementField(type='an', max_length=10, _d="Version ZKA TAN-Verfahren")
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=4, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
number_of_supported_lists = DataElementField(type='num', length=1, _d="Anzahl unterstützter aktiver TAN-Listen")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
tan_list_number_required = CodeField(enum=TANListNumberRequired, length=1, _d="TAN-Listennummer erforderlich")
cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
sms_charge_account_required = CodeField(enum=SMSChargeAccountRequired, length=1, _d="SMS-Abbuchungskonto erforderlich")
principal_account_required = CodeField(enum=PrincipalAccountRequired, length=1, _d="Auftraggeberkonto erforderlich")
challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
challenge_structured = DataElementField(type='jn', _d="Challenge strukturiert")
initialization_mode = CodeField(enum=InitializationMode, _d="Initialisierungsmodus")
description_required = CodeField(enum=DescriptionRequired, length=1, _d="Bezeichnung des TAN-Medium erforderlich")
supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
class TwoStepParameters6(TwoStepParametersCommon):
zka_id = DataElementField(type='an', max_length=32, _d="ZKA TAN-Verfahren")
zka_version = DataElementField(type='an', max_length=10, _d="Version ZKA TAN-Verfahren")
name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
max_length_return_value = DataElementField(type='num', max_length=4, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
sms_charge_account_required = CodeField(enum=SMSChargeAccountRequired, length=1, _d="SMS-Abbuchungskonto erforderlich")
principal_account_required = CodeField(enum=PrincipalAccountRequired, length=1, _d="Auftraggeberkonto erforderlich")
challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
challenge_structured = DataElementField(type='jn', _d="Challenge strukturiert")
initialization_mode = CodeField(enum=InitializationMode, _d="Initialisierungsmodus")
description_required = CodeField(enum=DescriptionRequired, length=1, _d="Bezeichnung des TAN-Medium erforderlich")
response_hhd_uc_required = DataElementField(type='jn', _d="Antwort HHD_UC erforderlich")
supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
class ParameterTwostepCommon(DataElementGroup):
onestep_method_allowed = DataElementField(type='jn')
multiple_tasks_allowed = DataElementField(type='jn')
task_hash_algorithm = CodeField(enum=TaskHashAlgorithm, length=1, _d="Auftrags-Hashwertverfahren")
class ParameterTwostepTAN1(ParameterTwostepCommon):
security_profile_bank_signature = DataElementField(type='code', length=1)
twostep_parameters = DataElementGroupField(type=TwoStepParameters1, min_count=1, max_count=98)
class ParameterTwostepTAN2(ParameterTwostepCommon):
twostep_parameters = DataElementGroupField(type=TwoStepParameters2, min_count=1, max_count=98)
class ParameterTwostepTAN3(ParameterTwostepCommon):
twostep_parameters = DataElementGroupField(type=TwoStepParameters3, min_count=1, max_count=98)
class ParameterTwostepTAN4(ParameterTwostepCommon):
twostep_parameters = DataElementGroupField(type=TwoStepParameters4, min_count=1, max_count=98)
class ParameterTwostepTAN5(ParameterTwostepCommon):
twostep_parameters = DataElementGroupField(type=TwoStepParameters5, min_count=1, max_count=98)
class ParameterTwostepTAN6(ParameterTwostepCommon):
twostep_parameters = DataElementGroupField(type=TwoStepParameters6, min_count=1, max_count=98)
class TransactionTanRequired(DataElementGroup):
transaction = DataElementField(type='an', max_length=6)
tan_required = DataElementField(type='jn')
class ParameterPinTan(DataElementGroup):
min_pin_length = DataElementField(type='num', max_length=2, required=False)
max_pin_length = DataElementField(type='num', max_length=2, required=False)
max_tan_length = DataElementField(type='num', max_length=2, required=False)
user_id_field_text = DataElementField(type='an', max_length=30, required=False)
customer_id_field_text = DataElementField(type='an', max_length=30, required=False)
transaction_tans_required = DataElementGroupField(type=TransactionTanRequired, max_count=999, required=False)
class Language2(RepresentableEnum):
"""Dialogsprache
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
DEFAULT = '0' #: Standard
DE = '1' #: Deutsch, 'de', Subset Deutsch, Codeset 1 (Latin 1)
EN = '2' #: Englisch, 'en', Subset Englisch, Codeset 1 (Latin 1)
FR = '3' #: Französisch, 'fr', Subset Französisch, Codeset 1 (Latin 1)
class SupportedLanguages2(DataElementGroup):
languages = CodeField(enum=Language2, max_length=3, min_count=1, max_count=9)
class SupportedHBCIVersions2(DataElementGroup):
versions = DataElementField(type='code', max_length=3, min_count=1, max_count=9)
class KTZ1(DataElementGroup):
"""Kontoverbindung ZV international, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
is_sepa = DataElementField(type='jn', _d="Kontoverwendung SEPA")
iban = DataElementField(type='an', max_length=34, _d="IBAN")
bic = DataElementField(type='an', max_length=11, _d="BIC")
account_number = DataElementField(type='id', _d="Konto-/Depotnummer")
subaccount_number = DataElementField(type='id', _d="Unterkontomerkmal")
bank_identifier = DataElementGroupField(type=BankIdentifier, _d="Kreditinstitutskennung")
def as_sepa_account(self):
from fints.models import SEPAAccount
if not self.is_sepa:
return None
return SEPAAccount(self.iban, self.bic, self.account_number, self.subaccount_number, self.bank_identifier.bank_code)
@classmethod
def from_sepa_account(cls, acc):
return cls(
is_sepa=True,
iban=acc.iban,
bic=acc.bic,
account_number=acc.accountnumber,
subaccount_number=acc.subaccount,
bank_identifier=BankIdentifier(
country_identifier=BankIdentifier.COUNTRY_ALPHA_TO_NUMERIC[acc.bic[4:6]],
bank_code=acc.blz
)
)
class KTI1(DataElementGroup):
"""Kontoverbindung international, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
iban = DataElementField(type='an', max_length=34, required=False, _d="IBAN")
bic = DataElementField(type='an', max_length=11, required=False, _d="BIC")
account_number = DataElementField(type='id', required=False, _d="Konto-/Depotnummer")
subaccount_number = DataElementField(type='id', required=False, _d="Unterkontomerkmal")
bank_identifier = DataElementGroupField(type=BankIdentifier, required=False, _d="Kreditinstitutskennung")
@classmethod
def from_sepa_account(cls, acc):
return cls(
iban=acc.iban,
bic=acc.bic,
)
class Account2(DataElementGroup):
"""Kontoverbindung, version 2
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account_number = DataElementField(type='id', _d="Konto-/Depotnummer")
subaccount_number = DataElementField(type='id', _d="Unterkontomerkmal")
country_identifier = DataElementField(type='ctr', _d="Länderkennzeichen")
bank_code = DataElementField(type='an', max_length=30, _d="Kreditinstitutscode")
@classmethod
def from_sepa_account(cls, acc):
return cls(
account_number=acc.accountnumber,
subaccount_number=acc.subaccount,
country_identifier=BankIdentifier.COUNTRY_ALPHA_TO_NUMERIC[acc.bic[4:6]],
bank_code=acc.blz,
)
class Account3(DataElementGroup):
"""Kontoverbindung, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account_number = DataElementField(type='id', _d="Konto-/Depotnummer")
subaccount_number = DataElementField(type='id', _d="Unterkontomerkmal")
bank_identifier = DataElementGroupField(type=BankIdentifier, _d="Kreditinstitutskennung")
@classmethod
def from_sepa_account(cls, acc):
return cls(
account_number=acc.accountnumber,
subaccount_number=acc.subaccount,
bank_identifier=BankIdentifier(
country_identifier=BankIdentifier.COUNTRY_ALPHA_TO_NUMERIC[acc.bic[4:6]],
bank_code=acc.blz
)
)
class SecurityRole(RepresentableEnum):
"""Rolle des Sicherheitslieferanten, kodiert, version 2
Kodierte Information über das Verhältnis desjenigen, der bezüglich der zu si-chernden Nachricht die Sicherheit gewährleistet.
Die Wahl ist von der bankfachlichen Auslegung der Signatur, respektive vom vertraglichen Zustand zwischen Kunde und Kreditinstitut abhängig.
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
ISS = '1' #: Erfasser, Erstsignatur
CON = '3' #: Unterstützer, Zweitsignatur
WIT = '4' #: Zeuge/Übermittler, nicht Erfasser
class CompressionFunction(RepresentableEnum):
"""Komprimierungsfunktion, version 2
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
NULL = '0' #: Keine Kompression
LZW = '1' #: Lempel, Ziv, Welch
COM = '2' #: Optimized LZW
LZSS = '3' #: Lempel, Ziv
LZHuf = '4' #: LZ + Huffman Coding
ZIP = '5' #: PKZIP
GZIP = '6' #: deflate (http://www.gzip.org/zlib)
BZIP2 = '7' #: bzip2 (http://sourceware.cygnus.com/bzip2/)
ZZZ = '999' #: Gegenseitig vereinbart
class SecurityApplicationArea(RepresentableEnum):
"""Bereich der Sicherheitsapplikation, kodiert, version 2
Informationen darüber, welche Daten vom kryptographischen Prozess verarbeitet werden.
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
SHM = '1' #: Signaturkopf und HBCI-Nutzdaten
SHT = '2' #: Von Signaturkopf bis Signaturabschluss
class SecurityClass(RepresentableEnum):
"""Sicherheitsklasse, version 1
Die Sicherheitsklasse gibt für jede Signatur den erforderlichen Sicherheitsdienst an.
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
NONE = 0 #: Kein Sicherheitsdienst erforderlich
AUTH = 1 #: Sicherheitsdienst 'Authentikation'
AUTH_ADV = 2 #: Sicherheitsdienst 'Authentikation' mit fortgeschrittener elektronischer Signatur, optionaler Zertifikatsprüfung
NON_REPUD = 3 #: Sicherheitsdienst 'Non-Repudiation' mit fortgeschrittener elektronischer Signatur, optionaler Zertifikatsprüfung
NON_REPUD_QUAL = 4 #: Sicherheitsdienst 'Non-Repudiation' mit fortgeschrittener bzw. qualifizierter elektronischer Signatur, zwingende Zertifikatsprüfung
class UPDUsage(RepresentableEnum):
"""UPD-Verwendung, version 2
Kennzeichen dafür, wie diejenigen Geschäftsvorfälle zu interpretieren sind, die bei der Beschreibung der Kontoinformationen nicht unter den erlaubten Geschäftsvorfällen aufgeführt sind.
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
UPD_CONCLUSIVE = '0' #: Die nicht aufgeführten Geschäftsvorfälle sind gesperrt
UPD_INCONCLUSIVE = '1' #: Bei nicht aufgeführten Geschäftsvorfällen ist keine Aussage möglich, ob diese erlaubt oder gesperrt sind
class SystemIDStatus(RepresentableEnum):
"""Kundensystem-Status, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
ID_UNNECESSARY = '0' #: Kundensystem-ID wird nicht benötigt
ID_NECESSARY = '1' #: Kundensystem-ID wird benötigt
class SynchronizationMode(RepresentableEnum):
"""Synchronisierungsmodus, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
NEW_SYSTEM_ID = '0' #: Neue Kundensystem-ID zurückmelden
LAST_MESSAGE = '1' #: Letzte verarbeitete Nachrichtennummer zurückmelden
SIGNATURE_ID = '2' #: Signatur-ID zurückmelden
class CreditDebit2(RepresentableEnum):
"""Soll-Haben-Kennzeichen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
CREDIT = 'C' #: Haben
DEBIT = 'D' #: Soll
class Balance1(DataElementGroup):
"""Saldo, version 1
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
credit_debit = CodeField(enum=CreditDebit2, length=1, _d="Soll-Haben-Kennzeichen")
amount = DataElementField(type='wrt', _d="Wert")
currency = DataElementField(type='cur', _d="Währung")
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', required=False, _d="Uhrzeit")
def as_mt940_Balance(self):
from mt940.models import Balance
return Balance(
self.credit_debit.value,
"{:.12f}".format(self.amount).rstrip('0'),
self.date,
currency=self.currency
)
class Balance2(DataElementGroup):
"""Saldo, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
credit_debit = CodeField(enum=CreditDebit2, length=1, _d="Soll-Haben-Kennzeichen")
amount = DataElementGroupField(type=Amount1, _d="Betrag")
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', required=False, _d="Uhrzeit")
def as_mt940_Balance(self):
from mt940.models import Balance
return Balance(
self.credit_debit.value,
"{:.12f}".format(self.amount.amount).rstrip('0'),
self.date,
currency=self.amount.currency
)
class Timestamp1(DataElementGroup):
"""Zeitstempel
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', required=False, _d="Uhrzeit")
class TANMediaType2(RepresentableEnum):
"""TAN-Medium-Art
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
ALL = '0' #: Alle
ACTIVE = '1' #: Aktiv
AVAILABLE = '2' #: Verfügbar
class TANMediaClass3(RepresentableEnum):
"""TAN-Medium-Klasse, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
ALL = 'A' #: Alle Medien
LIST = 'L' #: Liste
GENERATOR = 'G' #: TAN-Generator
MOBILE = 'M' #: Mobiltelefon mit mobileTAN
SECODER = 'S' #: Secoder
class TANMediaClass4(RepresentableEnum):
"""TAN-Medium-Klasse, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
ALL = 'A' #: Alle Medien
LIST = 'L' #: Liste
GENERATOR = 'G' #: TAN-Generator
MOBILE = 'M' #: Mobiltelefon mit mobileTAN
SECODER = 'S' #: Secoder
BILATERAL = 'B' #: Bilateral vereinbart
class TANMediumStatus(RepresentableEnum):
"""Status
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
ACTIVE = '1' #: Aktiv
AVAILABLE = '2' #: Verfügbar
ACTIVE_SUCCESSOR = '3' #: Aktiv Folgekarte
AVAILABLE_SUCCESSOR = '4' #: Verfügbar Folgekarte
class TANMedia4(DataElementGroup):
"""TAN-Medium-Liste, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_medium_class = CodeField(enum=TANMediaClass3, _d="TAN-Medium-Klasse")
status = CodeField(enum=TANMediumStatus, _d="Status")
card_number = DataElementField(type='id', required=False, _d="Kartennummer")
card_sequence = DataElementField(type='id', required=False, _d="Kartenfolgenummer")
card_type = DataElementField(type='num', required=False, _d="Kartenart")
account = DataElementGroupField(type=Account3, required=False, _d="Kontonummer Auftraggeber")
valid_from = DataElementField(type='dat', required=False, _d="Gültig ab")
valid_until = DataElementField(type='dat', required=False, _d="Gültig bis")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
mobile_number_masked = DataElementField(type='an', max_length=35, required=False, _d="Mobiltelefonnummer, verschleiert")
mobile_number = DataElementField(type='an', max_length=35, required=False, _d="Mobiltelefonnummer")
sms_charge_account = DataElementGroupField(type=KTI1, required=False, _d="SMS-Abbuchungskonto")
number_free_tans = DataElementField(type='num', max_length=3, required=False, _d="Anzahl freie TANs")
last_use = DataElementField(type='dat', required=False, _d="Letzte Benutzung")
active_since = DataElementField(type='dat', required=False, _d="Freigeschaltet am")
class TANMedia5(DataElementGroup):
"""TAN-Medium-Liste, version 5
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_medium_class = CodeField(enum=TANMediaClass4, _d="TAN-Medium-Klasse")
status = CodeField(enum=TANMediumStatus, _d="Status")
security_function = DataElementField(type='num', required=False, _d="Sicherheitsfunktion, kodiert")
card_number = DataElementField(type='id', required=False, _d="Kartennummer")
card_sequence = DataElementField(type='id', required=False, _d="Kartenfolgenummer")
card_type = DataElementField(type='num', required=False, _d="Kartenart")
account = DataElementGroupField(type=Account3, required=False, _d="Kontonummer Auftraggeber")
valid_from = DataElementField(type='dat', required=False, _d="Gültig ab")
valid_until = DataElementField(type='dat', required=False, _d="Gültig bis")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
mobile_number_masked = DataElementField(type='an', max_length=35, required=False, _d="Mobiltelefonnummer, verschleiert")
mobile_number = DataElementField(type='an', max_length=35, required=False, _d="Mobiltelefonnummer")
sms_charge_account = DataElementGroupField(type=KTI1, required=False, _d="SMS-Abbuchungskonto")
number_free_tans = DataElementField(type='num', max_length=3, required=False, _d="Anzahl freie TANs")
last_use = DataElementField(type='dat', required=False, _d="Letzte Benutzung")
active_since = DataElementField(type='dat', required=False, _d="Freigeschaltet am")
class TANUsageOption(RepresentableEnum):
"""TAN-Einsatzoption
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
ALL_ACTIVE = '0' #: Kunde kann alle "aktiven" Medien parallel nutzen
EXACTLY_ONE = '1' #: Kunde kann genau ein Medium zu einer Zeit nutzen
MOBILE_AND_GENERATOR = '2' #: Kunde kann ein Mobiltelefon und einen TAN-Generator parallel nutzen
class ParameterChallengeClass(DataElementGroup):
"""Parameter Challenge-Klasse
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
parameters = DataElementField(type='an', max_length=999, count=9, required=False)
class ResponseHHDUC(DataElementGroup):
"""Antwort HHD_UC
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
atc = DataElementField(type='an', max_length=5, _d="ATC")
ac = DataElementField(type='bin', max_length=256, _d="Application Cryptogram AC")
ef_id_data = DataElementField(type='bin', max_length=256, _d="EF_ID Data")
cvr = DataElementField(type='bin', max_length=256, _d="CVR")
version_info_chiptan = DataElementField(type='bin', max_length=256, _d="Versionsinfo der chipTAN-Applikation")
class ChallengeValidUntil(DataElementGroup):
"""Gültigkeitsdatum und -uhrzeit für Challenge
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', _d="Uhrzeit")
class BatchTransferParameter1(DataElementGroup):
"""Parameter SEPA-Sammelüberweisung, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
max_transfer_count = DataElementField(type='num', max_length=7, _d="Maximale Anzahl CreditTransferTransactionInformation")
sum_amount_required = DataElementField(type='jn', _d="Summenfeld benötigt")
single_booking_allowed = DataElementField(type='jn', _d="Einzelbuchung erlaubt")
class ServiceType2(RepresentableEnum):
T_ONLINE = 1 #: T-Online
TCP_IP = 2 #: TCP/IP (Protokollstack SLIP/PPP)
HTTPS = 3 #: https
class CommunicationParameter2(DataElementGroup):
"""Kommunikationsparameter, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
service_type = IntCodeField(enum=ServiceType2, max_length=2, _d="Kommunikationsdienst")
address = DataElementField(type='an', max_length=512, _d="Kommunikationsadresse")
address_adjunct = DataElementField(type='an', max_length=512, required=False, _d="Kommunikationsadresszusatz")
filter_function = DataElementField(type='an', length=3, required=False, _d="Filterfunktion")
filter_function_version = DataElementField(type='num', max_length=3, required=False, _d="Version der Filterfunktion")
class ScheduledDebitParameter1(DataElementGroup):
"""Parameter terminierte SEPA-Einzellastschrift einreichen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
min_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FNAL/RCUR")
max_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FNAL/RCUR")
min_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FRST/OOFF")
max_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FRST/OOFF")
class ScheduledDebitParameter2(DataElementGroup):
"""Parameter terminierte SEPA-Einzellastschrift einreichen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
min_advance_notice = DataElementField(type='an', max_length=99, _d="Minimale Vorlaufzeit SEPA-Lastschrift")
max_advance_notice = DataElementField(type='an', max_length=99, _d="Maximale Vorlaufzeit SEPA-Lastschrift")
allowed_purpose_codes = DataElementField(type='an', max_length=4096, required=False, _d="Zulässige purpose codes")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=9, required=False, _d="Unterstützte SEPA-Datenformate")
class ScheduledBatchDebitParameter1(DataElementGroup):
"""Parameter terminierte SEPA-Sammellastschrift einreichen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
min_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FNAL/RCUR")
max_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FNAL/RCUR")
min_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FRST/OOFF")
max_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FRST/OOFF")
max_debit_count = DataElementField(type='num', max_length=7, _d="Maximale Anzahl DirectDebitTransfer TransactionInformation")
sum_amount_required = DataElementField(type='jn', _d="Summenfeld benötigt")
single_booking_allowed = DataElementField(type='jn', _d="Einzelbuchung erlaubt")
class ScheduledBatchDebitParameter2(DataElementGroup):
"""Parameter terminierte SEPA-Sammellastschrift einreichen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
min_advance_notice = DataElementField(type='an', max_length=99, _d="Minimale Vorlaufzeit SEPA-Lastschrift")
max_advance_notice = DataElementField(type='an', max_length=99, _d="Maximale Vorlaufzeit SEPA-Lastschrift")
max_debit_count = DataElementField(type='num', max_length=7, _d="Maximale Anzahl DirectDebitTransfer TransactionInformation")
sum_amount_required = DataElementField(type='jn', _d="Summenfeld benötigt")
single_booking_allowed = DataElementField(type='jn', _d="Einzelbuchung erlaubt")
allowed_purpose_codes = DataElementField(type='an', max_length=4096, required=False, _d="Zulässige purpose codes")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=9, required=False, _d="Unterstützte SEPA-Datenformate")
class ScheduledCOR1DebitParameter1(DataElementGroup):
"""Parameter terminierte SEPA-COR1-Einzellastschrift, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
min_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FNAL/RCUR")
max_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FNAL/RCUR")
min_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FRST/OOFF")
max_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FRST/OOFF")
allowed_purpose_codes = DataElementField(type='an', max_length=4096, required=False, _d="Zulässige purpose codes")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=9, required=False, _d="Unterstützte SEPA-Datenformate")
class ScheduledCOR1BatchDebitParameter1(DataElementGroup):
"""Parameter terminierte SEPA-COR1-Sammellastschrift, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
max_debit_count = DataElementField(type='num', max_length=7, _d="Maximale Anzahl DirectDebitTransfer TransactionInformation")
sum_amount_required = DataElementField(type='jn', _d="Summenfeld benötigt")
single_booking_allowed = DataElementField(type='jn', _d="Einzelbuchung erlaubt")
min_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FNAL/RCUR")
max_advance_notice_FNAL_RCUR = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FNAL/RCUR")
min_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Minimale Vorlaufzeit FRST/OOFF")
max_advance_notice_FRST_OOFF = DataElementField(type='num', max_length=4, _d="Maximale Vorlaufzeit FRST/OOFF")
allowed_purpose_codes = DataElementField(type='an', max_length=4096, required=False, _d="Zulässige purpose codes")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=9, required=False, _d="Unterstützte SEPA-Datenformate")
class SupportedSEPAPainMessages1(DataElementGroup):
"""Unterstützte SEPA pain messages, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
sepa_descriptors = DataElementField(type='an', max_length=256, max_count=99, _d="SEPA Descriptor")
class QueryScheduledDebitParameter1(DataElementGroup):
"""Parameter Bestand terminierter SEPA-Einzellastschriften, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
date_range_allowed = DataElementField(type='jn', _d="Zeitraum möglich")
max_number_responses_allowed = DataElementField(type='jn', _d="Eingabe Anzahl Einträge erlaubt")
class QueryScheduledDebitParameter2(DataElementGroup):
"""Parameter Bestand terminierter SEPA-Einzellastschriften, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
max_number_responses_allowed = DataElementField(type='jn', _d="Eingabe Anzahl Einträge erlaubt")
date_range_allowed = DataElementField(type='jn', _d="Zeitraum möglich")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=9, required=False, _d="Unterstützte SEPA-Datenformate")
class QueryScheduledBatchDebitParameter1(DataElementGroup):
"""Parameter Bestand terminierter SEPA-Sammellastschriften, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
max_number_responses_allowed = DataElementField(type='jn', _d="Eingabe Anzahl Einträge erlaubt")
date_range_allowed = DataElementField(type='jn', _d="Zeitraum möglich")
class QueryCreditCardStatements2(DataElementGroup):
"""Parameter Kreditkartenumsätze anfordern, version 2
Source: reverse engineered"""
cutoff_days = DataElementField(type='num', max_length=4, _d="Maximale Vorhaltezeit der Umsätze")
max_number_responses_allowed = DataElementField(type='jn', _d="Eingabe Anzahl Einträge erlaubt")
date_range_allowed = DataElementField(type='jn', _d="Zeitraum möglich")
class SEPACCode1(RepresentableEnum):
REVERSAL = '1' #: Reversal
REVOCATION = '2' #: Revocation
DELETION = '3' #: Delete
class StatusSEPATask1(RepresentableEnum):
PENDING = '1' #: In Terminierung
DECLINED = '2' #: Abgelehnt von erster Inkassostelle
IN_PROGRESS = '3' #: in Bearbeitung
PROCESSED = '4' #: Creditoren-seitig verarbeitet, Buchung veranlasst
REVOKED = '5' #: R-Transaktion wurde veranlasst
class GetSEPAAccountParameter1(DataElementGroup):
"""Parameter SEPA-Kontoverbindung anfordern, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
single_account_query_allowed = DataElementField(type='jn', _d="Einzelkontenabruf erlaubt")
national_account_allowed = DataElementField(type='jn', _d="Nationale Kontoverbindung erlaubt")
structured_purpose_allowed = DataElementField(type='jn', _d="Strukturierter Verwendungszweck erlaubt")
supported_sepa_formats = DataElementField(type='an', max_length=256, max_count=99, required=False, _d="Unterstützte SEPA-Datenformate")
class SupportedMessageTypes(DataElementGroup):
"""Unterstützte camt-Messages
Source: Messages - Multibankfähige Geschäftsvorfälle (SEPA) - C.2.3.1.1.1
"""
expected_type = AlphanumericField(max_length=256, max_count=99, required=True, _d='Unterstützte camt-messages')
class BookedCamtStatements1(DataElementGroup):
"""Gebuchte camt-Umsätze
Source: Messages - Multibankfähige Geschäftsvorfälle (SEPA)"""
camt_statements = DataElementField(type='bin', min_count=1, required=True, _d="camt-Umsätze gebucht")
python-fints-4.0.0/fints/hhd/ 0000775 0000000 0000000 00000000000 14421014606 0016041 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/fints/hhd/__init__.py 0000664 0000000 0000000 00000000000 14421014606 0020140 0 ustar 00root root 0000000 0000000 python-fints-4.0.0/fints/hhd/flicker.py 0000664 0000000 0000000 00000020230 14421014606 0020027 0 ustar 00root root 0000000 0000000 # Inspired by:
# https://github.com/willuhn/hbci4java/blob/master/src/org/kapott/hbci/manager/FlickerCode.java
# https://6xq.net/flickercodes/
# https://wiki.ccc-ffm.de/projekte:tangenerator:start#flickercode_uebertragung
import math
import re
import time
HHD_VERSION_13 = 13
HHD_VERSION_14 = 14
LC_LENGTH_HHD14 = 3
LC_LENGTH_HHD13 = 2
LDE_LENGTH_DEFAULT = 2
LDE_LENGTH_SPARDA = 3
BIT_ENCODING = 6 # Position of encoding bit
BIT_CONTROLBYTE = 7 # Position of bit that tells if there are a control byte
ENCODING_ASC = 1
ENCODING_BCD = 2
def parse(code):
code = clean(code)
try:
return FlickerCode(code, HHD_VERSION_14)
except:
try:
return FlickerCode(code, HHD_VERSION_14, LDE_LENGTH_SPARDA)
except:
return FlickerCode(code, HHD_VERSION_13)
def clean(code):
if code.startswith('@'):
code = code[res.challenge_hhd_uc.index('@', 2) + 1:]
code = code.replace(" ", "").strip()
if "CHLGUC" in code and "CHLGTEXT" in code:
# Sometimes, HHD 1.3 codes are not transferred in the challenge field but in the free text,
# contained in CHLGUCXXXXCHLGTEXT
code = "0" + code[code.index("CHLGUC") + 11:code.index("CHLGTEXT")]
return code
def bit_sum(num, bits):
s = 0
for i in range(bits):
s += num & (1 << i)
return s
def digitsum(n):
q = 0
while n != 0:
q += n % 10
n = math.floor(n / 10)
return q
def h(num, l):
return hex(num).upper()[2:].zfill(l)
def asciicode(s):
return ''.join(h(ord(c), 2) for c in s)
def swap_bytes(s):
b = ""
for i in range(0, len(s), 2):
b += s[i + 1]
b += s[i]
return b
class FlickerCode:
def __init__(self, code, version, lde_len=LDE_LENGTH_DEFAULT):
self.version = version
self.lc = None
self.startcode = Startcode()
self.de1 = DE(lde_len)
self.de2 = DE(lde_len)
self.de3 = DE(lde_len)
self.rest = None
self.parse(code)
def parse(self, code):
length = LC_LENGTH_HHD14 if self.version == HHD_VERSION_14 else LC_LENGTH_HHD13
self.lc = int(code[0:length])
if len(code) < length+self.lc:
raise ValueError("lc too large: {} + {} > {}".format(self.lc, length, len(code)))
code = code[length:]
code = self.startcode.parse(code)
self.version = self.startcode.version
code = self.de1.parse(code, self.version)
code = self.de2.parse(code, self.version)
code = self.de3.parse(code, self.version)
self.rest = code or None
def render(self):
s = self.create_payload()
luhn = self.create_luhn_checksum()
xor = self.create_xor_checksum(s)
return s + luhn + xor
def create_payload(self):
s = str(self.startcode.render_length())
for b in self.startcode.control_bytes:
s += h(b, 2)
s += self.startcode.render_data()
for de in (self.de1, self.de2, self.de3):
s += de.render_length()
s += de.render_data()
l = (len(s) + 2) // 2 # data + checksum / chars per byte
lc = h(l, 2)
return lc + s
def create_xor_checksum(self, payload):
xorsum = 0
for c in payload:
xorsum ^= int(c, 16)
return h(xorsum, 1)
def create_luhn_checksum(self):
s = ""
for b in self.startcode.control_bytes:
s += h(b, 2)
s += self.startcode.render_data()
if self.de1.data is not None:
s += self.de1.render_data()
if self.de2.data is not None:
s += self.de2.render_data()
if self.de3.data is not None:
s += self.de3.render_data()
luhnsum = 0
for i in range(0, len(s), 2):
luhnsum += 1 * int(s[i], 16) + digitsum(2 * int(s[i + 1], 16))
m = luhnsum % 10
if m == 0:
return "0"
r = 10 - m
ss = luhnsum + r
luhn = ss - luhnsum
return h(luhn, 1)
class DE:
def __init__(self, lde_len):
self.length = 0
self.lde = 0
self.lde_length = lde_len
self.encoding = None
self.data = None
def parse(self, data, version):
self.version = version
if not data:
return data
self.lde = int(data[0:self.lde_length])
data = data[self.lde_length:]
self.length = bit_sum(self.lde, 5)
self.data = data[0:self.length]
return data[self.length:]
def set_encoding(self):
if self.data is None:
self.encoding = ENCODING_BCD
elif self.encoding is not None:
pass
elif re.match("^[0-9]{1,}$", self.data):
# BCD only if the value is fully numeric, no IBAN etc.
self.encoding = ENCODING_BCD
else:
self.encoding = ENCODING_ASC
def render_length(self):
self.set_encoding()
if self.data is None:
return ""
l = len(self.render_data()) // 2
if self.encoding == ENCODING_BCD:
return h(l, 2)
if self.version == HHD_VERSION_14:
l = l + (1 << BIT_ENCODING)
return h(l, 2)
return "1" + h(l, 1)
def render_data(self):
self.set_encoding()
if self.data is None:
return ""
if self.encoding == ENCODING_ASC:
return asciicode(self.data)
if len(self.data) % 2 == 1:
return self.data + "F"
return self.data
class Startcode(DE):
def __init__(self):
super().__init__(LDE_LENGTH_DEFAULT)
self.control_bytes = []
def parse(self, data):
self.lde = int(data[:2], 16)
data = data[2:]
self.length = bit_sum(self.lde, 5)
self.version = HHD_VERSION_13
if self.lde & (1 << BIT_CONTROLBYTE) != 0:
self.version = HHD_VERSION_14
for i in range(10):
cbyte = int(data[:2], 16)
self.control_bytes.append(cbyte)
data = data[2:]
if cbyte & (1 << BIT_CONTROLBYTE) == 0:
break
self.data = data[:self.length]
return data[self.length:]
def render_length(self):
s = super().render_length()
if self.version == HHD_VERSION_13 or not self.control_bytes:
return s
l = int(s, 16) + (1 << BIT_CONTROLBYTE)
return h(l, 2)
def code_to_bitstream(code):
"""Convert a flicker code into a bitstream in strings."""
# Inspired by Andreas Schiermeier
# https://git.ccc-ffm.de/?p=smartkram.git;a=blob_plain;f=chiptan/flicker/flicker.sh;h
# =7066293b4e790c2c4c1f6cbdab703ed9976ffe1f;hb=refs/heads/master
code = parse(code).render()
data = swap_bytes(code)
stream = ['10000', '00000', '11111', '01111', '11111', '01111', '11111']
for c in data:
v = int(c, 16)
stream.append('1' + str(v & 1) + str((v & 2) >> 1) + str((v & 4) >> 2) + str((v & 8) >> 3))
stream.append('0' + str(v & 1) + str((v & 2) >> 1) + str((v & 4) >> 2) + str((v & 8) >> 3))
return stream
def terminal_flicker_unix(code, field_width=3, space_width=3, height=1, clear=False, wait=0.05):
"""
Re-encodes a flicker code and prints it on a unix terminal.
:param code: Challenge value
:param field_width: Width of fields in characters (default: 3).
:param space_width: Width of spaces in characters (default: 3).
:param height: Height of fields in characters (default: 1).
:param clear: Clear terminal after every line (default: ``False``).
:param wait: Waiting interval between lines (default: 0.05).
"""
stream = code_to_bitstream(code)
high = '\033[48;05;15m'
low = '\033[48;05;0m'
std = '\033[0m'
while True:
for frame in stream:
if clear:
print('\033c', end='')
for i in range(height):
for c in frame:
print(low + ' ' * space_width, end='')
if c == '1':
print(high + ' ' * field_width, end='')
else:
print(low+ ' ' * field_width, end='')
print(low + ' ' * space_width + std)
time.sleep(wait)
python-fints-4.0.0/fints/message.py 0000664 0000000 0000000 00000002747 14421014606 0017306 0 ustar 00root root 0000000 0000000 from enum import Enum
from .formals import SegmentSequence
from .segments.base import FinTS3Segment
from .segments.dialog import HIRMS2
class MessageDirection(Enum):
FROM_CUSTOMER = 1
FROM_INSTITUTE = 2
class FinTSMessage(SegmentSequence):
DIRECTION = None
# Auto-Numbering, dialog relation, security base
def __init__(self, dialog=None, *args, **kwargs):
self.dialog = dialog
self.next_segment_number = 1
super().__init__(*args, **kwargs)
def __iadd__(self, segment: FinTS3Segment):
if not isinstance(segment, FinTS3Segment):
raise TypeError("Can only append FinTS3Segment instances, not {!r}".format(segment))
segment.header.number = self.next_segment_number
self.next_segment_number += 1
self.segments.append(segment)
return self
def response_segments(self, ref, *args, **kwargs):
for segment in self.find_segments(*args, **kwargs):
if segment.header.reference == ref.header.number:
yield segment
def responses(self, ref, code=None):
for segment in self.response_segments(ref, HIRMS2):
for response in segment.responses:
if code is None or response.code == code:
yield response
class FinTSCustomerMessage(FinTSMessage):
DIRECTION = MessageDirection.FROM_CUSTOMER
# Identification, authentication
class FinTSInstituteMessage(FinTSMessage):
DIRECTION = MessageDirection.FROM_INSTITUTE
python-fints-4.0.0/fints/models.py 0000664 0000000 0000000 00000000501 14421014606 0017127 0 ustar 00root root 0000000 0000000 from collections import namedtuple
SEPAAccount = namedtuple('SEPAAccount', 'iban bic accountnumber subaccount blz')
Saldo = namedtuple('Saldo', 'account date value currency')
Holding = namedtuple('Holding',
'ISIN name market_value value_symbol valuation_date pieces total_value acquisitionprice')
python-fints-4.0.0/fints/parser.py 0000664 0000000 0000000 00000037543 14421014606 0017160 0 ustar 00root root 0000000 0000000 import re
import warnings
from collections.abc import Iterable
from enum import Enum
from .formals import (
Container, DataElementGroupField, SegmentSequence, ValueList,
)
# Ensure that all segment types are loaded (otherwise the subclass find won't see them)
from .segments import ( # noqa
accounts, auth, bank, base, debit, depot, dialog,
journal, message, saldo, statement, transfer,
)
from .segments.base import FinTS3Segment
#
# FinTS 3.0 structure:
# Message := ( Segment "'" )+
# Segment := ( DEG "+" )+
# DEG := ( ( DE | DEG ) ":")+
#
# First DEG in segment is segment header
# Many DEG (Data Element Group) on Segment level are just DE (Data Element),
# Recursion DEG -> DEG must be limited, since no other separator characters
# are available. In general, a second order DEG must have fixed length but
# may have a variable repeat count if it is at the end of the segment
#
# Parsing:
# The message after detokenization/exploding is up to three levels deep:
# 1. level: Sequence of Segments
# 2. level: Sequence of Data Elements or Data Element Groups
# 3. level: Flat sequence of possibly nested Data Element or Data Element Groups
#
# On level 2 each item can be either a single item (a single Data Element), or
# a sequence. A sequence on level 2 can be either a repeated Data Element, or
# a flat representation of a Data Element Group or repeated Data Element Group.
# An item on level 3 is always a Data Element, but which Data Element it is depends
# on which fields have been consumed in the sequence before it.
#: Operate the parser in "robust mode". In this mode, errors during segment parsing
#: will be turned into a FinTSParserWarning and a generic FinTS3Segment (not a subclass)
#: will be constructed. This allows for all syntactically correct FinTS messages to be
#: consumed, even in the presence of errors in this library.
robust_mode = True
class FinTSParserWarning(UserWarning):
pass
class FinTSParserError(ValueError):
pass
TOKEN_RE = re.compile(rb"""
^(?: (?: \? (?P.) )
| (?P[^?:+@']+)
| (?P[+:'])
| (?: @ (?P[0-9]+) @ )
)""", re.X | re.S)
class Token(Enum):
EOF = 'eof'
CHAR = 'char'
BINARY = 'bin'
PLUS = '+'
COLON = ':'
APOSTROPHE = "'"
class ParserState:
def __init__(self, data: bytes, start=0, end=None, encoding='iso-8859-1'):
self._token = None
self._value = None
self._encoding = encoding
self._tokenizer = iter(self._tokenize(data, start, end or len(data), encoding))
def peek(self):
if not self._token:
self._token, self._value = next(self._tokenizer)
return self._token
def consume(self, token=None):
self.peek()
if token and token != self._token:
raise ValueError
self._token = None
return self._value
@staticmethod
def _tokenize(data, start, end, encoding):
pos = start
unclaimed = []
last_was = None
while pos < end:
match = TOKEN_RE.match(data[pos:end])
if match:
pos += match.end()
d = match.groupdict()
if d['ECHAR'] is not None:
unclaimed.append(d['ECHAR'])
elif d['CHAR'] is not None:
unclaimed.append(d['CHAR'])
else:
if unclaimed:
if last_was in (Token.BINARY, Token.CHAR):
raise ValueError
yield Token.CHAR, b''.join(unclaimed).decode(encoding)
unclaimed.clear()
last_was = Token.CHAR
if d['TOK'] is not None:
token = Token(d['TOK'].decode('us-ascii'))
yield token, d['TOK']
last_was = token
elif d['BINLEN'] is not None:
blen = int(d['BINLEN'].decode('us-ascii'), 10)
if last_was in (Token.BINARY, Token.CHAR):
raise ValueError
yield Token.BINARY, data[pos:pos+blen]
pos += blen
last_was = Token.BINARY
else:
raise ValueError
else:
raise ValueError
if unclaimed:
if last_was in (Token.BINARY, Token.CHAR):
raise ValueError
yield Token.CHAR, b''.join(unclaimed).decode(encoding)
unclaimed.clear()
last_was = Token.CHAR
yield Token.EOF, b''
class FinTS3Parser:
"""Parser for FinTS/HBCI 3.0 messages
"""
def parse_message(self, data: bytes) -> SegmentSequence:
"""Takes a FinTS 3.0 message as byte array, and returns a parsed segment sequence"""
if isinstance(data, bytes):
data = self.explode_segments(data)
message = SegmentSequence()
for segment in data:
seg = self.parse_segment(segment)
message.segments.append(seg)
return message
def parse_segment(self, segment):
clazz = FinTS3Segment.find_subclass(segment)
try:
return self._parse_segment_as_class(clazz, segment)
except FinTSParserError as e:
if robust_mode:
warnings.warn("Ignoring parser error and returning generic object: {}. Turn off robust_mode to see Exception.".format(str(e)), FinTSParserWarning)
return self._parse_segment_as_class(FinTS3Segment, segment)
else:
raise
def _parse_segment_as_class(self, clazz, segment):
seg = clazz()
data = iter(segment)
for name, field in seg._fields.items():
repeat = field.count != 1
constructed = isinstance(field, DataElementGroupField)
if not repeat:
try:
val = next(data)
except StopIteration:
if field.required:
raise FinTSParserError("Required field {}.{} was not present".format(seg.__class__.__name__, name))
break
try:
if not constructed:
setattr(seg, name, val)
else:
deg = self.parse_deg_noniter(field.type, val, field.required)
setattr(seg, name, deg)
except ValueError as e:
raise FinTSParserError("Wrong input when setting {}.{}".format(seg.__class__.__name__, name)) from e
else:
i = 0
while True:
try:
val = next(data)
except StopIteration:
break
try:
if not constructed:
getattr(seg, name)[i] = val
else:
deg = self.parse_deg_noniter(field.type, val, field.required)
getattr(seg, name)[i] = deg
except ValueError as e:
raise FinTSParserError("Wrong input when setting {}.{}".format(seg.__class__.__name__, name)) from e
i = i + 1
if field.count is not None and i >= field.count:
break
if field.max_count is not None and i >= field.max_count:
break
seg._additional_data = list(data)
return seg
def parse_deg_noniter(self, clazz, data, required):
if not isinstance(data, Iterable) or isinstance(data, (str, bytes)):
data = [data]
data_i = iter(data)
retval = self.parse_deg(clazz, data_i, required)
remainder = list(data_i)
if remainder:
raise FinTSParserError("Unparsed data {!r} after parsing {!r}".format(remainder, clazz))
return retval
def parse_deg(self, clazz, data_i, required=True):
retval = clazz()
for number, (name, field) in enumerate(retval._fields.items()):
repeat = field.count != 1
constructed = isinstance(field, DataElementGroupField)
is_last = number == len(retval._fields)-1
if not repeat:
try:
if not constructed:
try:
setattr(retval, name, next(data_i))
except StopIteration:
if required and field.required:
raise FinTSParserError("Required field {}.{} was not present".format(retval.__class__.__name__, name))
break
else:
deg = self.parse_deg(field.type, data_i, required and field.required)
setattr(retval, name, deg)
except ValueError as e:
raise FinTSParserError("Wrong input when setting {}.{}".format(retval.__class__.__name__, name)) from e
else:
i = 0
while True:
try:
if not constructed:
try:
getattr(retval, name)[i] = next(data_i)
except StopIteration:
break
else:
require_last = (field.max_count is None) if is_last else True
deg = self.parse_deg(field.type, data_i, require_last and required and field.required)
getattr(retval, name)[i] = deg
except ValueError as e:
raise FinTSParserError("Wrong input when setting {}.{}".format(retval.__class__.__name__, name)) from e
i = i + 1
if field.count is not None and i >= field.count:
break
if field.max_count is not None and i >= field.max_count:
break
return retval
@staticmethod
def explode_segments(data: bytes, start=0, end=None):
segments = []
parser = ParserState(data, start, end)
while parser.peek() != Token.EOF:
segment = []
while parser.peek() not in (Token.APOSTROPHE, Token.EOF):
data = None
deg = []
while parser.peek() in (Token.BINARY, Token.CHAR, Token.COLON):
if parser.peek() in (Token.BINARY, Token.CHAR):
data = parser.consume()
elif parser.peek() == Token.COLON:
deg.append(data)
data = None
parser.consume(Token.COLON)
if data and deg:
deg.append(data)
if deg:
data = deg
segment.append(data)
if parser.peek() == Token.PLUS:
parser.consume(Token.PLUS)
parser.consume(Token.APOSTROPHE)
segments.append(segment)
parser.consume(Token.EOF)
return segments
class FinTS3Serializer:
"""Serializer for FinTS/HBCI 3.0 messages
"""
def serialize_message(self, message: SegmentSequence) -> bytes:
"""Serialize a message (as SegmentSequence, list of FinTS3Segment, or FinTS3Segment) into a byte array"""
if isinstance(message, FinTS3Segment):
message = SegmentSequence([message])
if isinstance(message, (list, tuple, Iterable)):
message = SegmentSequence(list(message))
result = []
for segment in message.segments:
result.append(self.serialize_segment(segment))
return self.implode_segments(result)
def serialize_segment(self, segment):
seg = []
filler = []
for name, field in segment._fields.items():
repeat = field.count != 1
constructed = isinstance(field, DataElementGroupField)
val = getattr(segment, name)
empty = False
if not field.required:
if isinstance(val, Container):
if val.is_unset():
empty = True
elif isinstance(val, ValueList):
if len(val) == 0:
empty = True
elif val is None:
empty = True
if empty:
filler.append(None)
continue
else:
if filler:
seg.extend(filler)
filler.clear()
if not constructed:
if repeat:
seg.extend(field.render(val) for val in getattr(segment, name))
else:
seg.append(field.render(getattr(segment, name)))
else:
if repeat:
for val in getattr(segment, name):
seg.append(self.serialize_deg(val))
else:
seg.append(self.serialize_deg(getattr(segment, name), allow_skip=True))
if segment._additional_data:
seg.extend(segment._additional_data)
return seg
def serialize_deg(self, deg, allow_skip=False):
result = []
filler = []
for name,field in deg._fields.items():
repeat = field.count != 1
constructed = isinstance(field, DataElementGroupField)
val = getattr(deg, name)
empty = False
if field.count == 1 and not field.required:
if isinstance(val, Container):
if val.is_unset():
empty = True
elif isinstance(val, ValueList):
if len(val) == 0:
empty = True
elif val is None:
empty = True
if empty:
if allow_skip:
filler.append(None)
else:
result.append(None)
continue
else:
if filler:
result.extend(filler)
filler.clear()
if not constructed:
if repeat:
result.extend(field.render(val) for val in getattr(deg, name))
else:
result.append(field.render(getattr(deg, name)))
else:
if repeat:
for val in getattr(deg, name):
result.extend(self.serialize_deg(val))
else:
result.extend(self.serialize_deg(getattr(deg, name)))
return result
@staticmethod
def implode_segments(message: list):
level1 = []
for segment in message:
level2 = []
for deg in segment:
if isinstance(deg, (list, tuple)):
highest_index = max(((i+1) for (i, e) in enumerate(deg) if e != b'' and e is not None), default=0)
level2.append(
b":".join(FinTS3Serializer.escape_value(de) for de in deg[:highest_index])
)
else:
level2.append(FinTS3Serializer.escape_value(deg))
level1.append(b"+".join(level2))
return b"'".join(level1) + b"'"
@staticmethod
def escape_value(val):
if isinstance(val, str):
return re.sub(r"([+:'@?])", r"?\1", val).encode('iso-8859-1')
elif isinstance(val, bytes):
return "@{}@".format(len(val)).encode('us-ascii') + val
elif val is None:
return b''
else:
raise TypeError("Can only escape str, bytes and None")
python-fints-4.0.0/fints/security.py 0000664 0000000 0000000 00000014030 14421014606 0017515 0 ustar 00root root 0000000 0000000 import datetime
import random
from fints.exceptions import FinTSError
from .formals import (
AlgorithmParameterIVName, AlgorithmParameterName, CompressionFunction,
DateTimeType, EncryptionAlgorithm, EncryptionAlgorithmCoded,
HashAlgorithm, IdentifiedRole, KeyName, KeyType, OperationMode,
SecurityApplicationArea, SecurityDateTime,
SecurityIdentificationDetails, SecurityMethod, SecurityProfile,
SecurityRole, SignatureAlgorithm, UsageEncryption, UserDefinedSignature,
)
from .message import FinTSMessage
from .segments.message import HNSHA2, HNSHK4, HNVSD1, HNVSK3
from .types import SegmentSequence
class EncryptionMechanism:
def encrypt(self, message: FinTSMessage):
raise NotImplemented()
def decrypt(self, message: FinTSMessage):
raise NotImplemented()
class AuthenticationMechanism:
def sign_prepare(self, message: FinTSMessage):
raise NotImplemented()
def sign_commit(self, message: FinTSMessage):
raise NotImplemented()
def verify(self, message: FinTSMessage):
raise NotImplemented()
class PinTanDummyEncryptionMechanism(EncryptionMechanism):
def __init__(self, security_method_version=1):
super().__init__()
self.security_method_version = security_method_version
def encrypt(self, message: FinTSMessage):
assert message.segments[0].header.type == 'HNHBK'
assert message.segments[-1].header.type == 'HNHBS'
plain_segments = message.segments[1:-1]
del message.segments[1:-1]
_now = datetime.datetime.now()
message.segments.insert(
1,
HNVSK3(
security_profile=SecurityProfile(SecurityMethod.PIN, self.security_method_version),
security_function='998',
security_role=SecurityRole.ISS,
security_identification_details=SecurityIdentificationDetails(
IdentifiedRole.MS,
identifier=message.dialog.client.system_id,
),
security_datetime=SecurityDateTime(
DateTimeType.STS,
_now.date(),
_now.time(),
),
encryption_algorithm=EncryptionAlgorithm(
UsageEncryption.OSY,
OperationMode.CBC,
EncryptionAlgorithmCoded.TWOKEY3DES,
b'\x00'*8,
AlgorithmParameterName.KYE,
AlgorithmParameterIVName.IVC,
),
key_name=KeyName(
message.dialog.client.bank_identifier,
message.dialog.client.user_id,
KeyType.V,
0,
0,
),
compression_function=CompressionFunction.NULL,
)
)
message.segments[1].header.number = 998
message.segments.insert(
2,
HNVSD1(
data=SegmentSequence(segments=plain_segments)
)
)
message.segments[2].header.number = 999
def decrypt(self, message: FinTSMessage):
pass
class PinTanAuthenticationMechanism(AuthenticationMechanism):
def __init__(self, pin):
self.pin = pin
self.pending_signature = None
self.security_function = None
def sign_prepare(self, message: FinTSMessage):
_now = datetime.datetime.now()
rand = random.SystemRandom()
self.pending_signature = HNSHK4(
security_profile=SecurityProfile(SecurityMethod.PIN, 1),
security_function=self.security_function,
security_reference=rand.randint(1000000, 9999999),
security_application_area=SecurityApplicationArea.SHM,
security_role=SecurityRole.ISS,
security_identification_details=SecurityIdentificationDetails(
IdentifiedRole.MS,
identifier=message.dialog.client.system_id,
),
security_reference_number=1, # FIXME
security_datetime=SecurityDateTime(
DateTimeType.STS,
_now.date(),
_now.time(),
),
hash_algorithm=HashAlgorithm(
usage_hash='1',
hash_algorithm='999',
algorithm_parameter_name='1',
),
signature_algorithm=SignatureAlgorithm(
usage_signature='6',
signature_algorithm='10',
operation_mode='16',
),
key_name=KeyName(
message.dialog.client.bank_identifier,
message.dialog.client.user_id,
KeyType.S,
0,
0,
),
)
message += self.pending_signature
def _get_tan(self):
return None
def sign_commit(self, message: FinTSMessage):
if not self.pending_signature:
raise FinTSError("No signature is pending")
if self.pending_signature not in message.segments:
raise FinTSError("Cannot sign a message that was not prepared")
signature = HNSHA2(
security_reference=self.pending_signature.security_reference,
user_defined_signature=UserDefinedSignature(
pin=self.pin,
tan=self._get_tan(),
),
)
self.pending_signature = None
message += signature
def verify(self, message: FinTSMessage):
pass
class PinTanOneStepAuthenticationMechanism(PinTanAuthenticationMechanism):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.security_function = '999'
class PinTanTwoStepAuthenticationMechanism(PinTanAuthenticationMechanism):
def __init__(self, client, security_function, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = client
self.security_function = security_function
def _get_tan(self):
retval = self.client._pending_tan
self.client._pending_tan = None
return retval
python-fints-4.0.0/fints/segments/ 0000775 0000000 0000000 00000000000 14421014606 0017123 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/fints/segments/__init__.py 0000664 0000000 0000000 00000000000 14421014606 0021222 0 ustar 00root root 0000000 0000000 python-fints-4.0.0/fints/segments/accounts.py 0000664 0000000 0000000 00000002174 14421014606 0021320 0 ustar 00root root 0000000 0000000 from ..fields import DataElementGroupField
from ..formals import KTZ1, Account3, GetSEPAAccountParameter1
from .base import FinTS3Segment, ParameterSegment
class HKSPA1(FinTS3Segment):
"""SEPA-Kontoverbindung anfordern, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle
"""
accounts = DataElementGroupField(type=Account3, max_count=999, required=False, _d="Kontoverbindung")
class HISPA1(FinTS3Segment):
"""SEPA-Kontoverbindung rückmelden, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle
"""
accounts = DataElementGroupField(type=KTZ1, max_count=999, required=False, _d="SEPA-Kontoverbindung")
class HISPAS1(ParameterSegment):
"""SEPA-Kontoverbindung anfordern, Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle"""
parameter = DataElementGroupField(type=GetSEPAAccountParameter1, _d="Parameter SEPA-Kontoverbindung anfordern")
python-fints-4.0.0/fints/segments/auth.py 0000664 0000000 0000000 00000031312 14421014606 0020436 0 ustar 00root root 0000000 0000000 from fints.fields import CodeField, DataElementField, DataElementGroupField
from fints.formals import (
KTI1, BankIdentifier, ChallengeValidUntil, Language2,
ParameterChallengeClass, ParameterPinTan, ParameterTwostepTAN1,
ParameterTwostepTAN2, ParameterTwostepTAN3, ParameterTwostepTAN4,
ParameterTwostepTAN5, ParameterTwostepTAN6, ResponseHHDUC,
SystemIDStatus, TANMedia4, TANMedia5, TANMediaClass3,
TANMediaClass4, TANMediaType2, TANUsageOption,
)
from .base import FinTS3Segment, ParameterSegment
class HKIDN2(FinTS3Segment):
"""Identifikation, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
bank_identifier = DataElementGroupField(type=BankIdentifier, _d="Kreditinstitutskennung")
customer_id = DataElementField(type='id', _d="Kunden-ID")
system_id = DataElementField(type='id', _d="Kundensystem-ID")
system_id_status = CodeField(enum=SystemIDStatus, length=1, _d="Kundensystem-Status")
class HKVVB3(FinTS3Segment):
"""Verarbeitungsvorbereitung, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
bpd_version = DataElementField(type='num', max_length=3, _d="BPD-Version")
upd_version = DataElementField(type='num', max_length=3, _d="UPD-Version")
language = CodeField(enum=Language2, max_length=3, _d="Dialogsprache")
product_name = DataElementField(type='an', max_length=25, _d="Produktbezeichnung")
product_version = DataElementField(type='an', max_length=5, _d="Produktversion")
class HKTAN2(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
further_tan_follows = DataElementField(type='jn', length=1, required=False, _d="Weitere TAN folgt")
cancel_task = DataElementField(type='jn', length=1, required=False, _d="Auftrag stornieren")
challenge_class = DataElementField(type='num', max_length=2, required=False, _d="Challenge-Klasse")
parameter_challenge_class = DataElementGroupField(type=ParameterChallengeClass, required=False, _d="Parameter Challenge-Klasse")
class HKTAN3(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
further_tan_follows = DataElementField(type='jn', length=1, required=False, _d="Weitere TAN folgt")
cancel_task = DataElementField(type='jn', length=1, required=False, _d="Auftrag stornieren")
challenge_class = DataElementField(type='num', max_length=2, required=False, _d="Challenge-Klasse")
parameter_challenge_class = DataElementGroupField(type=ParameterChallengeClass, required=False, _d="Parameter Challenge-Klasse")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
class HKTAN5(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung, version 5
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
segment_type = DataElementField(type='an', max_length=6, required=False, _d="Segmentkennung")
account = DataElementGroupField(type=KTI1, required=False, _d="Kontoverbindung international Auftraggeber")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
further_tan_follows = DataElementField(type='jn', length=1, required=False, _d="Weitere TAN folgt")
cancel_task = DataElementField(type='jn', length=1, required=False, _d="Auftrag stornieren")
sms_charge_account = DataElementGroupField(type=KTI1, required=False, _d="SMS-Abbuchungskonto")
challenge_class = DataElementField(type='num', max_length=2, required=False, _d="Challenge-Klasse")
parameter_challenge_class = DataElementGroupField(type=ParameterChallengeClass, required=False, _d="Parameter Challenge-Klasse")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
class HKTAN6(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
segment_type = DataElementField(type='an', max_length=6, required=False, _d="Segmentkennung")
account = DataElementGroupField(type=KTI1, required=False, _d="Kontoverbindung international Auftraggeber")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
further_tan_follows = DataElementField(type='jn', length=1, required=False, _d="Weitere TAN folgt")
cancel_task = DataElementField(type='jn', length=1, required=False, _d="Auftrag stornieren")
sms_charge_account = DataElementGroupField(type=KTI1, required=False, _d="SMS-Abbuchungskonto")
challenge_class = DataElementField(type='num', max_length=2, required=False, _d="Challenge-Klasse")
parameter_challenge_class = DataElementGroupField(type=ParameterChallengeClass, required=False, _d="Parameter Challenge-Klasse")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
response_hhd_uc = DataElementGroupField(type=ResponseHHDUC, required=False, _d="Antwort HHD_UC")
class HITAN2(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung Rückmeldung, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
challenge = DataElementField(type='an', max_length=2048, required=False, _d="Challenge")
challenge_valid_until = DataElementGroupField(type=ChallengeValidUntil, required=False, _d="Gültigkeitsdatum und -uhrzeit für Challenge")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
ben = DataElementField(type='an', max_length=99, required=False, _d="BEN")
class HITAN3(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung Rückmeldung, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
challenge = DataElementField(type='an', max_length=2048, required=False, _d="Challenge")
challenge_valid_until = DataElementGroupField(type=ChallengeValidUntil, required=False, _d="Gültigkeitsdatum und -uhrzeit für Challenge")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
ben = DataElementField(type='an', max_length=99, required=False, _d="BEN")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
class HITAN5(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung Rückmeldung, version 5
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
challenge = DataElementField(type='an', max_length=2048, required=False, _d="Challenge")
challenge_hhduc = DataElementField(type='bin', required=False, _d="Challenge HHD_UC")
challenge_valid_until = DataElementGroupField(type=ChallengeValidUntil, required=False, _d="Gültigkeitsdatum und -uhrzeit für Challenge")
tan_list_number = DataElementField(type='an', max_length=20, required=False, _d="TAN-Listennummer")
ben = DataElementField(type='an', max_length=99, required=False, _d="BEN")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
class HITAN6(FinTS3Segment):
"""Zwei-Schritt-TAN-Einreichung Rückmeldung, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
challenge = DataElementField(type='an', max_length=2048, required=False, _d="Challenge")
challenge_hhduc = DataElementField(type='bin', required=False, _d="Challenge HHD_UC")
challenge_valid_until = DataElementGroupField(type=ChallengeValidUntil, required=False, _d="Gültigkeitsdatum und -uhrzeit für Challenge")
tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
class HKTAB4(FinTS3Segment):
"""TAN-Generator/Liste anzeigen Bestand, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_media_type = CodeField(enum=TANMediaType2, _d="TAN-Medium-Art")
tan_media_class = CodeField(enum=TANMediaClass3, _d="TAN-Medium-Klasse")
class HITAB4(FinTS3Segment):
"""TAN-Generator/Liste anzeigen Bestand Rückmeldung, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_usage_option = CodeField(enum=TANUsageOption, _d="TAN_Einsatzoption")
tan_media_list = DataElementGroupField(type=TANMedia4, max_count=99, required=False, _d="TAN-Medium-Liste")
class HKTAB5(FinTS3Segment):
"""TAN-Generator/Liste anzeigen Bestand, version 5
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_media_type = CodeField(enum=TANMediaType2, _d="TAN-Medium-Art")
tan_media_class = CodeField(enum=TANMediaClass4, _d="TAN-Medium-Klasse")
class HITAB5(FinTS3Segment):
"""TAN-Generator/Liste anzeigen Bestand Rückmeldung, version 5
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
tan_usage_option = CodeField(enum=TANUsageOption, _d="TAN_Einsatzoption")
tan_media_list = DataElementGroupField(type=TANMedia5, max_count=99, required=False, _d="TAN-Medium-Liste")
class HITANSBase(ParameterSegment):
pass
class HITANS1(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN1)
class HITANS2(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN2)
class HITANS3(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN3)
class HITANS4(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN4)
class HITANS5(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN5)
class HITANS6(HITANSBase):
parameter = DataElementGroupField(type=ParameterTwostepTAN6)
class HIPINS1(ParameterSegment):
"""PIN/TAN-spezifische Informationen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN
"""
parameter = DataElementGroupField(type=ParameterPinTan, _d="Parameter PIN/TAN-spezifische Informationen")
python-fints-4.0.0/fints/segments/bank.py 0000664 0000000 0000000 00000010031 14421014606 0020403 0 ustar 00root root 0000000 0000000 from fints.fields import CodeField, DataElementField, DataElementGroupField
from fints.formals import (
AccountInformation, AccountLimit, AllowedTransaction,
BankIdentifier, CommunicationParameter2, Language2,
SupportedHBCIVersions2, SupportedLanguages2, UPDUsage,
)
from .base import FinTS3Segment
class HIBPA3(FinTS3Segment):
"""Bankparameter allgemein, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
bpd_version = DataElementField(type='num', max_length=3, _d="BPD-Version")
bank_identifier = DataElementGroupField(type=BankIdentifier, _d="Kreditinstitutskennung")
bank_name = DataElementField(type='an', max_length=60, _d="Kreditinstitutsbezeichnung")
number_tasks = DataElementField(type='num', max_length=3, _d="Anzahl Geschäftsvorfallarten pro Nachricht")
supported_languages = DataElementGroupField(type=SupportedLanguages2, _d="Unterstützte Sprachen")
supported_hbci_version = DataElementGroupField(type=SupportedHBCIVersions2, _d="Unterstützte HBCI-Versionen")
max_message_length = DataElementField(type='num', max_length=4, required=False, _d="Maximale Nachrichtengröße")
min_timeout = DataElementField(type='num', max_length=4, required=False, _d="Minimaler Timeout-Wert")
max_timeout = DataElementField(type='num', max_length=4, required=False, _d="Maximaler Timeout-Wert")
class HIUPA4(FinTS3Segment):
"""Userparameter allgemein"""
user_identifier = DataElementField(type='id', _d="Benutzerkennung")
upd_version = DataElementField(type='num', max_length=3, _d="UPD-Version")
upd_usage = CodeField(UPDUsage, length=1, _d="UPD-Verwendung")
username = DataElementField(type='an', max_length=35, required=False, _d="Benutzername")
extension = DataElementField(type='an', max_length=2048, required=False, _d="Erweiterung, allgemein")
class HIUPD6(FinTS3Segment):
"""Kontoinformationen"""
account_information = DataElementGroupField(type=AccountInformation, required=False, _d="Kontoverbindung")
iban = DataElementField(type='an', max_length=34, _d="IBAN")
customer_id = DataElementField(type='id', _d="Kunden-ID")
account_type = DataElementField(type='num', max_length=2, _d="Kontoart")
account_currency = DataElementField(type='cur', _d="Kontowährung")
name_account_owner_1 = DataElementField(type='an', max_length=27, _d="Name des Kontoinhabers 1")
name_account_owner_2 = DataElementField(type='an', max_length=27, required=False, _d="Name des Kontoinhabers 2")
account_product_name = DataElementField(type='an', max_length=30, required=False, _d="Kontoproduktbezeichnung")
account_limit = DataElementGroupField(type=AccountLimit, required=False, _d="Kontolimit")
allowed_transactions = DataElementGroupField(type=AllowedTransaction, count=999, required=False, _d="Erlaubte Geschäftsvorfälle")
extension = DataElementField(type='an', max_length=2048, required=False, _d="Erweiterung, kontobezogen")
class HKKOM4(FinTS3Segment):
"""Kommunikationszugang anfordern, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
start_bank_identifier = DataElementGroupField(type=BankIdentifier, required=False, _d="Von Kreditinstitutskennung")
end_bank_identifier = DataElementGroupField(type=BankIdentifier, required=False, _d="Bis Kreditinstitutskennung")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIKOM4(FinTS3Segment):
"""Kommunikationszugang rückmelden, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
bank_identifier = DataElementGroupField(type=BankIdentifier, _d="Kreditinstitutskennung")
default_language = CodeField(enum=Language2, max_length=3, _d="Standardsprache")
communication_parameters = DataElementGroupField(type=CommunicationParameter2, min_count=1, max_count=9, _d="Kommunikationsparameter")
python-fints-4.0.0/fints/segments/base.py 0000664 0000000 0000000 00000005132 14421014606 0020410 0 ustar 00root root 0000000 0000000 import re
from fints.fields import DataElementField, DataElementGroupField, IntCodeField
from fints.formals import SecurityClass, SegmentHeader
from fints.types import Container, ContainerMeta
from fints.utils import SubclassesMixin, classproperty
TYPE_VERSION_RE = re.compile(r'^([A-Z]+)(\d+)$')
class FinTS3SegmentMeta(ContainerMeta):
@staticmethod
def _check_fields_recursive(instance):
for name, field in instance._fields.items():
if not isinstance(field, (DataElementField, DataElementGroupField)):
raise TypeError("{}={!r} is not DataElementField or DataElementGroupField".format(name, field))
if isinstance(field, DataElementGroupField):
FinTS3SegmentMeta._check_fields_recursive(field.type)
def __new__(cls, name, bases, classdict):
retval = super().__new__(cls, name, bases, classdict)
FinTS3SegmentMeta._check_fields_recursive(retval)
return retval
class FinTS3Segment(Container, SubclassesMixin, metaclass=FinTS3SegmentMeta):
header = DataElementGroupField(type=SegmentHeader, _d="Segmentkopf")
@classproperty
def TYPE(cls):
match = TYPE_VERSION_RE.match(cls.__name__)
if match:
return match.group(1)
@classproperty
def VERSION(cls):
match = TYPE_VERSION_RE.match(cls.__name__)
if match:
return int(match.group(2))
def __init__(self, *args, **kwargs):
if 'header' not in kwargs:
kwargs['header'] = SegmentHeader(self.TYPE, None, self.VERSION)
args = (kwargs.pop('header'), ) + args
super().__init__(*args, **kwargs)
@classmethod
def find_subclass(cls, segment):
h = SegmentHeader.naive_parse(segment[0])
target_cls = None
for possible_cls in cls._all_subclasses():
if getattr(possible_cls, 'TYPE', None) == h.type and getattr(possible_cls, 'VERSION', None) == h.version:
target_cls = possible_cls
if not target_cls:
target_cls = cls
return target_cls
class ParameterSegment_22(FinTS3Segment):
max_number_tasks = DataElementField(type='num', max_length=3, _d="Maximale Anzahl Aufträge")
min_number_signatures = DataElementField(type='num', length=1, _d="Anzahl Signaturen mindestens")
class ParameterSegment(FinTS3Segment):
max_number_tasks = DataElementField(type='num', max_length=3, _d="Maximale Anzahl Aufträge")
min_number_signatures = DataElementField(type='num', length=1, _d="Anzahl Signaturen mindestens")
security_class = IntCodeField(SecurityClass, length=1, _d="Sicherheitsklasse")
python-fints-4.0.0/fints/segments/debit.py 0000664 0000000 0000000 00000030776 14421014606 0020601 0 ustar 00root root 0000000 0000000 from ..fields import CodeField, DataElementField, DataElementGroupField
from ..formals import (
KTI1, Amount1, QueryScheduledBatchDebitParameter1,
QueryScheduledDebitParameter1, QueryScheduledDebitParameter2,
ScheduledBatchDebitParameter1, ScheduledBatchDebitParameter2,
ScheduledCOR1BatchDebitParameter1, ScheduledCOR1DebitParameter1,
ScheduledDebitParameter1, ScheduledDebitParameter2, SEPACCode1,
StatusSEPATask1, SupportedSEPAPainMessages1,
)
from .base import FinTS3Segment, ParameterSegment
class BatchDebitBase(FinTS3Segment):
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sum_amount = DataElementGroupField(type=Amount1, _d="Summenfeld")
request_single_booking = DataElementField(type='jn', _d="Einzelbuchung gewünscht")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class DebitResponseBase(FinTS3Segment):
task_id = DataElementField(type='an', max_length=99, required=False, _d="Auftragsidentifikation")
class HKDSE1(FinTS3Segment):
"""Terminierte SEPA-Einzellastschrift einreichen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class HIDSE1(DebitResponseBase):
"""Einreichung terminierter SEPA-Einzellastschrift bestätigen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDSES1(ParameterSegment):
"""Terminierte SEPA-Einzellastschrift einreichen Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledDebitParameter1, _d="Parameter terminierte SEPA-Sammellastschrift einreichen")
class HKDSE2(FinTS3Segment):
"""Terminierte SEPA-Einzellastschrift einreichen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class HIDSE2(DebitResponseBase):
"""Einreichung terminierter SEPA-Einzellastschrift bestätigen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDSES2(ParameterSegment):
"""Terminierte SEPA-Einzellastschrift einreichen Parameter, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledDebitParameter2, _d="Parameter terminierte SEPA-Sammellastschrift einreichen")
class HKDME1(BatchDebitBase):
"""Einreichung terminierter SEPA-Sammellastschrift, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDME1(DebitResponseBase):
"""Einreichung terminierter SEPA-Sammellastschrift bestätigen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDMES1(ParameterSegment):
"""Terminierte SEPA-Sammellastschrift einreichen Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledBatchDebitParameter1, _d="Parameter terminierte SEPA-Sammellastschrift einreichen")
class HKDME2(BatchDebitBase):
"""Einreichung terminierter SEPA-Sammellastschrift, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDME2(DebitResponseBase):
"""Einreichung terminierter SEPA-Sammellastschrift bestätigen, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDMES2(ParameterSegment):
"""Terminierte SEPA-Sammellastschrift einreichen Parameter, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledBatchDebitParameter2, _d="Parameter terminierte SEPA-Sammellastschrift einreichen")
class HKDSC1(FinTS3Segment):
"""Terminierte SEPA-COR1-Einzellastschrift einreichen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class HIDSC1(DebitResponseBase):
"""Einreichung terminierter SEPA-COR1-Einzellastschrift bestätigen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDSCS1(ParameterSegment):
"""Terminierte SEPA-COR1-Einzellastschrift Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledCOR1DebitParameter1, _d="Parameter terminierte SEPA-COR1-Einzellastschrift")
class HKDMC1(BatchDebitBase):
"""Terminierte SEPA-COR1-Sammellastschrift einreichen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDMC1(DebitResponseBase):
"""Einreichung terminierter SEPA-COR1-Sammellastschrift bestätigen, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
class HIDMCS1(ParameterSegment):
"""Terminierte SEPA-COR1-Sammellastschrift Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=ScheduledCOR1BatchDebitParameter1, _d="Parameter terminierte SEPA-COR1-Sammellastschrift")
class HKDBS1(FinTS3Segment):
"""Bestand terminierter SEPA-Einzellastschriften anfordern, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
supported_sepa_pain_messages = DataElementGroupField(type=SupportedSEPAPainMessages1, _d="Unterstützte SEPA pain messages")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIDBS1(FinTS3Segment):
"""Bestand terminierter SEPA-Einzellastschriften rückmelden, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
task_id = DataElementField(type='an', max_length=99, required=False, _d="Auftragsidentifikation")
task_cancelable = DataElementField(type='jn', required=False, _d="Auftrag löschbar")
task_changeable = DataElementField(type='jn', required=False, _d="Auftrag änderbar")
class HIDBSS1(ParameterSegment):
"""Bestand terminierter SEPA-Einzellastschriften Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=QueryScheduledDebitParameter1, _d="Parameter Bestand terminierter SEPA-Einzellastschriften")
class HKDBS2(FinTS3Segment):
"""Bestand terminierter SEPA-Einzellastschriften anfordern, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
supported_sepa_pain_messages = DataElementGroupField(type=SupportedSEPAPainMessages1, _d="Unterstützte SEPA pain messages")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIDBS2(FinTS3Segment):
"""Bestand terminierter SEPA-Einzellastschriften rückmelden, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
task_id = DataElementField(type='an', max_length=99, required=False, _d="Auftragsidentifikation")
sepa_c_code = CodeField(enum=SEPACCode1, _d="SEPA-C-Code")
task_changeable = DataElementField(type='jn', required=False, _d="Auftrag änderbar")
status_sepa_task = CodeField(enum=StatusSEPATask1, _d="Status SEPA-Auftrag")
class HIDBSS2(ParameterSegment):
"""Bestand terminierter SEPA-Einzellastschriften Parameter, version 2
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=QueryScheduledDebitParameter2, _d="Parameter Bestand terminierter SEPA-Einzellastschriften")
class HKDMB1(FinTS3Segment):
"""Bestand terminierter SEPA-Sammellastschriften anfordern, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIDMB1(FinTS3Segment):
"""Bestand terminierter SEPA-Sammellastschriften rückmelden, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
task_id = DataElementField(type='an', max_length=99, required=False, _d="Auftragsidentifikation")
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
date_entered = DataElementField(type='dat', required=False, _d="Einreichungsdatum")
date_booked = DataElementField(type='dat', required=False, _d="Ausführungsdatum")
debit_count = DataElementField(type='num', max_length=6, _d="Anzahl der Aufträge")
sum_amount = DataElementGroupField(type=Amount1, _d="Summe der Beträge")
class HIDMBS1(ParameterSegment):
"""Bestand terminierter SEPA-Sammellastschriften Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=QueryScheduledBatchDebitParameter1, _d="Parameter Bestand terminierter SEPA-Sammellastschriften")
python-fints-4.0.0/fints/segments/depot.py 0000664 0000000 0000000 00000003632 14421014606 0020614 0 ustar 00root root 0000000 0000000 from fints.fields import DataElementField, DataElementGroupField
from fints.formals import Account2, Account3
from .base import FinTS3Segment
class HKWPD5(FinTS3Segment):
"""Depotaufstellung anfordern, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=Account2, _d="Depot")
currency = DataElementField(type='cur', required=False, _d="Währung der Depotaufstellung")
quality = DataElementField(type='num', length=1, required=False, _d="Kursqualität")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIWPD5(FinTS3Segment):
"""Depotaufstellung rückmelden, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
holdings = DataElementField(type='bin', _d="Depotaufstellung")
class HKWPD6(FinTS3Segment):
"""Depotaufstellung anfordern, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=Account3, _d="Depot")
currency = DataElementField(type='cur', required=False, _d="Währung der Depotaufstellung")
quality = DataElementField(type='code', length=1, required=False, _d="Kursqualität")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIWPD6(FinTS3Segment):
"""Depotaufstellung rückmelden, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
holdings = DataElementField(type='bin', _d="Depotaufstellung")
python-fints-4.0.0/fints/segments/dialog.py 0000664 0000000 0000000 00000002720 14421014606 0020735 0 ustar 00root root 0000000 0000000 from ..fields import CodeField, DataElementField, DataElementGroupField
from ..formals import Response, SynchronizationMode
from .base import FinTS3Segment
class HKSYN3(FinTS3Segment):
"""Synchronisierung, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
synchronization_mode = CodeField(enum=SynchronizationMode, length=1)
class HISYN4(FinTS3Segment):
"""Synchronisierungsantwort"""
system_id = DataElementField(type='id', _d="Kundensystem-ID")
message_number = DataElementField(type='num', max_length=4, required=False, _d="Nachrichtennummer")
security_reference_signature_key = DataElementField(type='num', max_length=16, required=False, _d="Sicherheitsreferenznummer für Signierschlüssel")
security_reference_digital_signature = DataElementField(type='num', max_length=16, required=False, _d="Sicherheitsreferenznummer für Digitale Signatur")
class HKEND1(FinTS3Segment):
"""Dialogende, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
dialog_id = DataElementField(type='id', _d="Dialog-ID")
class HIRMG2(FinTS3Segment):
"""Rückmeldungen zur Gesamtnachricht"""
responses = DataElementGroupField(type=Response, min_count=1, max_count=99, _d="Rückmeldung")
class HIRMS2(FinTS3Segment):
"""Rückmeldungen zu Segmenten"""
responses = DataElementGroupField(type=Response, min_count=1, max_count=99, _d="Rückmeldung")
python-fints-4.0.0/fints/segments/journal.py 0000664 0000000 0000000 00000005161 14421014606 0021152 0 ustar 00root root 0000000 0000000 from fints.fields import DataElementField, DataElementGroupField
from fints.formals import ReferenceMessage, Response
from .base import FinTS3Segment, ParameterSegment, ParameterSegment_22
class HKPRO3(FinTS3Segment):
"""Statusprotokoll anfordern, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIPRO3(FinTS3Segment):
"""Statusprotokoll rückmelden, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
reference_message = DataElementGroupField(type=ReferenceMessage, _d="Bezugsnachricht")
reference = DataElementField(type='num', max_length=3, required=False, _d='Bezugssegment')
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', _d="Uhrzeit")
responses = DataElementGroupField(type=Response, _d="Rückmeldung")
class HIPROS3(ParameterSegment_22):
"""Statusprotokoll Parameter, version 3
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
class HKPRO4(FinTS3Segment):
"""Statusprotokoll anfordern, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIPRO4(FinTS3Segment):
"""Statusprotokoll rückmelden, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
reference_message = DataElementGroupField(type=ReferenceMessage, _d="Bezugsnachricht")
reference = DataElementField(type='num', max_length=3, required=False, _d='Bezugssegment')
date = DataElementField(type='dat', _d="Datum")
time = DataElementField(type='tim', _d="Uhrzeit")
responses = DataElementGroupField(type=Response, _d="Rückmeldung")
class HIPROS4(ParameterSegment):
"""Statusprotokoll Parameter, version 4
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals"""
python-fints-4.0.0/fints/segments/message.py 0000664 0000000 0000000 00000010247 14421014606 0021125 0 ustar 00root root 0000000 0000000 from fints.fields import (
CodeField, DataElementField, DataElementGroupField,
SegmentSequenceField, ZeroPaddedNumericField,
)
from fints.formals import (
Certificate, CompressionFunction, EncryptionAlgorithm,
HashAlgorithm, KeyName, ReferenceMessage, SecurityApplicationArea,
SecurityDateTime, SecurityIdentificationDetails, SecurityProfile,
SecurityRole, SignatureAlgorithm, UserDefinedSignature,
)
from .base import FinTS3Segment
class HNHBK3(FinTS3Segment):
"""Nachrichtenkopf"""
message_size = ZeroPaddedNumericField(length=12, _d="Größe der Nachricht (nach Verschlüsselung und Komprimierung)")
hbci_version = DataElementField(type='num', max_length=3, _d="HBCI-Version")
dialog_id = DataElementField(type='id', _d="Dialog-ID")
message_number = DataElementField(type='num', max_length=4, _d="Nachrichtennummer")
reference_message = DataElementGroupField(type=ReferenceMessage, required=False, _d="Bezugsnachricht")
class HNHBS1(FinTS3Segment):
"""Nachrichtenabschluss"""
message_number = DataElementField(type='num', max_length=4, _d="Nachrichtennummer")
class HNVSK3(FinTS3Segment):
"""Verschlüsselungskopf, version 3
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
security_profile = DataElementGroupField(type=SecurityProfile, _d="Sicherheitsprofil")
security_function = DataElementField(type='code', max_length=3, _d="Sicherheitsfunktion, kodiert")
security_role = CodeField(SecurityRole, max_length=3, _d="Rolle des Sicherheitslieferanten, kodiert")
security_identification_details = DataElementGroupField(type=SecurityIdentificationDetails, _d="Sicherheitsidentifikation, Details")
security_datetime = DataElementGroupField(type=SecurityDateTime, _d="Sicherheitsdatum und -uhrzeit")
encryption_algorithm = DataElementGroupField(type=EncryptionAlgorithm, _d="Verschlüsselungsalgorithmus")
key_name = DataElementGroupField(type=KeyName, _d="Schlüsselname")
compression_function = CodeField(CompressionFunction, max_length=3, _d="Komprimierungsfunktion")
certificate = DataElementGroupField(type=Certificate, required=False, _d="Zertifikat")
class HNVSD1(FinTS3Segment):
"""Verschlüsselte Daten, version 1
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
data = SegmentSequenceField(_d="Daten, verschlüsselt")
class HNSHK4(FinTS3Segment):
"""Signaturkopf, version 4
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
security_profile = DataElementGroupField(type=SecurityProfile, _d="Sicherheitsprofil")
security_function = DataElementField(type='code', max_length=3, _d="Sicherheitsfunktion, kodiert")
security_reference = DataElementField(type='an', max_length=14, _d="Sicherheitskontrollreferenz")
security_application_area = CodeField(SecurityApplicationArea, max_length=3, _d="Bereich der Sicherheitsapplikation, kodiert")
security_role = CodeField(SecurityRole, max_length=3, _d="Rolle des Sicherheitslieferanten, kodiert")
security_identification_details = DataElementGroupField(type=SecurityIdentificationDetails, _d="Sicherheitsidentifikation, Details")
security_reference_number = DataElementField(type='num', max_length=16, _d="Sicherheitsreferenznummer")
security_datetime = DataElementGroupField(type=SecurityDateTime, _d="Sicherheitsdatum und -uhrzeit")
hash_algorithm = DataElementGroupField(type=HashAlgorithm, _d="Hashalgorithmus")
signature_algorithm = DataElementGroupField(type=SignatureAlgorithm, _d="Signaturalgorithmus")
key_name = DataElementGroupField(type=KeyName, _d="Schlüsselname")
certificate = DataElementGroupField(type=Certificate, required=False, _d="Zertifikat")
class HNSHA2(FinTS3Segment):
"""Signaturabschluss, version 2
Source: FinTS Financial Transaction Services, Sicherheitsverfahren HBCI"""
security_reference = DataElementField(type='an', max_length=14, _d="Sicherheitskontrollreferenz")
validation_result = DataElementField(type='bin', max_length=512, required=False, _d="Validierungsresultat")
user_defined_signature = DataElementGroupField(type=UserDefinedSignature, required=False, _d="Benutzerdefinierte Signatur")
python-fints-4.0.0/fints/segments/saldo.py 0000664 0000000 0000000 00000012415 14421014606 0020602 0 ustar 00root root 0000000 0000000 from fints.fields import DataElementField, DataElementGroupField
from fints.formals import (
KTI1, Account2, Account3, Amount1, Balance1, Balance2, Timestamp1,
)
from .base import FinTS3Segment
class HKSAL5(FinTS3Segment):
"""Saldenabfrage, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=Account2, _d="Kontoverbindung Auftraggeber")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HISAL5(FinTS3Segment):
"""Saldenrückmeldung, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=Account2, _d="Kontoverbindung Auftraggeber")
account_product = DataElementField(type='an', max_length=30, _d="Kontoproduktbezeichnung")
currency = DataElementField(type='cur', _d="Kontowährung")
balance_booked = DataElementGroupField(type=Balance1, _d="Gebuchter Saldo")
balance_pending = DataElementGroupField(type=Balance1, required=False, _d="Saldo der vorgemerkten Umsätze")
line_of_credit = DataElementGroupField(type=Amount1, required=False, _d="Kreditlinie")
available_amount = DataElementGroupField(type=Amount1, required=False, _d="Verfügbarer Betrag")
used_amount = DataElementGroupField(type=Amount1, required=False, _d="Bereits verfügter Betrag")
booking_date = DataElementField(type='dat', required=False, _d="Buchungsdatum des Saldos")
booking_time = DataElementField(type='tim', required=False, _d="Buchungsuhrzeit des Saldos")
date_due = DataElementField(type='dat', required=False, _d="Fälligkeit")
class HKSAL6(FinTS3Segment):
"""Saldenabfrage, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=Account3, _d="Kontoverbindung Auftraggeber")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HISAL6(FinTS3Segment):
"""Saldenrückmeldung, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=Account3, _d="Kontoverbindung Auftraggeber")
account_product = DataElementField(type='an', max_length=30, _d="Kontoproduktbezeichnung")
currency = DataElementField(type='cur', _d="Kontowährung")
balance_booked = DataElementGroupField(type=Balance2, _d="Gebuchter Saldo")
balance_pending = DataElementGroupField(type=Balance2, required=False, _d="Saldo der vorgemerkten Umsätze")
line_of_credit = DataElementGroupField(type=Amount1, required=False, _d="Kreditlinie")
available_amount = DataElementGroupField(type=Amount1, required=False, _d="Verfügbarer Betrag")
used_amount = DataElementGroupField(type=Amount1, required=False, _d="Bereits verfügter Betrag")
overdraft = DataElementGroupField(type=Amount1, required=False, _d="Überziehung")
booking_timestamp = DataElementGroupField(type=Timestamp1, required=False, _d="Buchungszeitpunkt")
date_due = DataElementField(type='dat', required=False, _d="Fälligkeit")
class HKSAL7(FinTS3Segment):
"""Saldenabfrage, version 7
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HISAL7(FinTS3Segment):
"""Saldenrückmeldung, version 7
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
account_product = DataElementField(type='an', max_length=30, _d="Kontoproduktbezeichnung")
currency = DataElementField(type='cur', _d="Kontowährung")
balance_booked = DataElementGroupField(type=Balance2, _d="Gebuchter Saldo")
balance_pending = DataElementGroupField(type=Balance2, required=False, _d="Saldo der vorgemerkten Umsätze")
line_of_credit = DataElementGroupField(type=Amount1, required=False, _d="Kreditlinie")
available_amount = DataElementGroupField(type=Amount1, required=False, _d="Verfügbarer Betrag")
used_amount = DataElementGroupField(type=Amount1, required=False, _d="Bereits verfügter Betrag")
overdraft = DataElementGroupField(type=Amount1, required=False, _d="Überziehung")
booking_timestamp = DataElementGroupField(type=Timestamp1, required=False, _d="Buchungszeitpunkt")
date_due = DataElementField(type='dat', required=False, _d="Fälligkeit")
python-fints-4.0.0/fints/segments/statement.py 0000664 0000000 0000000 00000013461 14421014606 0021506 0 ustar 00root root 0000000 0000000 from fints.fields import DataElementField, DataElementGroupField
from fints.formals import KTI1, Account2, Account3, QueryCreditCardStatements2, SupportedMessageTypes, \
BookedCamtStatements1
from .base import FinTS3Segment, ParameterSegment
class HKKAZ5(FinTS3Segment):
"""Kontoumsätze anfordern/Zeitraum, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=Account2, _d="Kontoverbindung Auftraggeber")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIKAZ5(FinTS3Segment):
"""Kontoumsätze rückmelden/Zeitraum, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
statement_booked = DataElementField(type='bin', _d="Gebuchte Umsätze")
statement_pending = DataElementField(type='bin', required=False, _d="Nicht gebuchte Umsätze")
class HKKAZ6(FinTS3Segment):
"""Kontoumsätze anfordern/Zeitraum, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=Account3, _d="Kontoverbindung Auftraggeber")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIKAZ6(FinTS3Segment):
"""Kontoumsätze rückmelden/Zeitraum, version 6
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
statement_booked = DataElementField(type='bin', _d="Gebuchte Umsätze")
statement_pending = DataElementField(type='bin', required=False, _d="Nicht gebuchte Umsätze")
class HKKAZ7(FinTS3Segment):
"""Kontoumsätze anfordern/Zeitraum, version 7
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HIKAZ7(FinTS3Segment):
"""Kontoumsätze rückmelden/Zeitraum, version 7
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
statement_booked = DataElementField(type='bin', _d="Gebuchte Umsätze")
statement_pending = DataElementField(type='bin', required=False, _d="Nicht gebuchte Umsätze")
class DKKKU2(FinTS3Segment):
"""Kreditkartenumsätze anfordern, version 2
Source: Reverse engineered"""
account = DataElementGroupField(type=Account2, _d="Kontoverbindung Auftraggeber")
credit_card_number = DataElementField(type='an', _d="Kreditkartennummer")
subaccount = DataElementField(type='an', required=False, _d="Subaccount?")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class DIKKU2(FinTS3Segment):
"""Kreditkartenumsätze rückmelden, version 2
Source: Reverse engineered"""
class DIKKUS2(ParameterSegment):
"""Kreditkartenumsätze anfordern Parameter, version 2
Source: Reverse engineered"""
parameter = DataElementGroupField(type=QueryCreditCardStatements2, _d="Parameter Kreditkartenumsätze anfordern")
class HKCAZ1(FinTS3Segment):
"""Kontoumsätze anfordern/Zeitraum, version 5
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
supported_camt_messages = DataElementGroupField(type=SupportedMessageTypes, _d="Kontoverbindung international")
all_accounts = DataElementField(type='jn', _d="Alle Konten")
date_start = DataElementField(type='dat', required=False, _d="Von Datum")
date_end = DataElementField(type='dat', required=False, _d="Bis Datum")
max_number_responses = DataElementField(type='num', max_length=4, required=False, _d="Maximale Anzahl Einträge")
touchdown_point = DataElementField(type='an', max_length=35, required=False, _d="Aufsetzpunkt")
class HICAZ1(FinTS3Segment):
"""Kontoumsätze rückmelden/Zeitraum, version 1
Source: HBCI Homebanking-Computer-Interface, Schnittstellenspezifikation"""
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung Auftraggeber")
camt_descriptor = DataElementField(type='an', _d="camt-Deskriptor")
statement_booked = DataElementGroupField(type=BookedCamtStatements1, _d="Gebuchte Umsätze")
statement_pending = DataElementField(type='bin', required=False, _d="Nicht gebuchte Umsätze")
python-fints-4.0.0/fints/segments/transfer.py 0000664 0000000 0000000 00000007736 14421014606 0021336 0 ustar 00root root 0000000 0000000 from fints.fields import DataElementField, DataElementGroupField
from fints.formals import KTI1, Amount1, BatchTransferParameter1
from .base import FinTS3Segment, ParameterSegment
class HKCCS1(FinTS3Segment):
"""SEPA Einzelüberweisung, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class HKIPZ1(FinTS3Segment):
"""SEPA-instant Einzelüberweisung, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
# class HKIPZ2(FinTS3Segment):
# """SEPA-instant Einzelüberweisung, version 2
#
# Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
# account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
# sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
# sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
# allow_convert_sepa_transfer = DataElementField(type="jn", _d="Allow conversion to SEPA transfer if instant-payment not supported")
class HKCCM1(FinTS3Segment):
"""SEPA-Sammelüberweisung, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sum_amount = DataElementGroupField(type=Amount1, _d="Summenfeld")
request_single_booking = DataElementField(type='jn', _d="Einzelbuchung gewünscht")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
class HKIPM1(FinTS3Segment):
"""SEPA-instant Sammelüberweisung, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
sum_amount = DataElementGroupField(type=Amount1, _d="Summenfeld")
request_single_booking = DataElementField(type='jn', _d="Einzelbuchung gewünscht")
sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
# class HKIPM2(FinTS3Segment):
# """SEPA-instant Sammelüberweisung, version 2
#
# Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
# account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international")
# sum_amount = DataElementGroupField(type=Amount1, _d="Summenfeld")
# request_single_booking = DataElementField(type='jn', _d="Einzelbuchung gewünscht")
# sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor")
# sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message")
# allow_convert_sepa_transfer = DataElementField(type="jn", _d="Allow conversion to SEPA transfer if instant-payment not supported")
class HICCMS1(ParameterSegment):
"""SEPA-Sammelüberweisung Parameter, version 1
Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """
parameter = DataElementGroupField(type=BatchTransferParameter1, _d="Parameter SEPA-Sammelüberweisung")
python-fints-4.0.0/fints/types.py 0000664 0000000 0000000 00000040644 14421014606 0017024 0 ustar 00root root 0000000 0000000 from collections import OrderedDict
from collections.abc import Iterable
from contextlib import suppress
from .exceptions import FinTSNoResponseError
from .utils import SubclassesMixin
class Field:
def __init__(self, length=None, min_length=None, max_length=None, count=None, min_count=None, max_count=None, required=True, _d=None):
if length is not None and (min_length is not None or max_length is not None):
raise ValueError("May not specify both 'length' AND 'min_length'/'max_length'")
if count is not None and (min_count is not None or max_count is not None):
raise ValueError("May not specify both 'count' AND 'min_count'/'max_count'")
self.length = length
self.min_length = min_length
self.max_length = max_length
self.count = count
self.min_count = min_count
self.max_count = max_count
self.required = required
if not self.count and not self.min_count and not self.max_count:
self.count = 1
self.__doc__ = _d
def _default_value(self):
return None
def __get__(self, instance, owner):
if self not in instance._values:
self.__set__(instance, None)
return instance._values[self]
def __set__(self, instance, value):
if value is None:
if self.count == 1:
instance._values[self] = self._default_value()
else:
instance._values[self] = ValueList(parent=self)
else:
if self.count == 1:
value_ = self._parse_value(value)
self._check_value(value_)
else:
value_ = ValueList(parent=self)
for i, v in enumerate(value):
value_[i] = v
instance._values[self] = value_
def __delete__(self, instance):
self.__set__(instance, None)
def _parse_value(self, value):
raise NotImplementedError('Needs to be implemented in subclass')
def _render_value(self, value):
raise NotImplementedError('Needs to be implemented in subclass')
def _check_value(self, value):
with suppress(NotImplementedError):
self._render_value(value)
def _check_value_length(self, value):
if self.max_length is not None and len(value) > self.max_length:
raise ValueError("Value {!r} cannot be rendered: max_length={} exceeded".format(value, self.max_length))
if self.min_length is not None and len(value) < self.min_length:
raise ValueError("Value {!r} cannot be rendered: min_length={} not reached".format(value, self.min_length))
if self.length is not None and len(value) != self.length:
raise ValueError("Value {!r} cannot be rendered: length={} not satisfied".format(value, self.length))
def render(self, value):
if value is None:
return None
return self._render_value(value)
def _inline_doc_comment(self, value):
if self.__doc__:
d = self.__doc__.splitlines()[0].strip()
if d:
return " # {}".format(d)
return ""
class TypedField(Field, SubclassesMixin):
def __new__(cls, *args, **kwargs):
target_cls = None
fallback_cls = None
for subcls in cls._all_subclasses():
if getattr(subcls, 'type', '') is None:
fallback_cls = subcls
if getattr(subcls, 'type', None) == kwargs.get('type', None):
target_cls = subcls
break
if target_cls is None and fallback_cls is not None and issubclass(fallback_cls, cls):
target_cls = fallback_cls
retval = object.__new__(target_cls or cls)
return retval
def __init__(self, type=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.type = type or getattr(self.__class__, 'type', None)
class ValueList:
def __init__(self, parent):
self._parent = parent
self._data = []
def __getitem__(self, i):
if i >= len(self._data):
self.__setitem__(i, None)
if i < 0:
raise IndexError("Cannot access negative index")
return self._data[i]
def __setitem__(self, i, value):
if i < 0:
raise IndexError("Cannot access negative index")
if self._parent.count is not None:
if i >= self._parent.count:
raise IndexError("Cannot access index {} beyond count {}".format(i, self._parent.count))
elif self._parent.max_count is not None:
if i >= self._parent.max_count:
raise IndexError("Cannot access index {} beyound max_count {}".format(i, self._parent.max_count))
for x in range(len(self._data), i):
self.__setitem__(x, None)
if value is None:
value = self._parent._default_value()
else:
value = self._parent._parse_value(value)
self._parent._check_value(value)
if i == len(self._data):
self._data.append(value)
else:
self._data[i] = value
def __delitem__(self, i):
self.__setitem__(i, None)
def _get_minimal_true_length(self):
retval = 0
for i, val in enumerate(self._data):
if isinstance(val, Container):
if val.is_unset():
continue
elif val is None:
continue
retval = i + 1
return retval
def __len__(self):
if self._parent.count is not None:
return self._parent.count
else:
retval = self._get_minimal_true_length()
if self._parent.min_count is not None:
if self._parent.min_count > retval:
retval = self._parent.min_count
return retval
def __iter__(self):
for i in range(len(self)):
yield self[i]
def __repr__(self):
return "{!r}".format(list(self))
def print_nested(self, stream=None, level=0, indent=" ", prefix="", first_level_indent=True, trailer="", print_doc=True, first_line_suffix=""):
import sys
stream = stream or sys.stdout
stream.write(
((prefix + level * indent) if first_level_indent else "")
+ "[{}\n".format(first_line_suffix)
)
min_true_length = self._get_minimal_true_length()
skipped_items = 0
for i, val in enumerate(self):
if i > min_true_length:
skipped_items += 1
continue
if print_doc:
docstring = self._parent._inline_doc_comment(val)
else:
docstring = ""
if not hasattr(getattr(val, 'print_nested', None), '__call__'):
stream.write(
(prefix + (level + 1) * indent) + "{!r},{}\n".format(val, docstring)
)
else:
val.print_nested(stream=stream, level=level + 2, indent=indent, prefix=prefix, trailer=",", print_doc=print_doc, first_line_suffix=docstring)
if skipped_items:
stream.write((prefix + (level + 1) * indent) + "# {} empty items skipped\n".format(skipped_items))
stream.write((prefix + level * indent) + "]{}\n".format(trailer))
class SegmentSequence:
"""A sequence of FinTS3Segment objects"""
def __init__(self, segments=None):
if isinstance(segments, bytes):
from .parser import FinTS3Parser
parser = FinTS3Parser()
data = parser.explode_segments(segments)
segments = [parser.parse_segment(segment) for segment in data]
self.segments = list(segments) if segments else []
def render_bytes(self) -> bytes:
from .parser import FinTS3Serializer
return FinTS3Serializer().serialize_message(self)
def __repr__(self):
return "{}.{}({!r})".format(self.__class__.__module__, self.__class__.__name__, self.segments)
def print_nested(self, stream=None, level=0, indent=" ", prefix="", first_level_indent=True, trailer="", print_doc=True, first_line_suffix=""):
import sys
stream = stream or sys.stdout
stream.write(
((prefix + level * indent) if first_level_indent else "")
+ "{}.{}([".format(self.__class__.__module__, self.__class__.__name__)
+ first_line_suffix
+ "\n"
)
for segment in self.segments:
docstring = print_doc and segment.__doc__
if docstring:
docstring = docstring.splitlines()[0].strip()
if docstring:
docstring = " # {}".format(docstring)
else:
docstring = ""
segment.print_nested(stream=stream, level=level + 1, indent=indent, prefix=prefix, first_level_indent=True, trailer=",", print_doc=print_doc,
first_line_suffix=docstring)
stream.write((prefix + level * indent) + "]){}\n".format(trailer))
def find_segments(self, query=None, version=None, callback=None, recurse=True, throw=False):
"""Yields an iterable of all matching segments.
:param query: Either a str or class specifying a segment type (such as 'HNHBK', or :class:`~fints.segments.message.HNHBK3`), or a list or tuple of strings or classes.
If a list/tuple is specified, segments returning any matching type will be returned.
:param version: Either an int specifying a segment version, or a list or tuple of ints.
If a list/tuple is specified, segments returning any matching version will be returned.
:param callback: A callable that will be given the segment as its sole argument and must return a boolean indicating whether to return this segment.
:param recurse: If True (the default), recurse into SegmentSequenceField values, otherwise only look at segments in this SegmentSequence.
:param throw: If True, a FinTSNoResponseError is thrown if no result is found. Defaults to False.
The match results of all given parameters will be AND-combined.
"""
found_something = False
if query is None:
query = []
elif isinstance(query, str) or not isinstance(query, (list, tuple, Iterable)):
query = [query]
if version is None:
version = []
elif not isinstance(version, (list, tuple, Iterable)):
version = [version]
if callback is None:
callback = lambda s: True
for s in self.segments:
if ((not query) or any((isinstance(s, t) if isinstance(t, type) else s.header.type == t) for t in query)) and \
((not version) or any(s.header.version == v for v in version)) and \
callback(s):
yield s
found_something = True
if recurse:
for name, field in s._fields.items():
val = getattr(s, name)
if val and hasattr(val, 'find_segments'):
for v in val.find_segments(query=query, version=version, callback=callback, recurse=recurse):
yield v
found_something = True
if throw and not found_something:
raise FinTSNoResponseError(
'The bank\'s response did not contain a response to your request, please inspect debug log.'
)
def find_segment_first(self, *args, **kwargs):
"""Finds the first matching segment.
Same parameters as find_segments(), but only returns the first match, or None if no match is found."""
for m in self.find_segments(*args, **kwargs):
return m
return None
def find_segment_highest_version(self, query=None, version=None, callback=None, recurse=True, default=None):
"""Finds the highest matching segment.
Same parameters as find_segments(), but returns the match with the highest version, or default if no match is found."""
# FIXME Test
retval = None
for s in self.find_segments(query=query, version=version, callback=callback, recurse=recurse):
if not retval or s.header.version > retval.header.version:
retval = s
if retval is None:
return default
return retval
class ContainerMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
return OrderedDict()
def __new__(cls, name, bases, classdict):
retval = super().__new__(cls, name, bases, classdict)
retval._fields = OrderedDict()
for supercls in reversed(bases):
if hasattr(supercls, '_fields'):
retval._fields.update((k, v) for (k, v) in supercls._fields.items())
retval._fields.update((k, v) for (k, v) in classdict.items() if isinstance(v, Field))
return retval
class Container(metaclass=ContainerMeta):
def __init__(self, *args, **kwargs):
init_values = OrderedDict()
additional_data = kwargs.pop("_additional_data", [])
for init_value, field_name in zip(args, self._fields):
init_values[field_name] = init_value
args = ()
for field_name in self._fields:
if field_name in kwargs:
if field_name in init_values:
raise TypeError("__init__() got multiple values for argument {}".format(field_name))
init_values[field_name] = kwargs.pop(field_name)
super().__init__(*args, **kwargs)
self._values = {}
self._additional_data = additional_data
for k, v in init_values.items():
setattr(self, k, v)
@classmethod
def naive_parse(cls, data):
if data is None:
raise TypeError("No data provided")
retval = cls()
for ((name, field), value) in zip(retval._fields.items(), data):
setattr(retval, name, value)
return retval
def is_unset(self):
for name in self._fields.keys():
val = getattr(self, name)
if isinstance(val, Container):
if not val.is_unset():
return False
elif val is not None:
return False
return True
@property
def _repr_items(self):
for name, field in self._fields.items():
val = getattr(self, name)
if not field.required:
if isinstance(val, Container):
if val.is_unset():
continue
elif isinstance(val, ValueList):
if len(val) == 0:
continue
elif val is None:
continue
yield (name, val)
if self._additional_data:
yield ("_additional_data", self._additional_data)
def __repr__(self):
return "{}.{}({})".format(
self.__class__.__module__,
self.__class__.__name__,
", ".join(
"{}={!r}".format(name, val) for (name, val) in self._repr_items
)
)
def print_nested(self, stream=None, level=0, indent=" ", prefix="", first_level_indent=True, trailer="", print_doc=True, first_line_suffix=""):
"""Structured nested print of the object to the given stream.
The print-out is eval()able to reconstruct the object."""
import sys
stream = stream or sys.stdout
stream.write(
((prefix + level * indent) if first_level_indent else "")
+ "{}.{}(".format(self.__class__.__module__, self.__class__.__name__)
+ first_line_suffix
+ "\n"
)
for name, value in self._repr_items:
val = getattr(self, name)
if print_doc and not name.startswith("_"):
docstring = self._fields[name]._inline_doc_comment(val)
else:
docstring = ""
if not hasattr(getattr(val, 'print_nested', None), '__call__'):
stream.write(
(prefix + (level + 1) * indent) + "{} = {!r},{}\n".format(name, val, docstring)
)
else:
stream.write(
(prefix + (level + 1) * indent) + "{} = ".format(name)
)
val.print_nested(stream=stream, level=level + 2, indent=indent, prefix=prefix, first_level_indent=False, trailer=",", print_doc=print_doc,
first_line_suffix=docstring)
stream.write((prefix + level * indent) + "){}\n".format(trailer))
python-fints-4.0.0/fints/utils.py 0000664 0000000 0000000 00000026543 14421014606 0017022 0 ustar 00root root 0000000 0000000 import base64
import inspect
import json
import re
import zlib
from contextlib import contextmanager
from datetime import datetime
from enum import Enum
import mt940
from .models import Holding
def mt940_to_array(data):
data = data.replace("@@", "\r\n")
data = data.replace("-0000", "+0000")
transactions = mt940.models.Transactions()
return transactions.parse(data)
def classproperty(f):
class fx:
def __init__(self, getter):
self.getter = getter
def __get__(self, obj, type=None):
return self.getter(type)
return fx(f)
def compress_datablob(magic: bytes, version: int, data: dict):
data = dict(data)
for k, v in data.items():
if k.endswith("_bin"):
if v:
data[k] = base64.b64encode(v).decode("us-ascii")
serialized = json.dumps(data).encode('utf-8')
compressed = zlib.compress(serialized, 9)
return b';'.join([magic, b'1', str(version).encode('us-ascii'), compressed])
def decompress_datablob(magic: bytes, blob: bytes, obj: object = None):
if not blob.startswith(magic):
raise ValueError("Incorrect data blob")
s = blob.split(b';', 3)
if len(s) != 4:
raise ValueError("Incorrect data blob")
if not s[1].isdigit() or not s[2].isdigit():
raise ValueError("Incorrect data blob")
encoding_version = int(s[1].decode('us-ascii'), 10)
blob_version = int(s[2].decode('us-ascii'), 10)
if encoding_version != 1:
raise ValueError("Unsupported encoding version {}".format(encoding_version))
decompressed = zlib.decompress(s[3])
data = json.loads(decompressed.decode('utf-8'))
for k, v in data.items():
if k.endswith("_bin"):
if v:
data[k] = base64.b64decode(v.encode('us-ascii'))
if obj:
setfunc = getattr(obj, "_set_data_v{}".format(blob_version), None)
if not setfunc:
raise ValueError("Unknown data blob version")
setfunc(data)
else:
return blob_version, data
class SubclassesMixin:
@classmethod
def _all_subclasses(cls):
for subcls in cls.__subclasses__():
yield from subcls._all_subclasses()
yield cls
class DocTypeMixin:
_DOC_TYPE = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
type_ = self._DOC_TYPE
if type_ is None:
if isinstance(getattr(self, 'type', None), type):
type_ = getattr(self, 'type')
if type_ is not None:
if not self.__doc__:
self.__doc__ = ""
name = type_.__name__
if type_.__module__ != 'builtins':
name = "{}.{}".format(type_.__module__, name)
self.__doc__ = self.__doc__ + "\n\n:type: :class:`{}`".format(name)
class FieldRenderFormatStringMixin:
_FORMAT_STRING = None
def _render_value(self, value):
retval = self._FORMAT_STRING.format(value)
self._check_value_length(retval)
return retval
class FixedLengthMixin:
_FIXED_LENGTH = [None, None, None]
_DOC_TYPE = str
def __init__(self, *args, **kwargs):
for i, a in enumerate(('length', 'min_length', 'max_length')):
kwargs[a] = self._FIXED_LENGTH[i] if len(self._FIXED_LENGTH) > i else None
super().__init__(*args, **kwargs)
class ShortReprMixin:
def __repr__(self):
return "{}{}({})".format(
"{}.".format(self.__class__.__module__),
self.__class__.__name__,
", ".join(
("{!r}".format(value) if not name.startswith("_") else "{}={!r}".format(name, value))
for (name, value) in self._repr_items
)
)
def print_nested(self, stream=None, level=0, indent=" ", prefix="", first_level_indent=True, trailer="", print_doc=True, first_line_suffix=""):
stream.write(
( (prefix + level*indent) if first_level_indent else "")
+ "{!r}{}{}\n".format(self, trailer, first_line_suffix)
)
class MT535_Miniparser:
re_identification = re.compile(r"^:35B:ISIN\s(.*)\|(.*)\|(.*)$")
re_marketprice = re.compile(r"^:90B::MRKT\/\/ACTU\/([A-Z]{3})(\d*),{1}(\d*)$")
re_pricedate = re.compile(r"^:98A::PRIC\/\/(\d*)$")
re_pieces = re.compile(r"^:93B::AGGR\/\/UNIT\/(\d*),(\d*)$")
re_totalvalue = re.compile(r"^:19A::HOLD\/\/([A-Z]{3})(\d*),{1}(\d*)$")
re_acquisitionprice = re.compile(r"^:70E::HOLD\/\/\d*STK\|2(\d*?),{1}(\d*?)\+([A-Z]{3})$")
def parse(self, lines):
retval = []
# First: Collapse multiline clauses into one clause
clauses = self.collapse_multilines(lines)
# Second: Scan sequence of clauses for financial instrument
# sections
finsegs = self.grab_financial_instrument_segments(clauses)
# Third: Extract financial instrument data
for finseg in finsegs:
isin, name, market_price, price_symbol, price_date, pieces, acquisitionprice = (None,)*7
for clause in finseg:
# identification of instrument
# e.g. ':35B:ISIN LU0635178014|/DE/ETF127|COMS.-MSCI EM.M.T.U.ETF I'
m = self.re_identification.match(clause)
if m:
isin = m.group(1)
name = m.group(3)
# current market price
# e.g. ':90B::MRKT//ACTU/EUR38,82'
m = self.re_marketprice.match(clause)
if m:
price_symbol = m.group(1)
market_price = float(m.group(2) + "." + m.group(3))
# date of market price
# e.g. ':98A::PRIC//20170428'
m = self.re_pricedate.match(clause)
if m:
price_date = datetime.strptime(m.group(1), "%Y%m%d").date()
# number of pieces
# e.g. ':93B::AGGR//UNIT/16,8211'
m = self.re_pieces.match(clause)
if m:
pieces = float(m.group(1) + "." + m.group(2))
# total value of holding
# e.g. ':19A::HOLD//EUR970,17'
m = self.re_totalvalue.match(clause)
if m:
total_value = float(m.group(2) + "." + m.group(3))
# Acquisition price
# e.g ':70E::HOLD//1STK23,968293+EUR'
m = self.re_acquisitionprice.match(clause)
if m:
acquisitionprice = float(m.group(1) + '.' + m.group(2))
# processed all clauses
retval.append(
Holding(
ISIN=isin, name=name, market_value=market_price,
value_symbol=price_symbol, valuation_date=price_date,
pieces=pieces, total_value=total_value,
acquisitionprice=acquisitionprice))
return retval
def collapse_multilines(self, lines):
clauses = []
prevline = ""
for line in lines:
if line.startswith(":"):
if prevline != "":
clauses.append(prevline)
prevline = line
elif line.startswith("-"):
# last line
clauses.append(prevline)
clauses.append(line)
else:
prevline += "|{}".format(line)
return clauses
def grab_financial_instrument_segments(self, clauses):
retval = []
stack = []
within_financial_instrument = False
for clause in clauses:
if clause.startswith(":16R:FIN"):
# start of financial instrument
within_financial_instrument = True
elif clause.startswith(":16S:FIN"):
# end of financial instrument - move stack over to
# return value
retval.append(stack)
stack = []
within_financial_instrument = False
else:
if within_financial_instrument:
stack.append(clause)
return retval
class Password(str):
protected = False
def __init__(self, value):
self.value = value
self.blocked = False
@classmethod
@contextmanager
def protect(cls):
try:
cls.protected = True
yield None
finally:
cls.protected = False
def block(self):
self.blocked = True
def __str__(self):
if self.blocked and not self.protected:
raise Exception("Refusing to use PIN after block")
return '***' if self.protected else str(self.value)
def __repr__(self):
return self.__str__().__repr__()
def __add__(self, other):
return self.__str__().__add__(other)
def replace(self, *args, **kwargs):
return self.__str__().replace(*args, **kwargs)
class RepresentableEnum(Enum):
def __init__(self, *args, **kwargs):
Enum.__init__(self)
# Hack alert: Try to parse the docstring from the enum source, if available. Fail softly.
# FIXME Needs test
try:
val_1 = val_2 = repr(args[0])
if val_1.startswith("'"):
val_2 = '"' + val_1[1:-1] + '"'
elif val_1.startswith('"'):
val_2 = "'" + val_1[1:-1] + "'"
regex = re.compile(r"^.*?\S+\s*=\s*(?:(?:{})|(?:{}))\s*#:\s*(\S.*)$".format(
re.escape(val_1), re.escape(val_2)))
for line in inspect.getsourcelines(self.__class__)[0]:
m = regex.match(line)
if m:
self.__doc__ = m.group(1).strip()
break
except:
raise
def __repr__(self):
return "{}.{}.{}".format(self.__class__.__module__, self.__class__.__name__, self.name)
def __str__(self):
return self.value
def minimal_interactive_cli_bootstrap(client):
"""
This is something you usually implement yourself to ask your user in a nice, user-friendly way about these things.
This is mainly included to keep examples in the documentation simple and allow you to get started quickly.
"""
# Fetch available TAN mechanisms by the bank, if we don't know it already. If the client was created with cached data,
# the function is already set.
if not client.get_current_tan_mechanism():
client.fetch_tan_mechanisms()
mechanisms = list(client.get_tan_mechanisms().items())
if len(mechanisms) > 1:
print("Multiple tan mechanisms available. Which one do you prefer?")
for i, m in enumerate(mechanisms):
print(i, "Function {p.security_function}: {p.name}".format(p=m[1]))
choice = input("Choice: ").strip()
client.set_tan_mechanism(mechanisms[int(choice)][0])
if client.is_tan_media_required() and not client.selected_tan_medium:
print("We need the name of the TAN medium, let's fetch them from the bank")
m = client.get_tan_media()
if len(m[1]) == 1:
client.set_tan_medium(m[1][0])
else:
print("Multiple tan media available. Which one do you prefer?")
for i, mm in enumerate(m[1]):
print(i,
"Medium {p.tan_medium_name}: Phone no. {p.mobile_number_masked}, Last used {p.last_use}".format(
p=mm))
choice = input("Choice: ").strip()
client.set_tan_medium(m[1][int(choice)])
python-fints-4.0.0/requirements.txt 0000664 0000000 0000000 00000000046 14421014606 0017437 0 ustar 00root root 0000000 0000000 requests
mt-940
sepaxml==2.1.*
bleach
python-fints-4.0.0/setup.cfg 0000664 0000000 0000000 00000000316 14421014606 0015774 0 ustar 00root root 0000000 0000000 [flake8]
max-line-length = 160
[coverage:run]
source = fints
[coverage:report]
exclude_lines =
pragma: no cover
def __str__
der __repr__
if settings.DEBUG
NOQA
NotImplementedError
python-fints-4.0.0/setup.py 0000664 0000000 0000000 00000002656 14421014606 0015676 0 ustar 00root root 0000000 0000000 from codecs import open
from os import path
from fints import version
from setuptools import find_packages, setup
here = path.abspath(path.dirname(__file__))
try:
# Get the long description from the relevant file
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
except:
long_description = ''
setup(
name='fints',
version=version,
description='Pure-python FinTS 3.0 (formerly known as HBCI) implementation',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/raphaelm/python-fints',
author='Raphael Michel',
author_email='mail@raphaelmichel.de',
license='GNU Lesser General Public License v3 (LGPLv3)',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Other Audience',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
],
keywords='hbci banking fints',
install_requires=[
'bleach',
'mt-940',
'requests',
'sepaxml~=2.1',
],
packages=find_packages(include=['fints', 'fints.*']),
)
python-fints-4.0.0/tests/ 0000775 0000000 0000000 00000000000 14421014606 0015315 5 ustar 00root root 0000000 0000000 python-fints-4.0.0/tests/conftest.py 0000664 0000000 0000000 00000036030 14421014606 0017516 0 ustar 00root root 0000000 0000000 import glob
import os.path
import pytest
import http.server
import threading
import base64
import uuid
import re
import random
import fints.parser
from fints.types import SegmentSequence
from fints.segments.message import HNHBK3, HNVSK3, HNVSD1, HNHBS1
from fints.formals import SecurityProfile, SecurityIdentificationDetails, SecurityDateTime, EncryptionAlgorithm, KeyName, BankIdentifier
TEST_MESSAGES = {
os.path.basename(f).rsplit('.')[0]: open(f, 'rb').read() for f in
glob.glob(os.path.join(os.path.dirname(__file__), "messages", "*.bin"))
}
# We will turn off robust mode generally for tests
fints.parser.robust_mode = False
@pytest.fixture(scope="session")
def fints_server():
dialog_prefix = base64.b64encode(uuid.uuid4().bytes, altchars=b'_/').decode('us-ascii')
system_prefix = base64.b64encode(uuid.uuid4().bytes, altchars=b'_/').decode('us-ascii')
dialogs = {}
systems = {}
class FinTSHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def make_answer(self, dialog_id, message):
datadict = dialogs[dialog_id]
pin = None
tan = None
pinmatch = re.search(rb"HNSHA:\d+:\d+\+[^+]*\+[^+]*\+([^:+?']+)(?::([^:+?']+))?'", message)
if pinmatch:
pin = pinmatch.group(1).decode('us-ascii')
if pinmatch.group(2):
tan = pinmatch.group(2).decode('us-ascii')
if pin not in ('1234', '3938'):
return "HIRMG::2+9910::Pin ungültig'".encode('utf-8')
result = []
result.append(b"HIRMG::2+0010::Nachricht entgegengenommen'")
hkvvb = re.search(rb"'HKVVB:(\d+):3\+(\d+)\+(\d+)", message)
if hkvvb:
responses = [hkvvb.group(1)]
segments = []
if hkvvb.group(2) != b'78':
responses.append(b'3050::BPD nicht mehr aktuell, aktuelle Version enthalten.')
segments.append("HIBPA:6:3:4+78+280:12345678+Test Bank+1+1+300+500'HIKOM:7:4:4+280:12345678+1+3:http?://{host}?:{port}/'HISHV:8:3:4+J+RDH:3+PIN:1+RDH:9+RDH:10+RDH:7'HIEKAS:9:5:4+1+1+1+J:J:N:3'HIKAZS:10:4:4+1+1+365:J'HIKAZS:11:5:4+1+1+365:J:N'HIKAZS:12:6:4+1+1+1+365:J:N'HIKAZS:13:7:4+1+1+1+365:J:N'HIPPDS:14:1:4+1+1+1+1:Telekom:prepaid:N:::15;30;50:2:Vodafone:prepaid:N:::15;25;50:3:E-Plus:prepaid:N:::15;20;30:4:O2:prepaid:N:::15;20;30:5:Congstar:prepaid:N:::15;30;50:6:Blau:prepaid:N:::15;20;30'HIPAES:15:1:4+1+1+1'HIPROS:16:3:4+1+1'HIPSPS:17:1:4+1+1+1'HIQTGS:18:1:4+1+1+1'HISALS:19:5:4+3+1'HISALS:20:7:4+1+1+1'HISLAS:21:4:4+1+1+500:14:04:05'HICSBS:22:1:4+1+1+1+N:N'HICSLS:23:1:4+1+1+1+J'HICSES:24:1:4+1+1+1+1:400'HISUBS:25:4:4+1+1+500:14:51:53:54:56:67:68:69'HITUAS:26:2:4+1+1+1:400:14:51:53:54:56:67:68:69'HITUBS:27:1:4+1+1+J'HITUES:28:2:4+1+1+1:400:14:51:53:54:56:67:68:69'HITULS:29:1:4+1+1'HICCSS:30:1:4+1+1+1'HISPAS:31:1:4+1+1+1+J:J:N:sepade?:xsd?:pain.001.001.02.xsd:sepade?:xsd?:pain.001.002.02.xsd:sepade?:xsd?:pain.001.002.03.xsd:sepade?:xsd?:pain.001.003.03.xsd:sepade?:xsd?:pain.008.002.02.xsd:sepade?:xsd?:pain.008.003.02.xsd'HICCMS:32:1:4+1+1+1+500:N:N'HIDSES:33:1:4+1+1+1+3:45:6:45'HIBSES:34:1:4+1+1+1+2:45:2:45'HIDMES:35:1:4+1+1+1+3:45:6:45:500:N:N'HIBMES:36:1:4+1+1+1+2:45:2:45:500:N:N'HIUEBS:37:3:4+1+1+14:51:53:54:56:67:68:69'HIUMBS:38:1:4+1+1+14:51'HICDBS:39:1:4+1+1+1+N'HICDLS:40:1:4+1+1+1+0:0:N:J'HIPPDS:41:2:4+1+1+1+1:Telekom:prepaid:N:::15;30;50:2:Vodafone:prepaid:N:::15;25;50:3:E-plus:prepaid:N:::15;20;30:4:O2:prepaid:N:::15;20;30:5:Congstar:prepaid:N:::15;30;50:6:Blau:prepaid:N:::15;20;30'HICDNS:42:1:4+1+1+1+0:1:3650:J:J:J:J:N:J:J:J:J:0000:0000'HIDSBS:43:1:4+1+1+1+N:N:9999'HICUBS:44:1:4+1+1+1+N'HICUMS:45:1:4+1+1+1+OTHR'HICDES:46:1:4+1+1+1+4:1:3650:000:0000'HIDSWS:47:1:4+1+1+1+J'HIDMCS:48:1:4+1+1+1+500:N:N:2:45:2:45::sepade?:xsd?:pain.008.003.02.xsd'HIDSCS:49:1:4+1+1+1+2:45:2:45::sepade?:xsd?:pain.008.003.02.xsd'HIECAS:50:1:4+1+1+1+J:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.053.001.02'GIVPUS:51:1:4+1+1+1+N'GIVPDS:52:1:4+1+1+1+1'HITANS:53:5:4+1+1+1+J:N:0:942:2:MTAN2:mobileTAN::mobile TAN:6:1:SMS:3:1:J:1:0:N:0:2:N:J:00:1:1:962:2:HHD1.4:HHD:1.4:Smart-TAN plus manuell:6:1:Challenge:3:1:J:1:0:N:0:2:N:J:00:1:1:972:2:HHD1.4OPT:HHDOPT1:1.4:Smart-TAN plus optisch:6:1:Challenge:3:1:J:1:0:N:0:2:N:J:00:1:1'HIPINS:54:1:4+1+1+1+5:20:6:Benutzer ID::HKSPA:N:HKKAZ:N:HKKAZ:N:HKSAL:N:HKSLA:J:HKSUB:J:HKTUA:J:HKTUB:N:HKTUE:J:HKTUL:J:HKUEB:J:HKUMB:J:HKPRO:N:HKEKA:N:HKKAZ:N:HKKAZ:N:HKPPD:J:HKPAE:J:HKPSP:N:HKQTG:N:HKSAL:N:HKCSB:N:HKCSL:J:HKCSE:J:HKCCS:J:HKCCM:J:HKDSE:J:HKBSE:J:HKDME:J:HKBME:J:HKCDB:N:HKCDL:J:HKPPD:J:HKCDN:J:HKDSB:N:HKCUB:N:HKCUM:J:HKCDE:J:HKDSW:J:HKDMC:J:HKDSC:J:HKECA:N:GKVPU:N:GKVPD:N:HKTAN:N:HKTAN:N'HIAZSS:55:1:4+1+1+1+1:N:::::::::::HKTUA;2;0;1;811:HKDSC;1;0;1;811:HKPPD;2;0;1;811:HKDSE;1;0;1;811:HKSLA;4;0;1;811:HKTUE;2;0;1;811:HKSUB;4;0;1;811:HKCDL;1;0;1;811:HKCDB;1;0;1;811:HKKAZ;6;0;1;811:HKCSE;1;0;1;811:HKSAL;4;0;1;811:HKQTG;1;0;1;811:GKVPU;1;0;1;811:HKUMB;1;0;1;811:HKECA;1;0;1;811:HKDMC;1;0;1;811:HKDME;1;0;1;811:HKSAL;7;0;1;811:HKSPA;1;0;1;811:HKEKA;5;0;1;811:HKKAZ;4;0;1;811:HKPSP;1;0;1;811:HKKAZ;5;0;1;811:HKCSL;1;0;1;811:HKCDN;1;0;1;811:HKTUL;1;0;1;811:HKPPD;1;0;1;811:HKPAE;1;0;1;811:HKCCM;1;0;1;811:HKIDN;2;0;1;811:HKDSW;1;0;1;811:HKCUM;1;0;1;811:HKPRO;3;0;1;811:GKVPD;1;0;1;811:HKCDE;1;0;1;811:HKBSE;1;0;1;811:HKCSB;1;0;1;811:HKCCS;1;0;1;811:HKDSB;1;0;1;811:HKBME;1;0;1;811:HKCUB;1;0;1;811:HKUEB;3;0;1;811:HKTUB;1;0;1;811:HKKAZ;7;0;1;811'HIVISS:56:1:4+1+1+1+1;;;;'".format(host=server.server_address[0], port=server.server_address[1]).encode('us-ascii'))
if hkvvb.group(3) != b'3':
responses.append(b'3050::UPD nicht mehr aktuell, aktuelle Version enthalten.')
segments.append(b"HIUPA:57:4:4+test1+3+0'HIUPD:58:6:4+1::280:12345678+DE111234567800000001+test1++EUR+Fullname++Girokonto++HKSAK:1+HKISA:1+HKSSP:1+HKSAL:1+HKKAZ:1+HKEKA:1+HKCDB:1+HKPSP:1+HKCSL:1+HKCDL:1+HKPAE:1+HKPPD:1+HKCDN:1+HKCSB:1+HKCUB:1+HKQTG:1+HKSPA:1+HKDSB:1+HKCCM:1+HKCUM:1+HKCCS:1+HKCDE:1+HKCSE:1+HKDSW:1+HKPRO:1+HKSAL:1+HKKAZ:1+HKTUL:1+HKTUB:1+HKPRO:1+GKVPU:1+GKVPD:1'HIUPD:59:6:4+2::280:12345678+DE111234567800000002+test1++EUR+Fullname++Tagesgeld++HKSAK:1+HKISA:1+HKSSP:1+HKSAL:1+HKKAZ:1+HKEKA:1+HKPSP:1+HKCSL:1+HKPAE:1+HKCSB:1+HKCUB:1+HKQTG:1+HKSPA:1+HKCUM:1+HKCCS:1+HKCSE:1+HKPRO:1+HKSAL:1+HKKAZ:1+HKTUL:1+HKTUB:1+HKPRO:1+GKVPU:1+GKVPD:1'")
if pin == '3938':
responses.append(b'3938::Ihr Zugang ist vorl\u00e4ufig gesperrt - Bitte PIN-Sperre aufheben.')
else:
responses.append(b'3920::Zugelassene TAN-Verfahren fur den Benutzer:942')
responses.append(b'0901::*PIN gultig.')
responses.append(b'0020::*Dialoginitialisierung erfolgreich')
result.append(b"HIRMS::2:"+b"+".join(responses)+b"'")
result.extend(segments)
if b"'HKSYN:" in message:
system_id = "{};{:05d}".format(system_prefix, len(systems)+1)
systems[system_id] = {}
result.append("HISYN::4:5+{}'".format(system_id).encode('us-ascii'))
if b"'HKSPA:" in message:
result.append(b"HISPA::1:4+J:DE111234567800000001:GENODE23X42:00001::280:1234567890'")
hkkaz = re.search(rb"'HKKAZ:(\d+):7\+[^+]+\+N(?:\+[^+]*\+[^+]*\+[^+]*\+([^+]*))?'", message)
if hkkaz:
if hkkaz.group(2):
startat = int(hkkaz.group(2).decode('us-ascii'), 10)
else:
startat = 0
transactions = [
[
b'-',
b':20:STARTUMS',
b':25:12345678/0000000001',
b':28C:0',
b':60F:C150101EUR1041,23',
b':61:150101C182,34NMSCNONREF',
b':86:051?00UEBERWEISG?10931?20Ihre Kontonummer 0000001234',
b'?21/Test Ueberweisung 1?22n WS EREF: 1100011011 IBAN:',
b'?23 DE1100000100000001234 BIC?24: GENODE11 ?1011010100',
b'?31?32Bank',
b':62F:C150101EUR1223,57',
b'-',
], [
b'-',
b':20:STARTUMS',
b':25:12345678/0000000001',
b':28C:0',
b':60F:C150301EUR1223,57',
b':61:150301C100,03NMSCNONREF',
b':86:051?00UEBERWEISG?10931?20Ihre Kontonummer 0000001234',
b'?21/Test Ueberweisung 2?22n WS EREF: 1100011011 IBAN:',
b'?23 DE1100000100000001234 BIC?24: GENODE11 ?1011010100',
b'?31?32Bank',
b':61:150301C100,00NMSCNONREF',
b':86:051?00UEBERWEISG?10931?20Ihre Kontonummer 0000001234',
b'?21/Test Ueberweisung 3?22n WS EREF: 1100011011 IBAN:',
b'?23 DE1100000100000001234 BIC?24: GENODE11 ?1011010100',
b'?31?32Bank',
b':62F:C150101EUR1423,60',
b'-',
]
]
if startat+1 < len(transactions):
result.append("HIRMS::2:{}+3040::Es liegen weitere Informationen vor: {}'".format(hkkaz.group(1).decode('us-ascii'), startat+1).encode('iso-8859-1'))
tx = b"\r\n".join([b''] + transactions[startat] + [b''])
result.append("HIKAZ::7:{}+@{}@".format(hkkaz.group(1).decode('us-ascii'), len(tx)).encode('us-ascii') + tx + b"'")
hkccs = re.search(rb"'HKCCS:(\d+):1.*@\d+@(.*)/Document>'", message)
if hkccs:
segno = hkccs.group(1).decode('us-ascii')
pain = hkccs.group(2).decode('utf-8')
memomatch = re.search(r"]*>\s*]*>\s*([^<]+)\s*]*>\s*]*>\s*]*>\s*([^<]+)\s*]*>]*>\s*([^<]+)\s*