pax_global_header00006660000000000000000000000064144210146060014510gustar00rootroot0000000000000052 comment=5989160fb74ffee3f87119aa4db16391f644267d python-fints-4.0.0/000077500000000000000000000000001442101460600141535ustar00rootroot00000000000000python-fints-4.0.0/.github/000077500000000000000000000000001442101460600155135ustar00rootroot00000000000000python-fints-4.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001442101460600176765ustar00rootroot00000000000000python-fints-4.0.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012031442101460600223640ustar00rootroot00000000000000--- 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/000077500000000000000000000000001442101460600175505ustar00rootroot00000000000000python-fints-4.0.0/.github/workflows/tests.yml000066400000000000000000000014731442101460600214420ustar00rootroot00000000000000name: 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: codecovpython-fints-4.0.0/.gitignore000066400000000000000000000001411442101460600161370ustar00rootroot00000000000000__pycache__/ build/ dist/ *.egg-info env .idea/ test*.py !tests/* tests/messages/private_* *.pyc python-fints-4.0.0/LICENSE.txt000066400000000000000000000167431442101460600160110ustar00rootroot00000000000000 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.in000066400000000000000000000000671442101460600157140ustar00rootroot00000000000000recursive-include tests *.py *.bin include LICENSE.txt python-fints-4.0.0/README.md000066400000000000000000000030261442101460600154330ustar00rootroot00000000000000PyFinTS ======= 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/000077500000000000000000000000001442101460600151035ustar00rootroot00000000000000python-fints-4.0.0/docs/.gitignore000066400000000000000000000000101442101460600170620ustar00rootroot00000000000000_build/ python-fints-4.0.0/docs/Makefile000066400000000000000000000173231442101460600165510ustar00rootroot00000000000000# 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.rst000066400000000000000000000060731442101460600171210ustar00rootroot00000000000000.. _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.py000066400000000000000000000302011442101460600163760ustar00rootroot00000000000000#!/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.rst000066400000000000000000000045501442101460600171130ustar00rootroot00000000000000.. _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/000077500000000000000000000000001442101460600170705ustar00rootroot00000000000000python-fints-4.0.0/docs/developer/index.rst000066400000000000000000000004611442101460600207320ustar00rootroot00000000000000Developer 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.rst000066400000000000000000000102761442101460600212730ustar00rootroot00000000000000Parsing 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/000077500000000000000000000000001442101460600207155ustar00rootroot00000000000000python-fints-4.0.0/docs/developer/segments/all.rst000066400000000000000000000057721442101460600222320ustar00rootroot00000000000000All 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.rst000066400000000000000000000043261442101460600225630ustar00rootroot00000000000000FinTS 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.rst000066400000000000000000000005001442101460600214250ustar00rootroot00000000000000FinTS 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.rst000066400000000000000000000011061442101460600207640ustar00rootroot00000000000000Defining 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.rst000066400000000000000000000073731442101460600213140ustar00rootroot00000000000000Working 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.rst000066400000000000000000000012071442101460600167440ustar00rootroot00000000000000FinTS 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.bat000066400000000000000000000165141442101460600165170ustar00rootroot00000000000000@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.rst000066400000000000000000000046361442101460600200400ustar00rootroot00000000000000Getting 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.htmpython-fints-4.0.0/docs/reading.rst000066400000000000000000000043211442101460600172460ustar00rootroot00000000000000Reading 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.rst000066400000000000000000000136001442101460600166020ustar00rootroot00000000000000.. _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.rst000066400000000000000000000055601442101460600171330ustar00rootroot00000000000000Tested 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.rst000066400000000000000000000035601442101460600176500ustar00rootroot00000000000000.. _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.rst000066400000000000000000000175521442101460600173230ustar00rootroot00000000000000Troubleshooting 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.rst000066400000000000000000000040051442101460600202550ustar00rootroot00000000000000Upgrading 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.rst000066400000000000000000000016031442101460600202600ustar00rootroot00000000000000Upgrading 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.rst000066400000000000000000000012301442101460600202560ustar00rootroot00000000000000Upgrading 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/000077500000000000000000000000001442101460600152765ustar00rootroot00000000000000python-fints-4.0.0/fints/__init__.py000066400000000000000000000000221442101460600174010ustar00rootroot00000000000000version = '4.0.0' python-fints-4.0.0/fints/client.py000066400000000000000000001606111442101460600171330ustar00rootroot00000000000000import 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.py000066400000000000000000000025011442101460600200050ustar00rootroot00000000000000import 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.py000066400000000000000000000212021442101460600171040ustar00rootroot00000000000000import 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.py000066400000000000000000000012051442101460600200270ustar00rootroot00000000000000class 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.py000066400000000000000000000205151442101460600171210ustar00rootroot00000000000000import 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.py000066400000000000000000001375471442101460600173340ustar00rootroot00000000000000import 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/000077500000000000000000000000001442101460600160415ustar00rootroot00000000000000python-fints-4.0.0/fints/hhd/__init__.py000066400000000000000000000000001442101460600201400ustar00rootroot00000000000000python-fints-4.0.0/fints/hhd/flicker.py000066400000000000000000000202301442101460600200270ustar00rootroot00000000000000# 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.py000066400000000000000000000027471442101460600173060ustar00rootroot00000000000000from 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.py000066400000000000000000000005011442101460600171270ustar00rootroot00000000000000from 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.py000066400000000000000000000375431442101460600171600ustar00rootroot00000000000000import 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.py000066400000000000000000000140301442101460600175150ustar00rootroot00000000000000import 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/000077500000000000000000000000001442101460600171235ustar00rootroot00000000000000python-fints-4.0.0/fints/segments/__init__.py000066400000000000000000000000001442101460600212220ustar00rootroot00000000000000python-fints-4.0.0/fints/segments/accounts.py000066400000000000000000000021741442101460600213200ustar00rootroot00000000000000from ..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.py000066400000000000000000000313121442101460600204360ustar00rootroot00000000000000from 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.py000066400000000000000000000100311442101460600204030ustar00rootroot00000000000000from 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.py000066400000000000000000000051321442101460600204100ustar00rootroot00000000000000import 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.py000066400000000000000000000307761442101460600206010ustar00rootroot00000000000000from ..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.py000066400000000000000000000036321442101460600206140ustar00rootroot00000000000000from 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.py000066400000000000000000000027201442101460600207350ustar00rootroot00000000000000from ..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.py000066400000000000000000000051611442101460600211520ustar00rootroot00000000000000from 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.py000066400000000000000000000102471442101460600211250ustar00rootroot00000000000000from 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.py000066400000000000000000000124151442101460600206020ustar00rootroot00000000000000from 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.py000066400000000000000000000134611442101460600215060ustar00rootroot00000000000000from 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.py000066400000000000000000000077361442101460600213360ustar00rootroot00000000000000from 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.py000066400000000000000000000406441442101460600170240ustar00rootroot00000000000000from 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.py000066400000000000000000000265431442101460600170220ustar00rootroot00000000000000import 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.txt000066400000000000000000000000461442101460600174370ustar00rootroot00000000000000requests mt-940 sepaxml==2.1.* bleach python-fints-4.0.0/setup.cfg000066400000000000000000000003161442101460600157740ustar00rootroot00000000000000[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.py000066400000000000000000000026561442101460600156760ustar00rootroot00000000000000from 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/000077500000000000000000000000001442101460600153155ustar00rootroot00000000000000python-fints-4.0.0/tests/conftest.py000066400000000000000000000360301442101460600175160ustar00rootroot00000000000000import 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*