]> git.ipfire.org Git - thirdparty/python-fints.git/commitdiff
New-style object oriented interface
authorHenryk Plötz <henryk@ploetzli.ch>
Mon, 6 Aug 2018 18:09:49 +0000 (20:09 +0200)
committerRaphael Michel <mail@raphaelmichel.de>
Mon, 3 Dec 2018 18:34:17 +0000 (19:34 +0100)
fints/formals.py [new file with mode: 0644]
fints/message.py
fints/parser.py [new file with mode: 0644]
fints/segments/__init__.py
fints/utils.py
tests/conftest.py [new file with mode: 0644]

diff --git a/fints/formals.py b/fints/formals.py
new file mode 100644 (file)
index 0000000..7bb7cd7
--- /dev/null
@@ -0,0 +1,402 @@
+import re
+import warnings
+from contextlib import suppress
+from inspect import getmro
+from copy import deepcopy
+from collections import OrderedDict
+
+from fints.utils import classproperty, SubclassesMixin
+
+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 __len__(self):
+        if self._parent.count is not None:
+            return self._parent.count
+        else:
+            retval = len(self._data)
+            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]
+
+class Field:
+    def __init__(self, length=None, min_length=None, max_length=None, count=None, min_count=None, max_count=None, required=True):
+        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
+
+    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)
+
+class TypedField(Field, SubclassesMixin):
+    flat_length = 1
+
+    def __new__(cls, *args, **kwargs):
+        target_cls = None
+        fallback_cls = None
+        if 'type' in kwargs:
+            for subcls in cls._all_subclasses():
+                if getattr(subcls, 'type', '') is None:
+                    fallback_cls = subcls
+                if getattr(subcls, 'type', None) == kwargs['type']:
+                    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 DataElementField(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()
+
+    @property
+    def flat_length(self):
+        result = 0
+        for name, field in self.type._fields.items():
+            if field.count is None:
+                raise TypeError("Cannot compute flat length of field {}.{} with variable count".format(self.__class__.__name__, name))
+            result = result + field.count * field.flat_length
+        return result
+    
+
+class DataElementGroupField(ContainerField):
+    pass
+
+class GenericField(DataElementField):
+    type = None
+    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(DataElementField):
+    type = 'txt'
+
+    def _parse_value(self, value): return str(value)
+
+class AlphanumericField(TextField):
+    type = 'an'
+    
+class DTAUSField(DataElementField):
+    type = 'dta'
+
+class NumericField(DataElementField):
+    type = 'num'
+
+    def _parse_value(self, value): 
+        _value = str(value)
+        if len(_value) > 1 and _value[0] == '0':
+            raise TypeError("Leading zeroes not allowed for value of type 'num': {!r}".format(value))
+        return int(_value, 10)
+
+class DigitsField(DataElementField):
+    type = 'dig'
+
+    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'
+
+class BinaryField(DataElementField):
+    type = 'bin'
+
+    def _parse_value(self, value): return bytes(value)
+
+
+class SegmentSequence:
+    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 = segments or []
+
+    def __repr__(self):
+        return "{}({!r})".format(self.__class__.__name__, self.segments)
+
+    def print_nested(self, stream=None, level=0, indent="    ", prefix="", first_level_indent=True, trailer=""):
+        import sys
+        stream = stream or sys.stdout
+        stream.write(
+            ( (prefix + level*indent) if first_level_indent else "")
+            + "{}([".format(self.__class__.__name__) + "\n"
+        )
+        for segment in self.segments:
+            segment.print_nested(stream=stream, level=level+1, indent=indent, prefix=prefix, first_level_indent=True, trailer=",")
+        stream.write( (prefix + level*indent) + "]){}\n".format(trailer) )
+
+class SegmentSequenceField(DataElementField):
+    type = 'sf'
+
+    def _parse_value(self, value):
+        if isinstance(value, SegmentSequence):
+            return value
+        else:
+            return SegmentSequence(value)
+
+
+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):
+            retval._fields.update((k,v) for (k,v) in supercls.__dict__.items() if isinstance(v, Field))
+        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):
+        retval = cls()
+        for ((name, field), value) in zip(retval._fields.items(), data):
+            setattr(retval, name, value)
+        return retval
+
+    def __repr__(self):
+        return "{}{}({})".format(
+            "{}.".format(self.__class__.__module__),
+            self.__class__.__name__,
+            ", ".join(
+                "{}={!r}".format(name, getattr(self, name))
+                for name in list(self._fields.keys())+( ['_additional_data'] if self._additional_data else [] )
+            )
+        )
+
+    def print_nested(self, stream=None, level=0, indent="    ", prefix="", first_level_indent=True, trailer=""):
+        import sys
+        stream = stream or sys.stdout
+
+        stream.write(
+            ( (prefix + level*indent) if first_level_indent else "")
+            + "{}.{}(".format(self.__class__.__module__, self.__class__.__name__) + "\n"
+        )
+        for name, field in self._fields.items():
+            val = getattr(self, name)
+            if not hasattr( getattr(val, 'print_nested', None), '__call__'):
+                stream.write(
+                    (prefix + (level+1)*indent) + "{} = {!r},\n".format(name, val)
+                )
+            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=",")
+        if self._additional_data:
+            stream.write(
+                (prefix + (level+1)*indent) + "_additional_data=\n" +
+                (prefix + (level+2)*indent) + "{!r},\n".format(self._additional_data)
+            )
+        stream.write( (prefix + level*indent) + "){}\n".format(trailer) )
+
+
+class DataElementGroup(Container):
+    pass
+
+class SegmentHeader(DataElementGroup):
+    type = AlphanumericField(max_length=6)
+    number = NumericField(max_length=3)
+    version = NumericField(max_length=3)
+    reference = NumericField(max_length=3, required=False)
+
+class ReferenceMessage(DataElementGroup):
+    dialogue_id = DataElementField(type='id')
+    message_number = NumericField(max_length=4)
+
+class SecurityProfile(DataElementGroup):
+    security_method = DataElementField(type='code', length=3)
+    security_method_version = DataElementField(type='num')
+
+class SecurityIdentificationDetails(DataElementGroup):
+    name_party = DataElementField(type='code', max_length=3)
+    cid = DataElementField(type='bin', max_length=256)
+    identifier_party = DataElementField(type='id')
+
+class SecurityDateTime(DataElementGroup):
+    datetime_type = DataElementField(type='code', max_length=3)
+    date = DataElementField(type='dat')
+    time = DataElementField(type='tim')
+
+class EncryptionAlgorithm(DataElementGroup):
+    usage_encryption = DataElementField(type='code', max_length=3)
+    operation_mode = DataElementField(type='code', max_length=3)
+    encryption_algorithm = DataElementField(type='code', max_length=3)
+    algorithm_parameter_value = DataElementField(type='bin', max_length=512)
+    algorithm_parameter_name = DataElementField(type='code', max_length=3)
+    algorithm_parameter_iv_name = DataElementField(type='code', max_length=3)
+    algorithm_parameter_iv_value = DataElementField(type='bin', max_length=512)
+
+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)
+
+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_identifier = DataElementField(type='ctr')
+    bank_code = DataElementField(type='an', max_length=30)
+
+class KeyName(DataElementGroup):
+    bank_identifier = DataElementGroupField(type=BankIdentifier)
+    user_id = DataElementField(type='id')
+    key_type = DataElementField(type='code', length=1)
+    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)
index 1c570cd2c7f675acc8806e94be4d9dd0e8652ebe..7ca10372112f84a543ffb68cdc0e16989b79e9a2 100644 (file)
@@ -4,138 +4,8 @@ import re
 
 from fints.models import TANMethod1, TANMethod2, TANMethod3, TANMethod4, TANMethod5, TANMethod6
 from .segments.message import HNHBK, HNHBS, HNSHA, HNSHK, HNVSD, HNVSK
