From: Henryk Plötz Date: Sun, 16 Sep 2018 00:25:02 +0000 (+0200) Subject: Re-shuffle docs, document new client stuff X-Git-Tag: v2.0.0~1^2~33 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3049f29030d46ef706d4af9542fb51be54b1bb54;p=thirdparty%2Fpython-fints.git Re-shuffle docs, document new client stuff --- diff --git a/docs/client.rst b/docs/client.rst new file mode 100644 index 0000000..9253c38 --- /dev/null +++ b/docs/client.rst @@ -0,0 +1,78 @@ +.. _client: + +The client object +================= + +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: __init__, get_data, set_data + :noindex: + :undoc-members: + +Using the :func:`~fints.client.FinTS3Client.get_data`/: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.get_data` 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.get_data`. + + +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(..., set_data=datablob) + + with client: + accounts = client.get_sepa_accounts() + balance = client.get_balance(accounts[0]) + transactions = client.get_transactions(accounts[0]) + + datablob = client.get_data() + # 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. + + +Storing and restoring dialog state +---------------------------------- + +.. autoclass:: fints.client.FinTS3Client + :members: pause_dialog, resume_dialog + :noindex: + :undoc-members: + + diff --git a/docs/conf.py b/docs/conf.py index d3afa14..31bd5bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -385,4 +385,4 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {'https://docs.python.org/': None, 'https://mt940.readthedocs.io/en/latest/': None, } diff --git a/docs/developer.rst b/docs/developer.rst deleted file mode 100644 index e887237..0000000 --- a/docs/developer.rst +++ /dev/null @@ -1,209 +0,0 @@ -Developer documentation/API -=========================== - -Parsing and serialization -------------------------- - -.. autoclass:: fints.parser.FinTS3Parser - :members: - -.. autoclass:: fints.parser.FinTS3Serializer - :members: - -Example usage: - -.. code-block:: python - - >>> message = (b'HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043' - ... b"999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+" - ... b"2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:" - ... b'4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16' - ... b"+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100:" - ... b":Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'") - >>> from fints.parser import FinTS3Parser - >>> s = FinTS3Parser().parse_message(message) - >>> s - SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3), message_size='000000000428', hbci_version=300, dialogue_id='430711670077=043999659571CN9D=', message_number=2, reference_message=fints.formals.ReferenceMessage(dialogue_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. - -FinTS Segment Sequence ----------------------- - -A message is a sequence of segments. The :class:`~fints.formals.SegmentSequence` object allows searching for segments by type and version, by default recursing into nested sequences. - -.. autoclass:: fints.types.SegmentSequence - :members: - :undoc-members: print_nested - - -FinTS Segments --------------- -A segment is the core communication workhorse in FinTS. Each segment has a header of fixed format, which includes the segment type ("Segmentkennung"), number within the message, version, and, optionally, the number of the segment of another message it is in response or relation to ("Bezugssegment"). - -The header is followed by a nested structure of fields and groups of fields, the exact specification of which depends on the segment type and version. - -All segment classes derive from :class:`~fints.segments.FinTS3Segment`, which specifies the ``header`` attribute of :class:`~fints.formals.SegmentHeader` type. - -.. autoclass:: fints.segments.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. - -.. autoclass:: fints.formals.SegmentHeader - :members: - :member-order: bysource - -The :class:`~fints.segments.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 and 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 yield: - -.. code-block:: python - - fints.segments.HNHBS1( - header = fints.formals.SegmentHeader('HNHBS', 4, 1), - message_number = 1, - ) - -Working with Segments -~~~~~~~~~~~~~~~~~~~~~ - -Objects of :class:`~fints.segments.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.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. - -**FIXME** The ``number`` should in the future be generated automatically within in sequence (at least before serializing). - -.. 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, - ), - ], - ) - - -All Segments -____________ - -.. automodule:: fints.segments - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: print_nested, naive_parse, find_subclass, is_unset - :member-order: bysource - - -Defining new Segment classes ----------------------------- - -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 - :member-order: bysource - - diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 0000000..6404e90 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,14 @@ +Developer documentation/API +=========================== + +This part of the documentation is for you if you want to improve python-fints, but also if you just want to look behind the curtain. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + parsing + segments/index + sequence + working + types diff --git a/docs/developer/parsing.rst b/docs/developer/parsing.rst new file mode 100644 index 0000000..74fc71b --- /dev/null +++ b/docs/developer/parsing.rst @@ -0,0 +1,30 @@ +Parsing and serialization +------------------------- + +.. autoclass:: fints.parser.FinTS3Parser + :members: + +.. autoclass:: fints.parser.FinTS3Serializer + :members: + +Example usage: + +.. code-block:: python + + >>> message = (b'HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043' + ... b"999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+" + ... b"2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:" + ... b'4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16' + ... b"+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100:" + ... b":Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'") + >>> from fints.parser import FinTS3Parser + >>> s = FinTS3Parser().parse_message(message) + >>> s + SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3), message_size='000000000428', hbci_version=300, dialogue_id='430711670077=043999659571CN9D=', message_number=2, reference_message=fints.formals.ReferenceMessage(dialogue_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. diff --git a/docs/developer/segments/all.rst b/docs/developer/segments/all.rst new file mode 100644 index 0000000..c5ff5bf --- /dev/null +++ b/docs/developer/segments/all.rst @@ -0,0 +1,123 @@ +All Segments +____________ + + +fints.segments.accounts module +------------------------------ + +.. automodule:: fints.segments.accounts + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.auth module +-------------------------- + +.. automodule:: fints.segments.auth + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.bank module +-------------------------- + +.. automodule:: fints.segments.bank + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.base module +-------------------------- + +.. automodule:: fints.segments.base + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset, FinTS3Segment + +fints.segments.debit module +--------------------------- + +.. automodule:: fints.segments.debit + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.depot module +--------------------------- + +.. automodule:: fints.segments.depot + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.dialog module +---------------------------- + +.. automodule:: fints.segments.dialog + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.journal module +----------------------------- + +.. automodule:: fints.segments.journal + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.message module +----------------------------- + +.. automodule:: fints.segments.message + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.saldo module +--------------------------- + +.. automodule:: fints.segments.saldo + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.statement module +------------------------------- + +.. automodule:: fints.segments.statement + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset + +fints.segments.transfer module +------------------------------ + +.. automodule:: fints.segments.transfer + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: print_nested, naive_parse, find_subclass, is_unset diff --git a/docs/developer/segments/index.rst b/docs/developer/segments/index.rst new file mode 100644 index 0000000..102769a --- /dev/null +++ b/docs/developer/segments/index.rst @@ -0,0 +1,47 @@ +FinTS Segments +-------------- +A segment is the core communication workhorse in FinTS. Each segment has a header of fixed format, which includes the segment type ("Segmentkennung"), number within the message, version, and, optionally, the number of the segment of another message it is in response or relation to ("Bezugssegment"). + +The header is followed by a nested structure of fields and groups of fields, the exact specification of which depends on the segment type and version. + +All segment classes derive from :class:`~fints.segments.base.FinTS3Segment`, which specifies the ``header`` attribute of :class:`~fints.formals.SegmentHeader` type. + +.. autoclass:: fints.segments.base.FinTS3Segment + :members: + :inherited-members: print_nested + :member-order: bysource + + .. attribute:: TYPE + + Segment type. Will be determined from the class name in subclasses, if the class name consists only of uppercase characters followed by decimal digits. Subclasses may explicitly set a class attribute instead. + + .. attribute:: VERSION + + Segment version. Will be determined from the class name in subclasses, if the class name consists only of uppercase characters followed by decimal digits. Subclasses may explicitly set a class attribute instead. + + .. classmethod:: find_subclass(segment: list) + + Parse the given ``segment`` parameter as a :class:`~fints.formals.SegmentHeader` and return a subclass with matching type and version class attributes. + +The :class:`~fints.segments.base.FinTS3Segment` class and its base classes employ a number of dynamic programming techniques so that derived classes need only specify the name, order and type of fields. All type conversion, construction etc. will take place automatically. All derived classes basically should behave "as expected", returning only native Python datatypes. + +Consider this example segment class: + +.. code-block:: python + + class HNHBS1(FinTS3Segment): + message_number = DataElementField(type='num', max_length=4) + +Calling ``print_nested`` on an instance of this class might output: + +.. code-block:: python + + fints.segments.HNHBS1( + header = fints.formals.SegmentHeader('HNHBS', 4, 1), + message_number = 1, + ) + +.. toctree:: + :maxdepth: 2 + + all diff --git a/docs/developer/sequence.rst b/docs/developer/sequence.rst new file mode 100644 index 0000000..318de0b --- /dev/null +++ b/docs/developer/sequence.rst @@ -0,0 +1,9 @@ +FinTS Segment Sequence +---------------------- + +A message is a sequence of segments. The :class:`~fints.formals.SegmentSequence` object allows searching for segments by type and version, by default recursing into nested sequences. + +.. autoclass:: fints.types.SegmentSequence + :members: + :undoc-members: print_nested + diff --git a/docs/developer/types.rst b/docs/developer/types.rst new file mode 100644 index 0000000..419567d --- /dev/null +++ b/docs/developer/types.rst @@ -0,0 +1,33 @@ +Defining new Segment classes +---------------------------- + +Base types +~~~~~~~~~~~ + +.. automodule:: fints.types + :members: + :undoc-members: + :exclude-members: print_nested, SegmentSequence + :member-order: bysource + + +Field types +~~~~~~~~~~~ + +.. automodule:: fints.fields + :members: + :undoc-members: + :exclude-members: print_nested + :member-order: bysource + + +Constructed and helper types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: fints.formals + :members: + :undoc-members: + :exclude-members: print_nested SegmentHeader + :member-order: bysource + + diff --git a/docs/developer/working.rst b/docs/developer/working.rst new file mode 100644 index 0000000..ac521ec --- /dev/null +++ b/docs/developer/working.rst @@ -0,0 +1,81 @@ +Working with Segments +~~~~~~~~~~~~~~~~~~~~~ + +Objects of :class:`~fints.segments.base.FinTS3Segment` or a subclass can be created by calling their constructor. The constructor takes optional arguments for all fields of the class. Setting and getting fields and subfields works, and consumes and returns Python objects as appropriate: + +.. code-block:: python + + >>> from fints.segments import HNHBS1 + >>> s = HNHBS1() + >>> s + fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', None, 1), message_number=None) + >>> s.header.number = 3 + >>> s.header + fints.formals.SegmentHeader('HNHBS', 3, 1) + +When setting a value, format and length restrictions will be checked, if possible: + +.. code-block:: python + + >>> s.message_number = 'abc' + ValueError: invalid literal for int() with base 10: 'abc' + >>> s.message_number = 12345 + ValueError: Value '12345' cannot be rendered: max_length=4 exceeded + +The only exception is: Every field can be set to ``None`` in order to clear the field and make it unset, recursively. No checking is performed whether all fields that are required (or conditionally required) by the specification are set. For convenience, an unset constructed field will still be filled with an instance of the field's value type, so that subfield accessing will always work, without encountering ``None`` values on the way. + +.. code-block:: python + + >>> s.header = None + >>> s + fints.segments.HNHBS1(header=fints.formals.SegmentHeader(None, None, None), message_number=None) + +When calling the constructor with non-keyword arguments, fields are assigned in order, with the exception of ``header`` in :class:`~fints.segments.base.FinTS3Segment` subclasses, which can only be given as a keyword argument. When no ``header`` argument is present, a :class:`~fints.formals.SegmentHeader` is automatically constructed with default values (and no ``number``). It's generally not required to construct the ``header`` parameter manually. + +**FIXME** The ``number`` should in the future be generated automatically within in sequence (at least before serializing). + +.. 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, + ), + ], + ) diff --git a/docs/index.rst b/docs/index.rst index 165e5dc..12c466e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,16 +7,24 @@ FinTS client library This is a pure-python implementation of FinTS (formerly known as HBCI), a online-banking protocol commonly supported by German banks. -Documentation content ---------------------- +Library user documentation content +---------------------------------- .. toctree:: :maxdepth: 2 quickstart reading + client tans transfers debits tested - developer + + +Library developer documentation content +--------------------------------------- +.. toctree:: + :maxdepth: 2 + + developer/index diff --git a/docs/reading.rst b/docs/reading.rst index 9c373f5..c37982f 100644 --- a/docs/reading.rst +++ b/docs/reading.rst @@ -9,6 +9,7 @@ The most simple method allows you to get all bank accounts that your user has ac .. autoclass:: fints.client.FinTS3Client :noindex: :members: get_sepa_accounts + :noindex: This method will return a list of named tuples of the following type: @@ -16,6 +17,16 @@ This method will return a list of named tuples of the following type: You will need this account object for many further operations to show which account you want to operate on. +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 ------------------------- diff --git a/docs/tans.rst b/docs/tans.rst index df9f9ff..83a3602 100644 --- a/docs/tans.rst +++ b/docs/tans.rst @@ -17,22 +17,6 @@ you want to use: The returned values have a subtype ``fints.models.TANMethod``, with varying parameters depending on the version used by the bank: -.. autoclass:: fints.client.FinTS3Client - :noindex: - :members: get_tan_methods - -.. autoclass:: fints.models.TANMethod1 - -.. autoclass:: fints.models.TANMethod2 - -.. autoclass:: fints.models.TANMethod3 - -.. autoclass:: fints.models.TANMethod4 - -.. autoclass:: fints.models.TANMethod5 - -.. autoclass:: fints.models.TANMethod6 - .. warning:: If the ``description_required`` attribute is ``2``, you will need to get the description of the TAN medium you want to use and pass it as ``tan_description`` to some operations. You can send a request for this information with the ``client.get_tan_description()`` method call. Currently, this returns an unparsed @@ -44,14 +28,6 @@ TAN challenges You should then pass the chosen ``TANMethod`` object to your operation, e.g. ``start_simple_sepa_transfer``. If a TAN is required, this operation will return a ``TANChallenge``, again depending on the version used by the bank. -.. autoclass:: fints.models.TANChallenge3 - -.. autoclass:: fints.models.TANChallenge4 - -.. autoclass:: fints.models.TANChallenge5 - -.. autoclass:: fints.models.TANChallenge6 - The ``challenge`` attribute will contain human-readable instructions on how to proceed. Flicker-Code / optiTAN diff --git a/fints/client.py b/fints/client.py index 4446071..9aeac90 100644 --- a/fints/client.py +++ b/fints/client.py @@ -114,7 +114,7 @@ class TransactionResponse: return "<{o.__class__.__name__}(status={o.status!r}, responses={o.responses!r}, data={o.data!r})>".format(o=self) class FinTS3Client: - def __init__(self, bank_identifier, user_id, customer_id=None, set_data=None): + def __init__(self, bank_identifier, user_id, customer_id=None, set_data:bytes=None): self.accounts = [] if isinstance(bank_identifier, BankIdentifier): self.bank_identifier = bank_identifier @@ -249,12 +249,12 @@ class FinTS3Client: return data - def get_data(self, including_private=False): + def get_data(self, including_private:bool=False) -> bytes: # FIXME Test, document data = self._get_data_v1(including_private=including_private) return compress_datablob(DATA_BLOB_MAGIC, 1, data) - def set_data(self, blob): + def set_data(self, blob: bytes): # FIXME Test, document decompress_datablob(DATA_BLOB_MAGIC, blob, self) @@ -277,31 +277,30 @@ class FinTS3Client: 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) - 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) - - ... - ```` + 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) + 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': {}, @@ -779,6 +778,9 @@ class FinTS3Client: Caller SHOULD ensure that the dialog is resumed (and properly ended) within a reasonable amount of time. :Example: + + :: + client = FinTS3PinTanClient(..., set_data=None) with client: challenge = client.sepa_transfer(...)