From 4f4d0d8c29cbeec22b4e41f97c6229a1937a1278 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Henryk=20Pl=C3=B6tz?= Date: Sun, 19 Aug 2018 22:39:05 +0200 Subject: [PATCH] Convert get_sepa_accounts() and get_balance() to new API. Move segments around. Implement new types. Still missing: Dummy encryption, PIN authorization. --- fints/client.py | 171 +++++++++++++++++++------------ fints/connection.py | 8 +- fints/dialog.py | 132 +++++++++++++++++++++++- fints/fields.py | 23 ++++- fints/formals.py | 114 ++++++++++++++++++++- fints/message.py | 49 ++++++++- fints/segments/__init__.py | 28 +---- fints/segments/accounts.py | 19 +++- fints/segments/auth.py | 2 +- fints/segments/dialog.py | 22 ++++ fints/segments/message.py | 18 +++- fints/segments/saldo.py | 56 +++++++++- fints/types.py | 2 +- tests/test_formals.py | 4 +- tests/test_message_serializer.py | 2 +- tests/test_models.py | 5 +- 16 files changed, 540 insertions(+), 115 deletions(-) diff --git a/fints/client.py b/fints/client.py index 33920a4..7bd9df0 100644 --- a/fints/client.py +++ b/fints/client.py @@ -2,69 +2,97 @@ import datetime import logging from decimal import Decimal -from fints.segments import HISPA1 from fints.segments.debit import HKDME, HKDSE from mt940.models import Balance from sepaxml import SepaTransfer from .connection import FinTSHTTPSConnection -from .dialog import FinTSDialog +from .dialog import FinTSDialogOLD, FinTSDialog from .formals import TwoStepParametersCommon -from .message import FinTSMessage +from .message import FinTSMessageOLD from .models import ( SEPAAccount, TANChallenge, TANChallenge3, TANChallenge4, TANChallenge5, TANChallenge6, ) -from .segments.accounts import HKSPA +from .segments import HIUPA4, HIBPA3 +from .segments.accounts import HKSPA, HKSPA1, HISPA1 from .segments.auth import HKTAB, HKTAN +from .segments.dialog import HKSYN3, HISYN4 from .segments.depot import HKWPD -from .segments.saldo import HKSAL +from .segments.saldo import HKSAL6, HKSAL7, HISAL6, HISAL7 from .segments.statement import HKKAZ from .segments.transfer import HKCCM, HKCCS from .utils import MT535_Miniparser, Password, mt940_to_array +from .formals import Account3, KTI1, BankIdentifier, SynchronisationMode logger = logging.getLogger(__name__) +SYSTEM_ID_UNASSIGNED = '0' class FinTS3Client: version = 300 - def __init__(self): + def __init__(self, bank_identifier, user_id, customer_id=None): self.accounts = [] + if isinstance(bank_identifier, BankIdentifier): + self.bank_identifier = bank_identifier + elif isinstance(bank_identifier, str): + self.bank_identifier = BankIdentifier('280', bank_identifier) + else: + raise TypeError("bank_identifier must be BankIdentifier or str (BLZ)") + self.system_id = SYSTEM_ID_UNASSIGNED + self.user_id = user_id + self.customer_id = customer_id or user_id + self.bpd_version = 0 + self.bpa = None + self.bpd = [] + self.upd_version = 0 + self.product_name = 'pyfints' + self.product_version = '0.2' + + def _new_dialog(self, lazy_init=False): + raise NotImplemented() - def _new_dialog(self): + def _new_message(self, dialog: FinTSDialogOLD, segments, tan=None): raise NotImplemented() - def _new_message(self, dialog: FinTSDialog, segments, tan=None): + def _ensure_system_id(self): raise NotImplemented() + def process_institute_response(self, message): + bpa = message.find_segment_first(HIBPA3) + if bpa: + self.bpa = bpa + self.bpd_version = bpa.bpd_version + self.bpd = list( + message.find_segments( + callback = lambda m: len(m.header.type) == 6 and m.header.type[1] == 'I' and m.header.type[5] == 'S' + ) + ) + + for seg in message.find_segments(HIUPA4): + self.upd_version = seg.upd_version + + def find_bpd(self, type): + for seg in self.bpd: + if seg.header.type == type: + yield seg + def get_sepa_accounts(self): """ Returns a list of SEPA accounts :return: List of SEPAAccount objects. """ - dialog = self._new_dialog() - dialog.sync() - dialog.init() - - def _get_msg(): - return self._new_message(dialog, [ - HKSPA(3, None, None, None) - ]) - - with self.pin.protect(): - logger.debug('Sending HKSPA: {}'.format(_get_msg())) - - resp = dialog.send(_get_msg()) - logger.debug('Got HKSPA response: {}'.format(resp)) - dialog.end() + with self._new_dialog() as dialog: + response = dialog.send(HKSPA1()) + self.accounts = [] - for seg in resp.find_segments(HISPA1): + for seg in response.find_segments(HISPA1): self.accounts.extend(seg.accounts) - return self.accounts + return [a for a in [acc.as_sepa_account() for acc in self.accounts] if a] def get_statement(self, account: SEPAAccount, start_date: datetime.datetime, end_date: datetime.date): """ @@ -121,7 +149,7 @@ class FinTS3Client: dialog.end() return statement - def _create_statement_message(self, dialog: FinTSDialog, account: SEPAAccount, start_date, end_date, touchdown): + def _create_statement_message(self, dialog: FinTSDialogOLD, account: SEPAAccount, start_date, end_date, touchdown): hversion = dialog.hkkazversion if hversion in (4, 5, 6): @@ -153,35 +181,35 @@ class FinTS3Client: :param account: SEPA account to fetch the balance :return: A mt940.models.Balance object """ - # init dialog - dialog = self._new_dialog() - dialog.sync() - dialog.init() - - # execute job - def _get_msg(): - return self._create_balance_message(dialog, account) - - with self.pin.protect(): - logger.debug('Sending HKSAL: {}'.format(_get_msg())) - - resp = dialog.send(_get_msg()) - logger.debug('Got HKSAL response: {}'.format(resp)) - # end dialog - dialog.end() + max_hksal_version = max( + (seg.header.version for seg in self.find_bpd('HISALS')), + default=6 + ) - # find segment and split up to balance part - seg = resp._find_segment('HISAL') - arr = seg[4] + if max_hksal_version in (1, 2, 3, 4, 5, 6): + seg = HKSAL6( + Account3.from_sepa_account(account), + False + ) + elif max_hksal_version == 7: + seg = HKSAL7( + KTI1.from_sepa_account(account), + False + ) + else: + raise ValueError('Unsupported HKSAL version {}'.format(max_hksal_version)) - # get balance date - date = datetime.datetime.strptime(arr[3], "%Y%m%d").date() - # return balance - return Balance(arr[0], arr[1], date, currency=arr[2]) + with self._new_dialog() as dialog: + response = dialog.send(seg) + + # find segment + seg = response.find_segment_first((HISAL6, HISAL7)) + if seg: + return seg.balance_booked.as_mt940_Balance() - def _create_balance_message(self, dialog: FinTSDialog, account: SEPAAccount): + def _create_balance_message(self, dialog: FinTSDialogOLD, account: SEPAAccount): hversion = dialog.hksalversion if hversion in (1, 2, 3, 4, 5, 6): @@ -242,7 +270,7 @@ class FinTS3Client: logger.debug('No HIWPD response segment found - maybe account has no holdings?') return [] - def _create_get_holdings_message(self, dialog: FinTSDialog, account: SEPAAccount): + def _create_get_holdings_message(self, dialog: FinTSDialogOLD, account: SEPAAccount): hversion = dialog.hksalversion if hversion in (1, 2, 3, 4, 5, 6): @@ -264,7 +292,7 @@ class FinTS3Client: ) ]) - def _create_send_tan_message(self, dialog: FinTSDialog, challenge: TANChallenge, tan): + def _create_send_tan_message(self, dialog: FinTSDialogOLD, challenge: TANChallenge, tan): return self._new_message(dialog, [ HKTAN(3, '2', challenge.reference, '', challenge.version) ], tan) @@ -454,7 +482,7 @@ class FinTS3Client: dialog.end() return dialog.tan_mechs - def _create_get_tan_description_message(self, dialog: FinTSDialog): + def _create_get_tan_description_message(self, dialog: FinTSDialogOLD): return self._new_message(dialog, [ HKTAB(3) ]) @@ -483,18 +511,35 @@ class FinTS3Client: class FinTS3PinTanClient(FinTS3Client): - def __init__(self, blz, username, pin, server): - self.username = username - self.blz = blz + def __init__(self, bank_identifier, user_id, pin, server, customer_id=None): self.pin = Password(pin) self.connection = FinTSHTTPSConnection(server) - self.systemid = 0 - super().__init__() + super().__init__(bank_identifier=bank_identifier, user_id=user_id, customer_id=customer_id) - def _new_dialog(self): - dialog = FinTSDialog(self.blz, self.username, self.pin, self.systemid, self.connection) - return dialog + def _new_dialog(self, lazy_init=False): + if not lazy_init: + self._ensure_system_id() - def _new_message(self, dialog: FinTSDialog, segments, tan=None): - return FinTSMessage(self.blz, self.username, self.pin, dialog.systemid, dialog.dialogid, dialog.msgno, + return FinTSDialog(self, lazy_init=lazy_init) + + # FIXME + # dialog = FinTSDialogOLD(self.blz, self.username, self.pin, self.systemid, self.connection) + # return dialog + + def _new_message(self, dialog: FinTSDialogOLD, segments, tan=None): + return FinTSMessageOLD(self.blz, self.username, self.pin, dialog.systemid, dialog.dialogid, dialog.msgno, segments, dialog.tan_mechs, tan) + + def _ensure_system_id(self): + if self.system_id != SYSTEM_ID_UNASSIGNED: + return + + with self._new_dialog(lazy_init=True) as dialog: + response = dialog.init( + HKSYN3(SynchronisationMode.NEW_SYSTEM_ID), + ) + + seg = response.find_segment_first(HISYN4) + if not seg: + raise ValueError('Could not find system_id') + self.system_id = seg.system_id diff --git a/fints/connection.py b/fints/connection.py index 281a7f9..69b3e6d 100644 --- a/fints/connection.py +++ b/fints/connection.py @@ -4,7 +4,7 @@ import requests from fints.parser import FinTS3Parser from fints.utils import Password -from .message import FinTSMessage, FinTSResponse +from .message import FinTSMessage, FinTSInstituteMessage class FinTSConnectionError(Exception): @@ -18,15 +18,15 @@ class FinTSHTTPSConnection: def send(self, msg: FinTSMessage): print("Sending >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") with Password.protect(): - FinTS3Parser().parse_message(str(msg).encode('iso-8859-1')).print_nested() + msg.print_nested() print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") r = requests.post( - self.url, data=base64.b64encode(str(msg).encode('iso-8859-1')), + self.url, data=base64.b64encode(msg.render_bytes()), ) 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 = FinTSResponse(response) + retval = FinTSInstituteMessage(segments=response) #import pprint; pprint.pprint(response) FIXME Remove print("Received <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") with Password.protect(): diff --git a/fints/dialog.py b/fints/dialog.py index 89d874a..f6f358d 100644 --- a/fints/dialog.py +++ b/fints/dialog.py @@ -1,17 +1,139 @@ import logging -from .message import FinTSMessage +from .message import FinTSMessageOLD, MessageDirection, FinTSMessage, FinTSCustomerMessage from .segments.auth import HKIDN, HKSYN, HKVVB -from .segments.dialog import HKEND +from .segments.dialog import HKEND, HKEND1 +from .segments.auth import HKIDN2, HKVVB3 +from .segments.message import HNHBK3, HNHBS1 +from .formals import BankIdentifier, SystemIDStatus, Language2, SynchronisationMode logger = logging.getLogger(__name__) +DIALOGUE_ID_UNASSIGNED = '0' class FinTSDialogError(Exception): pass class FinTSDialog: + def __init__(self, client=None, lazy_init=False): + 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 = [] + self.enc_mechanism = None + self.open = False + self.need_init = True + self.lazy_init = lazy_init + self.dialogue_id = DIALOGUE_ID_UNASSIGNED + + def __enter__(self): + if not self.lazy_init: + self.init() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.end() + + def init(self, *extra_segments): + 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 + ), + HKVVB3( + self.client.bpd_version, + self.client.upd_version, + Language2.DE, + self.client.product_name, + self.client.product_version + ) + ] + for s in extra_segments: + segments.append(s) + + try: + self.open = True + retval = self.send(*segments) + self.need_init = False + return retval + except: + self.open = False + raise + finally: + self.lazy_init = False + + def end(self): + if self.open: + self.send(HKEND1(self.dialogue_id)) + self.open = False + + def send(self, *segments): + if not self.open: + if self.lazy_init and self.need_init: + self.init() + + if not self.open: + raise Exception("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.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 + self.messages[response.segments[0].message_number] = message + 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.dialogue_id == DIALOGUE_ID_UNASSIGNED: + seg = response.find_segment_first(HNHBK3) + if not seg: + raise ValueError('Could not find dialogue_id') + self.dialogue_id = seg.dialogue_id + + self.client.process_institute_response(response) + + return response + + def new_customer_message(self): + message = FinTSCustomerMessage(self) + message += HNHBK3(0, 300, self.dialogue_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): + message += HNHBS1(message.segments[0].message_number) + + # Create signature(s) in reverse order: from inner to outer + for auth_mech in reversed(self.auth_mechanisms): + auth_mech.sign_commit(message) + + if self.enc_mechanism: + self.enc_mechanism.encrypt(message) + + message.segments[0].message_size = len(message.render_bytes()) + + +class FinTSDialogOLD: def __init__(self, blz, username, pin, systemid, connection): self.blz = blz self.username = username @@ -29,7 +151,7 @@ class FinTSDialog: seg_prepare = HKVVB(4) seg_sync = HKSYN(5) - return FinTSMessage(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ + return FinTSMessageOLD(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ seg_identification, seg_prepare, seg_sync @@ -39,13 +161,13 @@ class FinTSDialog: seg_identification = HKIDN(3, self.blz, self.username, self.systemid) seg_prepare = HKVVB(4) - return FinTSMessage(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ + return FinTSMessageOLD(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ seg_identification, seg_prepare, ], self.tan_mechs) def _get_msg_end(self): - return FinTSMessage(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ + return FinTSMessageOLD(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ HKEND(3, self.dialogid) ]) diff --git a/fints/fields.py b/fints/fields.py index b6cec5f..39e6400 100644 --- a/fints/fields.py +++ b/fints/fields.py @@ -217,7 +217,28 @@ class DigitsField(FieldRenderFormatStringMixin, DataElementField): class FloatField(FieldRenderFormatStringMixin, DataElementField): type = 'float' - ## FIXME: Not implemented, no one uses this? + _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 + + _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 = super()._render_value(value) + return retval.replace('.', ',').rstrip('0') + +class AmountField(FixedLengthMixin, FloatField): + type = 'wrt' + _FIXED_LENGTH = [None, None, 15] + # FIXME Needs test class BinaryField(DataElementField): type = 'bin' diff --git a/fints/formals.py b/fints/formals.py index 99acc4f..6043e4c 100644 --- a/fints/formals.py +++ b/fints/formals.py @@ -300,7 +300,7 @@ class SupportedLanguages2(DataElementGroup): class SupportedHBCIVersions2(DataElementGroup): versions = DataElementField(type='code', max_length=3, min_count=1, max_count=9) -class AccountInternational(DataElementGroup): +class KTZ1(DataElementGroup): """Kontoverbindung ZV international, version 1 Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """ @@ -311,6 +311,71 @@ class AccountInternational(DataElementGroup): 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 + if not self.bank_identifier.country_identifier == '280': + # FIXME Limitation of SEPAAccount object + 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='280', + 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, + account_number=acc.accountnumber, + subaccount_number=acc.subaccount, + bank_identifier=BankIdentifier( + country_identifier='280', + 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='280', + bank_code=acc.blz + ) + ) + class SecurityRole(RepresentableEnum): """Rolle des Sicherheitslieferanten, kodiert, version 2 @@ -372,3 +437,50 @@ class SystemIDStatus(RepresentableEnum): 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 SynchronisationMode(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 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 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 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") diff --git a/fints/message.py b/fints/message.py index f60fe43..7287c43 100644 --- a/fints/message.py +++ b/fints/message.py @@ -1,11 +1,52 @@ import random +from enum import Enum from .formals import SegmentSequence -from .segments import HIBPA3, HIRMG2, HIRMS2, HISYN4, HNHBK3, HITANSBase -from .segments.message import HNHBK, HNHBS, HNSHA, HNSHK, HNVSD, HNVSK +from .segments import FinTS3Segment, HIBPA3, HIRMG2, HIRMS2, HITANSBase +from .segments.message import HNHBK3, HNHBS1, HNSHA, HNSHK, HNVSD, HNVSK +class MessageDirection(Enum): + FROM_CUSTOMER = 1 + FROM_INSTITUTE = 2 -class FinTSMessage: +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 sign_prepare(self, auth_mech): + pass + + def sign_commit(self, auth_mech): + pass + + def encrypt(self, enc_mech): + pass + + def decrypt(self, enc_mech): + pass + + +class FinTSCustomerMessage(FinTSMessage): + DIRECTION = MessageDirection.FROM_CUSTOMER + # Identification, authentication + +class FinTSInstituteMessage(FinTSMessage): + DIRECTION = MessageDirection.FROM_INSTITUTE + +class FinTSMessageOLD: def __init__(self, blz, username, pin, systemid, dialogid, msgno, encrypted_segments, tan_mechs=None, tan=None): self.blz = blz self.username = username @@ -112,7 +153,7 @@ class FinTSResponse(SegmentSequence): if seg.header.reference == int(str(ref.segmentno)): return seg - def get_touchdowns(self, msg: FinTSMessage): + def get_touchdowns(self, msg: FinTSMessageOLD): touchdown = {} for msgseg in msg.encrypted_segments: seg = self._find_segment_for_reference(HIRMS2, msgseg) diff --git a/fints/segments/__init__.py b/fints/segments/__init__.py index 11e1d98..9d66c60 100644 --- a/fints/segments/__init__.py +++ b/fints/segments/__init__.py @@ -1,7 +1,7 @@ import re from fints.formals import ( - AccountInformation, AccountInternational, AccountLimit, + AccountInformation, KTZ1, AccountLimit, AllowedTransaction, BankIdentifier, Certificate, Container, ContainerMeta, DataElementField, DataElementGroupField, EncryptionAlgorithm, HashAlgorithm, KeyName, ParameterPinTan, ParameterTwostepTAN1, @@ -81,18 +81,6 @@ class FinTS3Segment(Container, SubclassesMixin, metaclass=FinTS3SegmentMeta): return target_cls -class HNHBK3(FinTS3Segment): - "Nachrichtenkopf" - message_size = DataElementField(type='dig', length=12, _d="Größe der Nachricht (nach Verschlüsselung und Komprimierung)") - hbci_version = DataElementField(type='num', max_length=3, _d="HBCI-Version") - dialogue_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 HNVSD1(FinTS3Segment): "Verschlüsselte Daten" @@ -161,13 +149,6 @@ class HIUPD6(FinTS3Segment): allowed_transactions = DataElementGroupField(type=AllowedTransaction, max_count=999, required=False, _d="Erlaubte Geschäftsvorfälle") extension = DataElementField(type='an', max_length=2048, required=False, _d="Erweiterung, kontobezogen") -class HISYN4(FinTS3Segment): - "Synchronisierungsantwort" - customer_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 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") @@ -216,9 +197,4 @@ class HIBPA3(FinTS3Segment): 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 HISPA1(FinTS3Segment): - """SEPA-Kontoverbindung rückmelden, version 1 - - Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle - """ - accounts = DataElementGroupField(type=AccountInternational, max_count=999, required=False, _d="SEPA-Kontoverbindung") +from . import accounts, auth, debit, depot, dialog, message, saldo, statement, transfer diff --git a/fints/segments/accounts.py b/fints/segments/accounts.py index 6357af0..aa668aa 100644 --- a/fints/segments/accounts.py +++ b/fints/segments/accounts.py @@ -1,5 +1,7 @@ -from . import FinTS3SegmentOLD +from . import FinTS3SegmentOLD, FinTS3Segment +from ..fields import DataElementGroupField +from ..formals import KTZ1, Account3 class HKSPA(FinTS3SegmentOLD): """ @@ -17,3 +19,18 @@ class HKSPA(FinTS3SegmentOLD): ]) if accno is not None else '' ] super().__init__(segno, data) + +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") + diff --git a/fints/segments/auth.py b/fints/segments/auth.py index f01bb6a..de77e6a 100644 --- a/fints/segments/auth.py +++ b/fints/segments/auth.py @@ -1,7 +1,7 @@ from fints.utils import fints_escape from . import FinTS3SegmentOLD, FinTS3Segment -from fints.formals import BankIdentifier, SystemIDStatus, Language2 +from fints.formals import BankIdentifier, SystemIDStatus, Language2, SynchronisationMode from fints.fields import DataElementGroupField, DataElementField, CodeField class HKIDN(FinTS3SegmentOLD): diff --git a/fints/segments/dialog.py b/fints/segments/dialog.py index 4d74679..97b9913 100644 --- a/fints/segments/dialog.py +++ b/fints/segments/dialog.py @@ -1,5 +1,21 @@ from . import FinTS3SegmentOLD +from . import FinTS3Segment +from ..fields import CodeField, DataElementField +from ..formals import SynchronisationMode + +class HKSYN3(FinTS3Segment): + """Synchronisierung, version 3 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals""" + synchronisation_mode = CodeField(enum=SynchronisationMode, 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 HKEND(FinTS3SegmentOLD): """ @@ -14,3 +30,9 @@ class HKEND(FinTS3SegmentOLD): dialogid, ] super().__init__(segno, data) + +class HKEND1(FinTS3Segment): + """Dialogende, version 1 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Formals""" + dialogue_id = DataElementField(type='id', _d="Dialog-ID") diff --git a/fints/segments/message.py b/fints/segments/message.py index ff2440b..8180184 100644 --- a/fints/segments/message.py +++ b/fints/segments/message.py @@ -2,7 +2,21 @@ import time from fints.utils import fints_escape -from . import FinTS3SegmentOLD +from . import FinTS3SegmentOLD, FinTS3Segment +from fints.formals import ReferenceMessage +from fints.fields import DataElementGroupField, DataElementField, ZeroPaddedNumericField + +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") + dialogue_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 HNHBK(FinTS3SegmentOLD): @@ -48,7 +62,7 @@ class HNSHK(FinTS3SegmentOLD): secref, self.SECURITY_BOUNDARY, self.SECURITY_SUPPLIER_ROLE, - ':'.join(['1', '', str(systemid)]), + ':'.join(['1', '', fints_escape(str(systemid))]), 1, ':'.join(['1', time.strftime('%Y%m%d'), time.strftime('%H%M%S')]), ':'.join(['1', '999', '1']), # Negotiate hash algorithm diff --git a/fints/segments/saldo.py b/fints/segments/saldo.py index a53333d..19091a2 100644 --- a/fints/segments/saldo.py +++ b/fints/segments/saldo.py @@ -1,4 +1,7 @@ -from . import FinTS3SegmentOLD +from . import FinTS3SegmentOLD, FinTS3Segment + +from fints.formals import Account3, KTI1, Balance2, Amount1, Timestamp1 +from fints.fields import DataElementGroupField, DataElementField class HKSAL(FinTS3SegmentOLD): @@ -15,3 +18,54 @@ class HKSAL(FinTS3SegmentOLD): 'N' ] super().__init__(segno, data) + +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_time = 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_time = DataElementGroupField(type=Timestamp1, required=False, _d="Buchungszeitpunkt") + date_due = DataElementField(type='dat', required=False, _d="Fälligkeit") diff --git a/fints/types.py b/fints/types.py index 997bcef..e5876fa 100644 --- a/fints/types.py +++ b/fints/types.py @@ -129,7 +129,7 @@ class SegmentSequence: def find_segments(self, query=None, version=None, callback=None, recurse=True): """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.HNHBK3`), or a list or tuple of strings or classes. + :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. diff --git a/tests/test_formals.py b/tests/test_formals.py index d529282..0b8808e 100644 --- a/tests/test_formals.py +++ b/tests/test_formals.py @@ -342,7 +342,7 @@ def test_container_generic(): def test_find_1(): from conftest import TEST_MESSAGES from fints.parser import FinTS3Parser - from fints.segments import HNHBS1, HNHBK3 + from fints.segments.message import HNHBS1, HNHBK3 m = FinTS3Parser().parse_message(TEST_MESSAGES['basic_complicated']) @@ -365,7 +365,7 @@ def test_find_1(): def test_find_by_class(): from conftest import TEST_MESSAGES from fints.parser import FinTS3Parser - from fints.segments import HNHBS1 + from fints.segments.message import HNHBS1 m = FinTS3Parser().parse_message(TEST_MESSAGES['basic_complicated']) diff --git a/tests/test_message_serializer.py b/tests/test_message_serializer.py index 14db934..661fedb 100644 --- a/tests/test_message_serializer.py +++ b/tests/test_message_serializer.py @@ -63,6 +63,6 @@ def test_escape(): def test_serialize_2(): from fints.formals import SegmentSequence import fints.formals, fints.segments - 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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(date_time_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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(date_time_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)]) + s = SegmentSequence([fints.segments.message.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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(date_time_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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(date_time_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.message.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', 5, 1), message_number=2)]) assert FinTS3Serializer().serialize_message(s) == TEST_MESSAGES['basic_simple'] diff --git a/tests/test_models.py b/tests/test_models.py index 68ca208..10fa2f6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,8 @@ from fints.formals import ( AlphanumericField, DataElementField, GenericField, NumericField, SegmentHeader, SegmentSequence, ) -from fints.segments import HNHBK3, HNHBS1, FinTS3Segment +from fints.segments import FinTS3Segment +from fints.segments.message import HNHBK3, HNHBS1 def test_metaclass_foo(): @@ -119,7 +120,7 @@ def test_find_subclass(): def test_nested_output_evalable(): import fints.segments, fints.formals - a = SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3, None), 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, None), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='998', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(date_time_type='1', date=None, time=None), 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', algorithm_parameter_iv_value=None), 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', certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.HNVSD1(header=fints.formals.SegmentHeader('HNVSD', 999, 1, None), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader('HNSHK', 2, 4, None), 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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(date_time_type='1', date=None, time=None), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1', algorithm_parameter_value=None), 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), certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader('HIRMG', 3, 2, None), _additional_data=[['0010', None, 'Nachricht entgegengenommen.'], ['0100', None, 'Dialog beendet.']]), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader('HNSHA', 4, 2, None), _additional_data=['9166926'])])), fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', 5, 1, None), message_number=2)]) + a = SegmentSequence([fints.segments.message.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3, None), 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, None), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='998', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(date_time_type='1', date=None, time=None), 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', algorithm_parameter_iv_value=None), 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', certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.HNVSD1(header=fints.formals.SegmentHeader('HNVSD', 999, 1, None), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader('HNSHK', 2, 4, None), 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(identified_role='2', cid=None, identifier='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(date_time_type='1', date=None, time=None), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1', algorithm_parameter_value=None), 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), certificate=fints.formals.Certificate(certificate_type=None, certificate_content=None)), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader('HIRMG', 3, 2, None), _additional_data=[['0010', None, 'Nachricht entgegengenommen.'], ['0100', None, 'Dialog beendet.']]), fints.segments.FinTS3Segment(header=fints.formals.SegmentHeader('HNSHA', 4, 2, None), _additional_data=['9166926'])])), fints.segments.message.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', 5, 1, None), message_number=2)]) output = StringIO() a.print_nested(stream=output) -- 2.39.5