+from .parser import FinTS3Parser
 
-TOKEN_RE = re.compile(rb"""
-                        ^(?:  (?: \? (?P<ECHAR>.) )
-                            | (?P<CHAR>[^?:+@']+)
-                            | (?P<TOK>[+:'])
-                            | (?: @ (?P<BINLEN>[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 FinTSMessageBase:
-    def __init__(self, *segments):
-        self.segments = []
-        for segment in segments:
-            self.add_segment(segment)
-
-    def add_segment(self, segment):
-        self.segments.append(segment)
-
-    @classmethod
-    def parse(cls, data: bytes, start=0, end=None):
-        return cls(*cls.parse_segments(data, start, end))
-
-    @classmethod
-    def parse_segments(cls, 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)
-                    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 FinTSMessage:
     def __init__(self, blz, username, pin, systemid, dialogid, msgno, encrypted_segments, tan_mechs=None, tan=None):
@@ -194,13 +64,13 @@ class FinTSMessage:
         return str(self.build_header()) + ''.join([str(s) for s in self.segments])
 
 
-class FinTSResponse(FinTSMessageBase):
+class FinTSResponse:
     def __init__(self, data):
-        self.segments = self.parse_segments(data)
+        self.segments = FinTS3Parser.explode_segments(data)
         self.payload = self.segments
         for seg in self.segments:
             if seg[0][0] == 'HNVSD':
-                self.payload = self.parse_segments(seg[1])
+                self.payload = FinTS3Parser.explode_segments(seg[1])
 
     def __str__(self):
         return str(self.payload)
diff --git a/fints/parser.py b/fints/parser.py
new file mode 100644 (file)
index 0000000..6886f67
--- /dev/null
@@ -0,0 +1,218 @@
+from enum import Enum
+from collections import Iterable
+from contextlib import suppress
+import re
+from .segments import FinTS3Segment
+from .formals import DataElementField, DataElementGroupField, SegmentSequence
+
+# 
+# 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
+#
+
+TOKEN_RE = re.compile(rb"""
+                        ^(?:  (?: \? (?P<ECHAR>.) )
+                            | (?P<CHAR>[^?:+@']+)
+                            | (?P<TOK>[+:'])
+                            | (?: @ (?P<BINLEN>[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:
+    def parse_message(self, data):
+        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)
+        seg = clazz()
+
+        data = iter(segment)
+        for name, field in seg._fields.items():
+            try:
+                val = next(data)
+            except StopIteration:
+                pass
+            else:
+                deg = self.parse_n_deg(field, val)
+                setattr(seg, name, deg)
+        seg._additional_data = list(data)
+
+        return seg
+
+    def parse_n_deg(self, field, data):
+        if not isinstance(data, Iterable) or isinstance(data, (str, bytes)):
+            data = [data]
+
+        data_i = iter(data)
+        field_index = 0
+        field_length = field.flat_length
+
+        retval = []
+        eod = False
+
+        while not eod:
+            vals = []
+            try:
+                for x in range(field_length):
+                    vals.append(next(data_i))
+            except StopIteration:
+                eod = True
+
+            if field.count == 1:
+                if isinstance(field, DataElementField):
+                    if not len(vals):
+                        return
+                    return vals[0]
+                elif isinstance(field, DataElementGroupField):
+                    return self.parse_deg(field.type, vals)
+                else:
+                    raise Error("Internal error")
+                break
+
+            if field_index >= (field.count if field.count is not None else len(data) // field_length):
+                break
+
+            if isinstance(field, DataElementField):
+                retval.append(vals[0] if len(vals) else None)
+            elif isinstance(field, DataElementGroupField):
+                retval.append(self.parse_deg(field.type, vals))
+            else:
+                raise Error("Internal error")
+
+        return retval
+
+    def parse_deg(self, clazz, vals):
+        retval = clazz()
+
+        data_i = iter(vals)
+        for name, field in retval._fields.items():
+            deg = self.parse_n_deg(field, data_i)
+            setattr(retval, name, deg)
+
+        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)
+                    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
+
index 86926010d3ec5001160d24ee3e365f35daf97b20..014f5f8cf883e55d30f24fcc0eae23f448e9cb3e 100644 (file)
@@ -1,14 +1,87 @@
-class FinTS3Segment:
-    type = '???'
-    country_code = 280
-    version = 2
-
-    def __init__(self, segmentno, data):
-        self.segmentno = segmentno
-        self.data = data
-
-    def __str__(self):
-        res = '{}:{}:{}'.format(self.type, self.segmentno, self.version)
-        for d in self.data:
-            res += '+' + str(d)
-        return res + "'"
+import re
+
+from fints.formals import Container, SegmentHeader, DataElementGroupField, DataElementField, ReferenceMessage, SegmentSequenceField, SecurityProfile, SecurityIdentificationDetails, SecurityDateTime, EncryptionAlgorithm, KeyName, Certificate, HashAlgorithm, SignatureAlgorithm
+
+from fints.utils import classproperty, SubclassesMixin
+
+TYPE_VERSION_RE = re.compile(r'^([A-Z]+)(\d+)$')
+
+class FinTS3Segment(Container, SubclassesMixin):
+    header = DataElementGroupField(type=SegmentHeader)
+
+    @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'] = None
+
+        args = (kwargs.pop('header'), ) + args
+
+        return 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 HNHBK3(FinTS3Segment):
+    message_size = DataElementField(type='dig', length=12)
+    hbci_version = DataElementField(type='num', max_length=3)
+    dialogue_id = DataElementField(type='id')
+    message_number = DataElementField(type='num', max_length=4)
+    reference_message = DataElementGroupField(type=ReferenceMessage, required=False)
+
+class HNHBS1(FinTS3Segment):
+    message_number = DataElementField(type='num', max_length=4)
+
+
+class HNVSD1(FinTS3Segment):
+    data = SegmentSequenceField()
+
+class HNVSK3(FinTS3Segment):
+    security_profile = DataElementGroupField(type=SecurityProfile)
+    security_function = DataElementField(type='code', max_length=3)
+    security_role = DataElementField(type='code', max_length=3)
+    security_identification_details = DataElementGroupField(type=SecurityIdentificationDetails)
+    security_datetime = DataElementGroupField(type=SecurityDateTime)
+    encryption_algorithm = DataElementGroupField(type=EncryptionAlgorithm)
+    key_name = DataElementGroupField(type=KeyName)
+    compression_function = DataElementField(type='code', max_length=3)
+    certificate = DataElementGroupField(type=Certificate)
+
+class HNSHK4(FinTS3Segment):
+    security_profile = DataElementGroupField(type=SecurityProfile)
+    security_function = DataElementField(type='code', max_length=3)
+    security_reference = DataElementField(type='an', max_length=14)
+    security_application_area = DataElementField(type='code', max_length=3)
+    security_role = DataElementField(type='code', max_length=3)
+    security_identification_details = DataElementGroupField(type=SecurityIdentificationDetails)
+    security_reference_number = DataElementField(type='num', max_length=16)
+    security_datetime = DataElementGroupField(type=SecurityDateTime)
+    hash_algorithm = DataElementGroupField(type=HashAlgorithm)
+    signature_algorithm = DataElementGroupField(type=SignatureAlgorithm)
+    key_name = DataElementGroupField(type=KeyName)
+    certificate = DataElementGroupField(type=Certificate)
+
+
+
+
index c6fce95c92764c84ea78d9bf4d5d30d18dce3086..50fdfc7ae4151d00a0abdee98c136ef61db27ae6 100644 (file)
@@ -40,6 +40,23 @@ def split_for_data_elements(deg):
     return re.split(':(?<!\?:)', deg)
 
 
+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)
+
+
+class SubclassesMixin:
+    @classmethod
+    def _all_subclasses(cls):
+        for subcls in cls.__subclasses__():
+            yield from subcls._all_subclasses()
+        yield cls
+
+
 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*)$")
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644 (file)
index 0000000..c0c28bb
--- /dev/null
@@ -0,0 +1,295 @@
+import pytest
+
+COMPLICATED_EXAMPLE = \
+(b'HNHBK:1:3+000000021319+300+430711670077=043999659571CN9D=+1+430711670077=043'
+ b"999659571CN9D=:1'HNVSK:998:3+PIN:1+998+1+2::0+1:20180730:135639+2:2:13:@8@00"
+ b"000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@21094@HNSHK:2:4+PIN:1+99"
+ b'9+1259150+1+1+2::0+1+1:20180730:135639+1:999:1+6:10:16+280:15050500:hermes:S'
+ b":0:0'HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.'"
+ b"HIRMS:4:2:5+0020::Auftrag ausgef\xfchrt.'HIRMS:5:2:4+3050::UPD nicht mehr a"
+ b'ktuell, aktuelle Version enthalten.+3050::BPD nicht mehr aktuell, aktuelle V'
+ b'ersion enthalten.+3920::Zugelassene Zwei-Schritt-Verfahren f\xfcr den Benut'
+ b"zer.:910:911:912+0020::Der Auftrag wurde ausgef\xfchrt.'HIBPA:6:3:4+3+280:1"
+ b"5050500+Sparkasse Vorpommern+3+1+300'HIKOM:7:4:4+280:15050500+1+3:banking-mv"
+ b"6.s-fints-pt-mv.de/fints30+2:banking-mv6.s-fints-pt-mv.de::MIM:1'HISHV:8:3:4"
+ b"+N+RAH:7+PIN:1+DDV:1'HICERS:9:1:4+999+0+4+1:N:J:J:N:RAH:7'HICSUS:10:1:4+1+1+"
+ b'1+INTC;CORT:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:'
+ b"std?:iso?:20022?:tech?:xsd?:pain.001.001.03'DIWOKS:11:1:4+1+1+1+9999999,99:E"
+ b"UR'DIWDHS:12:1:4+1+1+1+J:N:730'DIBVES:13:1:4+1+1+1+E'DIPTZS:14:1:4+1+1+1+J'D"
+ b"IEEAS:15:1:4+1+1+1+DKPTZ:1:N'DIALES:16:1:4+1+1+1+V-EC-KARTE:V-S-CARD:V-WERTK"
+ b"ARTE'DIALLS:17:1:4+1+1+1'DIALNS:18:1:4+1+1+1+V-EC-KARTE:V-S-CARD:V-WERTKARTE"
+ b"'DIANAS:19:1:4+1+1+1+1:15'DIANLS:20:1:4+1+1+1'DIBAZS:21:2:4+1+1+1+J:J'DIBKDS"
+ b":22:4:4+1+1+1'DIBKUS:23:3:4+1+1+1+J:N'DIBUMS:24:3:4+1+1+1+N'DIBVAS:25:1:4+1+"
+ b"1+1'DIBVBS:26:1:4+1+1+1'DIBVDS:27:1:4+1+1+1'DIBVKS:28:1:4+1+1+1+J:V-EC-KARTE"
+ b":V-S-CARD:V-WERTKARTE'DIBVPS:29:1:4+1+1+1+8:20'DIBVRS:30:1:4+1+1+1+8:20::N:N"
+ b"'DIBVSS:31:1:4+1+1+1'DIBVSS:32:2:4+1+1+1'DIDFAS:33:1:4+1+1+1+N'DIDFBS:34:1:4"
+ b"+1+1+1'DIDFCS:35:1:4+1+1+1'DIDFDS:36:1:4+1+1+1'DIDFLS:37:1:4+1+1+1'DIDFUS:38"
+ b":1:4+1+1+1+N'DIDFUS:39:2:4+1+1+1+N'DIDIHS:40:1:4+1+1+1'DIDFSS:41:2:4+1+1+1+N"
+ b':1;DekaBank-Konzern;5;Swisscanto;7;JPMorgan Fleming;8;Lombard Odier;10;Frank'
+ b'lin Templeton;11;Gartmore;12;Goldman Sachs;13;Black Rock Merrill;14;Threadne'
+ b'edle;15;UBS;16;Schroders:10_10:Aktienfonds Asien - Pazifik ohne Japan:10_20:'
+ b'Aktienfonds Branche:10_30:Aktienfonds Deutschland:10_40:Aktienfonds Emerging'
+ b' Markets:10_50:Aktienfonds Euroland:10_60:Aktienfonds Europa L\xe4nder:10_7'
+ b'0:Aktienfonds Europa:10_80:Aktienfonds Japan:10_90:Aktienfonds Lateinamerika'
+ b':10_100:Aktienfonds Nordamerika:10_110:Aktienfonds Osteuropa:10_120:Aktienfo'
+ b'nds Welt:10_400:Aktienfonds Afrika:10_410:Aktienfonds Mittlerer Osten:10_420'
+ b':Aktienfonds Nordeuropa:20_130:Dachfonds Chance Plus:20_140:Dachfonds Chance'
+ b':20_150:Dachfonds Ertrag Plus:20_160:Dachfonds Ertrag:20_170:Dachfonds Wachs'
+ b'tum:20_180:Dachfonds laufzeitbegrenzt:30_430:Garantiefonds:40_200:Geldmarktf'
+ b'onds:40_210:Geldmarktnahe Fonds:50_220:Alternative Investmentfonds Hedgefond'
+ b's:50_230:Alternative Investmentfonds Private Equity:50_240:Alternative Inves'
+ b'tmentfonds Rohstofffonds:60_250:Sonderkonzepte Absolute-/Total-Returnstrateg'
+ b'iefonds:60_260:Sonderkonzepte Altersvorsorgefonds:60_270:Sonderkonzepte Inst'
+ b'itutionelle Fondskonzepte:60_280:Sonderkonzepte Steuerorientierte Fonds:70_3'
+ b'0:Immobilienfonds Deutschland:70_70:Immobilienfonds Europa:70_120:Immobilien'
+ b'fonds Welt:80_50:Mischfonds Euroland:80_290:Mischfonds ausgewogen:80_300:Mis'
+ b'chfonds dynamisch:80_310:Mischfonds flexibel:80_320:Mischfonds konservativ:9'
+ b'0_330:Rentenfonds Inflationsindexierte Anleihen:90_340:Rentenfonds Laufzeitf'
+ b'onds:90_350:Rentenfonds MBS:90_360:Rentenfonds Nachranganleihen:90_370:Rente'
+ b'nfonds Staatsanleihen:90_380:Rentenfonds Unternehmensanleihen:90_390:Rentenf'
+ b"onds Wandelanleihen'DIDDIS:42:1:4+1+1+1+DKDOF;2:DKDFO;2'DIDFOS:43:2:4+1+1+1'"
+ b"DIDFPS:44:2:4+1+1+1'DIDPFS:45:2:4+1+1+1'DIDFES:46:2:4+1+1+1'DIDEFS:47:2:4+1+"
+ b"1+1'DIDOFS:48:2:4+1+1+1'DIFAFS:49:2:4+1+1+1+N:N'DIGBAS:50:1:4+1+1+1'DIGBSS:5"
+ b"1:1:4+1+1+1+J'DIKAUS:52:3:4+1+1+1+N'DIKKAS:53:2:4+1+1+1+N:N:2'DIKKSS:54:3:4+"
+ b"1+1+1'DIKKUS:55:2:4+1+1+1+90:N:J'DIKSBS:56:1:4+3+1+1+J'DIKSPS:57:1:4+3+1+1+;"
+ b':sepade.pain.001.001.02.xsd:sepade.pain.001.002.02.xsd:sepade.pain.001.002.0'
+ b'3.xsd:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:i'
+ b"so?:20022?:tech?:xsd?:pain.001.001.03'DIPAES:58:1:4+1+1'DIPSAS:59:1:4+1+1'DI"
+ b"PSPS:60:1:4+1+1'DIQUOS:61:1:4+1+1+1'DIQUTS:62:1:4+1+1+1'DITLAS:63:1:4+1+1'DI"
+ b"TLFS:64:1:4+1+1+N'DITLFS:65:2:4+1+1+N'DITSPS:66:1:4+1+1+N'DIVVDS:67:1:4+3+1+"
+ b"1'DIVVUS:68:1:4+3+1+1+N:J'DIWAPS:69:1:4+1+1+J:STOP;SLOS;LMTO;MAKT:J:J:GDAY;G"
+ b"TMO;GTHD:J:1:N:N:N:9999999,99:EUR'DIWAPS:70:4:4+1+1+1+J:STOP;STLI;LMTO;MAKT;"
+ b"OCOO;TRST:J:J:J:J:J:GDAY;GTMO;GTHD:J:1:N:N:N:9999999,99:EUR'DIWDGS:71:1:4+1+"
+ b"1+1+J:N:N'DIWGVS:72:1:4+1+1+1+J:730:N'DIWLVS:73:1:4+1+1+1+J:365:N'DINZPS:74:"
+ b"3:4+1+1+1+N:N:4:N:N:::N:J'DIFOPS:75:3:4+1+1+1+N:4:N:N:N::::MAKT:N:J'DIFPOS:7"
+ b"6:3:4+1+1+1+N:4:N:N:::N:J'DIWOPS:77:5:4+1+1+1+0:N:4:N:N::::9999999,99:EUR:ST"
+ b'OP;STLI;LMTO;MAKT;OCOO;TRST:BUYI;SELL;AUCT;CONT;ALNO;DIHA:GDAY;GTMO;GTHD;GTC'
+ b"A;IOCA;OPEN;CLOS;FIKI:N:J'DIWVBS:78:1:4+1+1+1+N:N'DIZDFS:79:2:4+1+1+1'DIZDLS"
+ b":80:2:4+1+1+1'HIAUBS:81:5:4+1+1+1'HIBMES:82:1:4+1+1+1+2:28:2:28:1000:J:N'HIB"
+ b"SES:83:1:4+1+1+1+2:28:2:28'HICAZS:84:1:4+3+1+1+450:N:N:urn?:iso?:std?:iso?:2"
+ b"0022?:tech?:xsd?:camt.052.001.02'HICCMS:85:1:4+1+1+1+1000:J:N'HICCSS:86:1:4+"
+ b"1+1+1'HICDBS:87:1:4+3+1+1+N'HICDES:88:1:4+3+1+1+4:0:9999:0102030612:01020304"
+ b"050607080910111213141516171819202122232425262728293099'HICDLS:89:1:4+3+1+1+0"
+ b":9999:J:J'HICDNS:90:1:4+3+1+1+0:0:9999:J:J:J:J:J:N:J:J:J:0102030612:01020304"
+ b"050607080910111213141516171819202122232425262728293099'HICDUS:91:1:4+3+1+1+1"
+ b":0:9999:1:N:N'HICMBS:92:1:4+1+1+1+N:J'HICMES:93:1:4+1+1+1+1:360:1000:J:N'HIC"
+ b"MLS:94:1:4+1+1+1'HICSAS:95:1:4+1+1+1+1:360'HICSBS:96:1:4+1+1+1+N:J'HICSES:97"
+ b":1:4+1+1+1+1:360'HICSLS:98:1:4+1+1+1+J'HICUBS:99:1:4+3+1+1+J'HICUMS:100:1:4+"
+ b'3+1+1+;:sepade.pain.001.001.02.xsd:sepade.pain.001.002.02.xsd:sepade.pain.00'
+ b'1.002.03.xsd:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?'
+ b":std?:iso?:20022?:tech?:xsd?:pain.001.001.03'HIDMCS:101:1:4+1+1+1+1000:J:N:2"
+ b":28:2:28::urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02'HIDMES:102:1"
+ b":4+1+1+1+2:28:2:28:1000:J:N'HIDSBS:103:1:4+3+1+1+J:J:56'HIDSCS:104:1:4+1+1+1"
+ b"+2:28:2:28::urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02'HIDSES:105"
+ b":1:4+1+1+1+2:28:2:28'HIDSWS:106:1:4+1+1+1+N'HIEKAS:107:2:4+1+1+1+J:J:N:1'HIE"
+ b"KAS:108:3:4+1+1+1+J:J:N:1'HIEKPS:109:1:4+1+1+1+J:J:N'HIFGBS:110:2:4+3+1'HIFG"
+ b"BS:111:3:4+3+1'HIFRDS:112:1:4+1+1'HIFRDS:113:4:4+1+1+1+N:J:N:0:Kreditinstitu"
+ b"t:1:DekaBank'HIKAZS:114:4:4+3+1+360:J'HIKAZS:115:5:4+3+1+360:J:N'HIKDMS:116:"
+ b"2:4+3+0+2048'HIKDMS:117:3:4+3+0+2048'HIKDMS:118:4:4+3+0+2048'HIKIFS:119:1:4+"
+ b"1+1'HIKIFS:120:4:4+1+1+1+J:J'HIKIFS:121:5:4+1+1+1+J:J'HIKIFS:122:6:4+1+1+1+J"
+ b":J'HIMTAS:123:1:4+1+1+1+N'HIMTAS:124:2:4+1+1+1+N:J'HIMTFS:125:1:4+1+1+1'HIMT"
+ b"RS:126:1:4+1+1+1+N'HIMTRS:127:2:4+1+1+1+N:J'HINEAS:128:1:4+1+1+1:2:3:4'HINEZ"
+ b"S:129:3:4+1+1+1+N:N:4:N:N:::N:J'HIWFOS:130:3:4+1+1+1+N:4:N:N:N::::MAKT:N:J'H"
+ b'IWPOS:131:5:4+1+1+1+0:N:4:N:N::::9999999,99:EUR:STOP;STLI;LMTO;MAKT;OCOO;TRS'
+ b"T:BUYI;SELL;AUCT;CONT;ALNO;DIHA:GDAY;GTMO;GTHD;GTCA;IOCA;OPEN;CLOS;FIKI:N:J'"
+ b'HIWSDS:132:5:4+3+1+1+J:A;Inland DAX:B;Inland Sonstige:C;Ausland Europa:D;Aus'
+ b"land Sonstige'HIFPOS:133:3:4+1+1+1+N:4:N:N:::N:J'HIPAES:134:1:4+1+1+1'HIPPDS"
+ b':135:1:4+1+1+1+1:Telekom:Xtra-Card:N:::15;30;50:2:Vodafone:CallYa:N:::15;25;'
+ b'50:3:E-Plus:Free and easy:N:::15;20;30:4:O2:Loop:N:::15;20;30:5:congstar:con'
+ b'gstar:N:::15;30;50:6:blau:blau:N:::15;20;30:8:o.tel.o:o.tel.o:N:::9;19;29:9:'
+ b"SIM Guthaben:SIM Guthaben:N:::15;30;50'HIQTGS:136:1:4+1+1+1'HISALS:137:3:4+3"
+ b"+1'HISALS:138:4:4+3+1'HISALS:139:5:4+3+1'HISPAS:140:1:4+1+1+1+J:N:N:sepade.p"
+ b'ain.001.001.02.xsd:sepade.pain.001.002.02.xsd:sepade.pain.001.002.03.xsd:sep'
+ b'ade.pain.008.002.02.xsd:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.0'
+ b'3:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:'
+ b'20022?:tech?:xsd?:pain.001.001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain'
+ b".008.001.02'HISPAS:141:2:4+1+1+1+J:N:N:N:sepade.pain.001.001.02.xsd:sepade.p"
+ b'ain.001.002.02.xsd:sepade.pain.001.002.03.xsd:sepade.pain.008.002.02.xsd:urn'
+ b'?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.003.03:urn?:iso?:std?:iso?:20022'
+ b'?:tech?:xsd?:pain.008.003.02:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.'
+ b"001.03:urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.008.001.02'HITABS:142:2:4+"
+ b"1+1+1'HITABS:143:3:4+1+1+1'HITABS:144:4:4+1+1+1'HITAUS:145:1:4+1+1+1+N:N:J'H"
+ b"ITAZS:146:1:4+1+1+1'HITAZS:147:2:4+1+1+1'HITMLS:148:1:4+1+1+1'HITSYS:149:1:4"
+ b"+1+1+1+N:N'HIWDUS:150:4:4+3+1+999'HIWFPS:151:2:4+3+1+RENTEN:INVESTMENTFONDS:"
+ b'GENUSSSCHEINE:SPARBRIEFE:UNTERNEHMENSANLEIHEN:EMERGING MARKET ANLEIHEN:STRUK'
+ b'TURIERTE ANLEIHEN:ZERTIFIKATE:AKTIEN:OPTIONSSCHEINE:ALLE ANGEBOTE EIGENES IN'
+ b"STITUT:ALLE ANGEBOTE UEBERGEORD. INSTITUTE'HIWOAS:152:2:4+1+1+J:STOP;SLOS;LM"
+ b"TO;MAKT:J:J:GDAY;GTMO;GTHD:J:1:N:N:N:9999999,99:EUR'HIWOAS:153:4:4+1+1+1+J:S"
+ b'TOP;STLI;LMTO;MAKT;OCOO;TRST:J:J:J:J:J:GDAY;GTMO;GTHD:J:1:N:N:N:9999999,99:E'
+ b"UR'HIWPDS:154:3:4+3+1+J'HIWPDS:155:5:4+1+1+J:N:N'HIWPRS:156:1:4+3+1+J:J:N:N:"
+ b':Aktien:Festverzinsliche Wertpapiere:Fonds:Fremdw\xe4hrungsanleihen:Genusss'
+ b"cheine:Indexzertifikate:Optionsscheine:Wandel- und Optionsanleihen cum'HIWPS"
+ b"S:157:1:4+3+1+J'HIWSOS:158:4:4+3+1+1+J:J:90:1:2:3:4:5:6:7:8:9:10:11'HIWSOS:1"
+ b"59:5:4+3+1+1+J:J:90:1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17'HITANS:160:1:4"
+ b'+1+1+1+J:N:0:0:920:2:smsTAN:smsTAN:6:1:TAN-Nummer:3:1:J:J:900:2:iTAN:iTAN:6:'
+ b"1:TAN-Nummer:3:1:J:J'HITANS:161:3:4+1+1+1+J:N:0:910:2:HHD1.3.0:chipTAN manue"
+ b'll:6:1:TAN-Nummer:3:1:J:2:0:N:N:N:00:0:1:911:2:HHD1.3.0OPT:chipTAN optisch:6'
+ b':1:TAN-Nummer:3:1:J:2:0:N:N:N:00:0:1:912:2:HHD1.3.0USB:chipTAN USB:6:1:TAN-N'
+ b'ummer:3:1:J:2:0:N:N:N:00:0:1:920:2:smsTAN:smsTAN:6:1:TAN-Nummer:3:1:J:2:0:N:'
+ b'N:N:00:2:5:921:2:pushTAN:pushTAN:6:1:TAN-Nummer:3:1:J:2:0:N:N:N:00:2:2:900:2'
+ b":iTAN:iTAN:6:1:TAN-Nummer:3:1:J:2:0:N:N:N:00:0:0'HIPINS:162:1:4+1+1+0+5:5:6:"
+ b'USERID:CUSTID:HKAUB:J:HKBME:J:HKBSE:J:HKCAZ:N:HKCCM:J:HKCCS:J:HKCDB:N:HKCDE:'
+ b'J:HKCDL:J:HKCDN:J:HKCDU:J:HKCMB:N:HKCME:J:HKCML:J:HKCSA:J:HKCSB:N:HKCSE:J:HK'
+ b'CSL:J:HKCSU:J:HKCUB:N:HKCUM:J:HKDMC:J:HKDME:J:HKDSB:N:HKDSC:J:HKDSE:J:HKDSW:'
+ b'J:HKEKA:N:HKEKP:N:HKFGB:N:HKFRD:N:HKKAZ:N:HKKDM:J:HKKIF:N:HKMTA:J:HKMTF:N:HK'
+ b'MTR:J:HKNEA:N:HKNEZ:J:HKWFO:J:HKWPO:J:HKFPO:J:HKWSD:N:HKPAE:J:HKPPD:J:HKQTG:'
+ b'N:HKSAL:N:HKSPA:N:HKTAB:N:HKTAU:N:HKTAZ:N:HKTML:N:HKTSY:N:HKUTA:N:HKWDU:N:HK'
+ b'WFP:N:HKWOA:J:HKWPD:N:HKWPK:N:HKWPR:N:HKWPS:J:HKWSO:N:HKTAN:N:DKBKD:N:DKBKU:'
+ b'N:DKBUM:N:DKFDA:N:DKPAE:N:DKPSA:J:DKPSP:N:DKTLA:N:DKTLF:J:DKTSP:N:DKWAP:N:DK'
+ b'ALE:J:DKALL:J:DKALN:J:DKANA:J:DKANL:J:DKBAZ:N:DKBVA:J:DKBVB:J:DKBVD:N:DKBVK:'
+ b'N:DKBVP:J:DKBVR:J:DKBVS:N:DKDFA:N:DKDFB:N:DKDFC:J:DKDFD:N:DKDFL:J:DKDFU:N:DK'
+ b'DIH:J:DKDFS:N:DKDDI:N:DKDFO:J:DKDFP:J:DKDPF:N:DKDFE:J:DKDEF:N:DKDOF:N:DKFAF:'
+ b'N:DKGBA:J:DKGBS:J:DKKAU:N:DKKKA:N:DKKKS:N:DKKKU:N:DKKSB:N:DKKSP:N:DKQUO:N:DK'
+ b'QUT:N:DKVVD:N:DKVVU:N:DKWDG:N:DKWGV:N:DKWLV:N:DKNZP:N:DKFOP:N:DKFPO:N:DKWOP:'
+ b"N:DKWVB:N:DKZDF:J:DKZDL:J:DKWOK:N:DKWDH:N:DKBVE:J:DKPTZ:N:DKEEA:N'HIAZSS:163"
+ b':1:4+1+1+1+1:N:::::::::::HKFGB;2;0;1;811:DKDFU;1;0;1;811:DKDOF;2;0;1;811:DKD'
+ b'FB;1;0;1;811:HKCSU;1;0;1;811:HKTAB;4;0;1;811:DKDIH;1;0;1;811:DKVVD;1;0;1;811'
+ b':HKCDB;1;0;1;811:HKWPS;1;0;1;811:DKDFP;2;0;1;811:DKTLA;1;0;1;811:DKBVP;1;0;1'
+ b';811:DKKSB;1;0;1;811:HKDME;1;0;1;811:HKWPD;3;0;1;811:DKEEA;1;0;1;811:DKFAF;2'
+ b';0;1;811:HKCSL;1;0;1;811:HKCML;1;0;1;811:HKCUM;1;0;1;811:DKDEF;2;0;1;811:HKF'
+ b'GB;3;0;1;811:HKCCM;1;0;1;811:DKDFD;1;0;1;811:HKTAU;1;0;1;811:HKWFP;2;0;1;811'
+ b':DKWAP;1;0;1;811:DKALE;1;0;1;811:HKCCS;1;0;1;811:DKBVS;1;0;1;811:HKFRD;1;0;1'
+ b';811:DKGBA;1;0;1;811:DKZDL;2;0;1;811:HKTAB;2;0;1;811:HKMTA;1;0;1;811:DKBAZ;2'
+ b';0;1;811:HKCME;1;0;1;811:HKQTG;1;0;1;811:DKWOK;1;0;1;811:DKALN;1;0;1;811:DKF'
+ b'OP;3;0;1;811:DKBVS;2;0;1;811:DKBUM;3;0;1;811:HKWSO;4;0;1;811:HKTAZ;1;0;1;811'
+ b':HKPAE;1;0;1;811:DKWGV;1;0;1;811:HKNEZ;3;0;1;811:DKDFU;2;0;1;811:HKDMC;1;0;1'
+ b';811:HKAUB;5;0;1;811:DKWDH;1;0;1;811:DKPAE;1;0;1;811:HKTAB;3;0;1;811:HKDSC;1'
+ b';0;1;811:DKWVB;1;0;1;811:HKBSE;1;0;1;811:DKWAP;4;0;1;811:DKDFL;1;0;1;811:HKW'
+ b'PO;5;0;1;811:HKTAZ;2;0;1;811:DKWOP;5;0;1;811:HKMTR;2;0;1;811:HKDSE;1;0;1;811'
+ b':HKTSY;1;0;1;811:DKKSP;1;0;1;811:HKWOA;4;0;1;811:DKTSP;1;0;1;811:HKKIF;1;0;1'
+ b';811:HKKDM;4;0;1;811:DKGBS;1;0;1;811:HKWPR;1;0;1;811:HKSPA;1;0;1;811:HKWFO;3'
+ b';0;1;811:DKWLV;1;0;1;811:HKKAZ;4;0;1;811:HKKIF;4;0;1;811:HKKAZ;5;0;1;811:HKE'
+ b'KP;1;0;1;811:DKTLF;2;0;1;811:DKFPO;3;0;1;811:HKPPD;1;0;1;811:HKMTA;2;0;1;811'
+ b':HKCSA;1;0;1;811:DKWDG;1;0;1;811:DKDFE;2;0;1;811:DKQUO;1;0;1;811:HKMTR;1;0;1'
+ b';811:DKALL;1;0;1;811:DKPSP;1;0;1;811:HKIDN;2;0;1;811:HKWPD;5;0;1;811:HKCER;1'
+ b';0;1;811:DKBKD;4;0;1;811:HKDSW;1;0;1;811:HKCMB;1;0;1;811:DKKKS;3;0;1;811:HKW'
+ b'SO;5;0;1;811:HKFPO;3;0;1;811:HKSAL;3;0;1;811:HKCDE;1;0;1;811:DKBVD;1;0;1;811'
+ b':HKSPA;2;0;1;811:HKFRD;4;0;1;811:DKTLF;1;0;1;811:DKZDF;2;0;1;811:DKKKU;2;0;1'
+ b';811:HKEKA;3;0;1;811:DKPTZ;1;0;1;811:DKBVK;1;0;1;811:DKPSA;1;0;1;811:HKMTF;1'
+ b';0;1;811:DKDFA;1;0;1;811:HKWOA;2;0;1;811:DKDDI;1;0;1;811:DKNZP;3;0;1;811:HKC'
+ b'DL;1;0;1;811:HKCSE;1;0;1;811:HKSAL;4;0;1;811:HKWDU;4;0;1;811:DKKAU;3;0;1;811'
+ b':DKANL;1;0;1;811:HKNEA;1;0;1;811:HKKDM;2;0;1;811:DKVVU;1;0;1;811:HKCDU;1;0;1'
+ b';811:HKKIF;6;0;1;811:HKCUB;1;0;1;811:HKTAN;1;0;1;811:DKDFO;2;0;1;811:DKQUT;1'
+ b';0;1;811:HKCDN;1;0;1;811:HKEKA;2;0;1;811:DKBVA;1;0;1;811:DKKKA;2;0;1;811:DKD'
+ b'PF;2;0;1;811:DKBVR;1;0;1;811:DKBVE;1;0;1;811:DKBVB;1;0;1;811:HKSAL;5;0;1;811'
+ b':DKANA;1;0;1;811:HKKDM;3;0;1;811:DKBKU;3;0;1;811:HKWSD;5;0;1;811:HKCAZ;1;0;1'
+ b';811:HKCSB;1;0;1;811:HKTML;1;0;1;811:HKKIF;5;0;1;811:DKDFC;1;0;1;811:DKDFS;2'
+ b";0;1;811:HKDSB;1;0;1;811:HKBME;1;0;1;811'HIVISS:164:1:4+1+1+1+1;L;;\xdcberw"
+ b'eisung??;;;;2;L;;Dauerauftrag;;;;2;L;;\xe4ndern??;;;;3;L;;Dauerauftrag??;;;'
+ b';4;L;;Dauerauftrag;;;;4;L;;l\xf6schen??;;;;5;L;;DA-Aussetzung;;;;5;L;;bearb'
+ b'eiten??;;;;6;L;;Termin-;;;;6;L;;\xfcberweisung??;;;;7;L;;Termin\xfcberwei-'
+ b';;;;7;L;;sung \xe4ndern??;;;;8;L;;Termin\xfcberwei-;;;;8;L;;sung l\xf6sch'
+ b'en??;;;;9;L;;\xdcbertrag??;;;;10;L;;Lastschrift-;;;;10;L;;widerspruch??;;;;'
+ b'11;L;;Sammel-;;;;11;L;;lastschrift??;;;;12;L;;Einzel-;;;;12;L;;lastschrift??'
+ b';;;;13;L;;Betrag?:;;;;13;R;16;#;;;;14;L;;IBAN Empf\xe4nger?:;;;;14;R;10;#;;'
+ b';;15;L;;Konto Empf\xe4nger?:;;;;15;R;10;#;;;;16;L;;Konto Zahler?:;;;;16;R;1'
+ b'0;#;;;;17;L;;Anzahl Posten?:;;;;17;R;4;#;;;;18;L;;IBAN Zahler?:;;;;18;R;10;#'
+ b';;;;19;L;;Betrag Vorkomma?:;;;;19;R;16;#;;;;20;L;;Abo-Ladeauftrag;;;;20;L;;a'
+ b'nlegen??;;;;21;L;;Abo-Ladeauftrag;;;;21;L;;l\xf6schen??;;;;22;L;;Abo-Ladeau'
+ b'ftrag;;;;22;L;;\xe4ndern??;;;;23;L;;Kartennummer?:;;;;23;R;10;#;;;;24;L;;An'
+ b'meldename;;;;24;L;;\xe4ndern??;;;;25;L;;Anmeldename;;;;25;L;;l\xf6schen??;'
+ b';;;26;L;;FondsSparplan;;;;26;L;;l\xf6schen??;;;;27;L;;FondsSparplan;;;;27;L'
+ b';;\xe4ndern??;;;;28;L;;ISIN?:;;;;28;R;12;#;;;;29;L;;Depot?:;;;;29;R;10;#;;;'
+ b';30;L;;Wertpapier-;;;;30;L;;auftrag l\xf6schen??;;;;31;L;;Wertpapier-;;;;31'
+ b';L;;auftrag??;;;;32;L;;FondsSparplan;;;;32;L;;neu??;;;;33;L;;Autorisierung;;'
+ b';;33;L;;Direkthandel??;;;;34;L;;WKN / ISIN?:;;;;34;R;12;#;;;;35;L;;L\xe4nde'
+ b'rfreischal-;;;;35;L;;tung verwalten??;;;;36;L;;Auslandseinsatz;;;;36;L;;sper'
+ b'ren/entsper??;;;;37;L;;Postfachkonten;;;;37;L;;verwalten??;;;;38;L;;Gutschei'
+ b'nkauf??;;;;39;L;;DSRZ-Datei;;;;39;L;;freigeben??;;;;40;L;;DSRZ-Datei;;;;40;L'
+ b';;l\xf6schen??;;;;41;L;;EU-\xdcberweisung??;;;;42;L;;Auslands-;;;;42;L'
+ b';;\xfcberweisung??;;;;43;L;;Konto/IBAN?:;;;;43;R;10;#;;;;44;L;;Sammel-;;;;4'
+ b'4;L;;\xfcberweisung??;;;;45;L;;term. Sammel-;;;;45;L;;\xfcberweisung??;;;;'
+ b'46;L;;term. Sammel-;;;;46;L;;\xfcberw. l\xf6schen??;;;;47;L;;Eil\xfcberwe'
+ b'isung??;;;;48;L;;Festpreisorder??;;;;49;L;;Mitteilung??;;;;50;L;;Einzel-;;;;'
+ b'50;L;;lastschrift??;;;;51;L;;Neuemission;;;;51;L;;zeichnen??;;;;52;L;;Handy-'
+ b'Aufladung??;;;;53;L;;Mobil Nr.?:;;;;53;R;16;#;;;;54;L;;Wertpapier-;;;;54;L;;'
+ b'fondsorder??;;;;55;L;;Wertpapierorder;;;;55;L;;\xe4ndern??;;;;56;L;;Wertpap'
+ b'ierorder??;;;;57;L;;Wertpapierorder;;;;57;L;;streichen??;;;;58;L;;Adress'
+ b'\xe4nderung??;;;;59;L;;Deka-Depot;;;;59;L;;freischalten??;;;;60;L;;Kontowec'
+ b'ker;;;;60;L;;registrieren??;;;;61;L;;Produktverkauf??;;;;62;L;;Produktverkau'
+ b'f??;;;;63;L;;Auftragstitel?:;;;;63;L;16;#;;;;64;L;;Kauf Sorten und;;;;64;L;;'
+ b'Edelmetalle??;;;;65;L;;Auftrag;;;;65;L;;senden??;;;;66;L;;Konto/IBAN?:;;;;66'
+ b';L;16;#;;;;67;L;;IBAN Empf\xe4nger?:;;;;67;L;16;#;;;;68;L;;Freigabe;;;;68;L'
+ b';;des Auftrags?:;;;;69;L;;L\xf6schen;;;;69;L;;des Auftrags??;;;;70;L;32;#;;'
+ b';;71;L;;Einzelauftrag;;;;71;L;;Ausland??;;;;72;L;;paydirekt;;;;72;L;;Registr'
+ b'ierung??;;;;73;L;;\xc4nderung;;;;73;L;;paydirekt-Konto??;;;;74;L;;\xc4nd. '
+ b'paydirekt;;;;74;L;;Benutzername??;;;;75;L;;\xc4nd. paydirekt;;;;75;L;;Passw'
+ b'ort??;;;;76;L;;paydirekt;;;;76;L;;Entsperren??;;;;77;L;;Postfach-L\xf6sch-;'
+ b';;;77;L;;regel anlegen??;;;;78;L;;Postfach-L\xf6sch-;;;;78;L;;regel \xe4nd'
+ b'ern??;;;;79;L;;Postfach-L\xf6sch-;;;;79;L;;regel l\xf6schen??;;;;80;L;;Eil'
+ b'zahlung??;;;:DKALE;1;811;20;;;23;1;3;13;1;7,1;65;;:DKALL;1;811;21;;;65;;:DKA'
+ b'LN;1;811;22;;;23;1;3;13;1;7,1;65;;:DKANA;1;811;24;;;65;;:DKANL;1;811;25;;;65'
+ b';;:DKDFC;1;811;26;;;28;1;4;29;1;3,1;65;;:DKDFE;1;811;27;;;28;1;4;29;1;3,1;65'
+ b';;:DKDFE;2;811;27;;;28;1;4;29;1;3,1;65;;:DKDFL;1;811;30;;;29;1;3,1;65;;:DKDF'
+ b'O;1;811;31;;;28;1;5;29;1;4,1;65;;:DKDFO;2;811;31;;;28;1;5;29;1;4,1;65;;:DKDF'
+ b'P;1;811;32;;;28;1;4;29;1;3,1;65;;:DKDFP;2;811;32;;;28;1;4;29;1;3,1;65;;:DKDI'
+ b'H;1;811;33;;;29;1;2,1;34;1;3,2;65;;:DKGBA;1;811;35;;;23;1;3;65;;:DKGBS;1;811'
+ b';36;;;23;1;3;65;;:DKZDF;1;811;39;;;13;1;4,2;65;;:DKZDF;2;811;39;;;13;1;4,2;6'
+ b'5;;:DKZDL;1;811;40;;;65;;:DKZDL;2;811;40;;;65;;:HKAUB;5;811;42;;;66;3;T.16;1'
+ b'9;3;T.18;65;;:HKBME;1;811;11;;;13;1;3,1;17;4;NbOfTxs.1;65;;:HKBSE;1;811;12;;'
+ b';18;4;IBAN.2;13;4;InstdAmt.1;65;;:HKCCM;1;811;44;;;13;1;3,1;17;4;NbOfTxs.1;6'
+ b'5;;:HKCCS;1;811;1;;;14;4;IBAN.2;13;4;InstdAmt.1;65;;:HKCDE;1;811;3;;;14;4;IB'
+ b'AN.2;13;4;InstdAmt.1;65;;:HKCDL;1;811;4;;;65;;:HKCDN;1;811;2;;;14;4;IBAN.2;1'
+ b'3;4;InstdAmt.1;65;;:HKCDU;1;811;5;;;14;4;IBAN.2;13;4;InstdAmt.1;65;;:HKCME;1'
+ b';811;45;;;13;1;3,1;17;4;NbOfTxs.1;65;;:HKCML;1;811;46;;;65;;:HKCSA;1;811;7;;'
+ b';14;4;IBAN.2;13;4;InstdAmt.1;65;;:HKCSE;1;811;6;;;14;4;IBAN.2;13;4;InstdAmt.'
+ b'1;65;;:HKCSL;1;811;8;;;65;;:HKCUM;1;811;9;;;14;4;IBAN.2;13;4;InstdAmt.1;65;;'
+ b':HKDMC;1;811;11;;;13;1;3,1;17;4;NbOfTxs.1;65;;:HKDME;1;811;11;;;13;1;3,1;17;'
+ b'4;NbOfTxs.1;65;;:HKDSC;1;811;12;;;18;4;IBAN.2;13;4;InstdAmt.1;65;;:HKDSE;1;8'
+ b'11;12;;;18;4;IBAN.2;13;4;InstdAmt.1;65;;:HKDSW;1;811;10;;;65;;:HKFPO;1;811;4'
+ b'8;;;29;1;2,1;65;;:HKFPO;3;811;48;;;29;1;2,1;65;;:HKKDM;2;811;49;;;65;;:HKKDM'
+ b';3;811;49;;;65;;:HKKDM;4;811;49;;;65;;:HKNEZ;1;811;51;;;29;1;2,1;65;;:HKNEZ;'
+ b'3;811;51;;;29;1;2,1;65;;:HKPPD;1;811;52;;;13;1;5,1;53;1;4;65;;:HKWFO;1;811;5'
+ b'4;;;29;1;2,1;65;;:HKWFO;3;811;54;;;29;1;2,1;65;;:HKWOA;2;811;55;;;29;1;2,1;6'
+ b'5;;:HKWOA;4;811;55;;;29;1;2,1;65;;:HKWPO;2;811;56;;;29;1;2,1;65;;:HKWPO;4;81'
+ b'1;56;;;29;1;2,1;65;;:HKWPO;5;811;56;;;29;1;2,1;65;;:HKWPS;1;811;57;;;29;1;2,'
+ b'1;65;;:DKBVA;1;811;73;;;65;;:DKBVB;1;811;74;;;65;;:DKBVP;1;811;75;;;65;;:DKB'
+ b'VR;1;811;72;;;65;;:DKBVR;2;811;72;;;65;;:DKBVE;1;811;76;;;65;;:HKCSU;1;811;8'
+ b"0;;;14;4;IBAN.2;13;4;InstdAmt.1;65;;'HIUPA:165:4:4+2233445566+0+0++PERSNR001"
+ b"43218765090'HIUPD:166:6:4+0987654321::280:15050500+DE78150505000987654321+33"
+ b'44556677+10+EUR+McZeus+Hermes+Sparkassenbuch++HKSAK:1+HKISA:1+HKSSP:1+HKPAE:'
+ b'1+HKTSY:1+HKTAB:1+HKTAU:1+HKSPA:1+HKCAZ:1+HKCUB:1+DKPSA:1+DKPSP:1+HKTAN:1+DK'
+ b'ANA:1+DKANL:1+DKKBA:1+DKDKL:1+DKBDK:1+HKFRD:1+HKKDM:1+HKKAZ:1+HKKIF:1+HKSAL:'
+ b'1+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++{"umsltzt"'
+ b'?:"2017-12-30-01.12.54.799483"}\'HIUPD:167:6:4+1234567890::280:15050500+D'
+ b'E58150505001234567890+2233445566+1+EUR+McZeus+Hermes+IndividualKonto++HKSAK:'
+ b'1+HKISA:1+HKSSP:1+HKPAE:1+HKTSY:1+HKTAB:1+HKTAU:1+HKSPA:1+HKCAZ:1+HKCCS:1+HK'
+ b'CDB:1+HKCDE:1+HKCDL:1+HKCDN:1+HKCDU:1+HKCSA:1+HKCSB:1+HKCSE:1+HKCSL:1+HKCUB:'
+ b'1+HKCUM:1+HKDSB:1+HKDSW:1+HKEKP:1+HKPPD:1+DKPSA:1+DKPSP:1+HKTAN:1+DKANA:1+DK'
+ b'ANL:1+DKKBA:1+DKDKL:1+DKBDK:1+DKALE:1+DKALL:1+DKALN:1+DKBAZ:1+DKBVK:1+DKTCK:'
+ b'1+DKZDF:1+DKZDL:1+HKFRD:1+HKKDM:1+HKAUB:1+HKKAZ:1+HKKIF:1+HKSAL:1+++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
+ b'++++++++++++++++++++++++++++++{"umsltzt"?:"2018-07-30-11.16.03.582678"}\''
+ b"HISYN:168:4:5+oIm3BlHv6mQBAADYgbPpp?+kWrAQA'HNSHA:169:2+1259150''HNHBS:170:1"
+ b"+1'")
+
+SIMPLE_EXAMPLE = \
+(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'")