]> git.ipfire.org Git - thirdparty/python-fints.git/commitdiff
Fix and document serializer
authorHenryk Plötz <henryk@ploetzli.ch>
Fri, 10 Aug 2018 22:01:42 +0000 (00:01 +0200)
committerRaphael Michel <mail@raphaelmichel.de>
Mon, 3 Dec 2018 18:34:17 +0000 (19:34 +0100)
docs/developer.rst
fints/formals.py
fints/parser.py
tests/test_message_serializer.py

index 89da01b4c5032f35d09470a52a9a122ea9bd3b9e..e49346c63a6eccb2377a8d4f671046f87466fb1c 100644 (file)
@@ -26,8 +26,8 @@ Example usage:
    SegmentSequence([fints.segments.HNHBK3(header=fints.formals.SegmentHeader('HNHBK', 1, 3), message_size='000000000428', hbci_version=300, dialogue_id='430711670077=043999659571CN9D=', message_number=2, reference_message=fints.formals.ReferenceMessage(dialogue_id='430711670077=043999659571CN9D=', message_number=2)), fints.segments.HNVSK3(header=fints.formals.SegmentHeader('HNVSK', 998, 3), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='998', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), encryption_algorithm=fints.formals.EncryptionAlgorithm(usage_encryption='2', operation_mode='2', encryption_algorithm='13', algorithm_parameter_value=b'00000000', algorithm_parameter_name='5', algorithm_parameter_iv_name='1'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0), compression_function='0'), fints.segments.HNVSD1(header=fints.formals.SegmentHeader('HNVSD', 999, 1), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader('HNSHK', 2, 4), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='999', security_reference='9166926', security_application_area='1', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1'), signature_algorithm=fints.formals.SignatureAlgorithm(usage_signature='6', signature_algorithm='10', operation_mode='16'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0)), fints.segments.HIRMG2(header=fints.formals.SegmentHeader('HIRMG', 3, 2), response=[fints.formals.Response(response_code='0010', reference_element=None, response_text='Nachricht entgegengenommen.'), fints.formals.Response(response_code='0100', reference_element=None, response_text='Dialog beendet.')]), fints.segments.HNSHA2(header=fints.formals.SegmentHeader('HNSHA', 4, 2), security_reference='9166926')])), fints.segments.HNHBS1(header=fints.formals.SegmentHeader('HNHBS', 5, 1), message_number=2)])
    >>> from fints.parser import FinTS3Serializer
    >>> FinTS3Serializer().serialize_message(s)
+   b"HNHBK:1:3+000000000428+300+430711670077=043999659571CN9D=+2+430711670077=043999659571CN9D=:2'HNVSK:998:3+PIN:1+998+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+2:2:13:@8@00000000:5:1+280:15050500:hermes:S:0:0+0'HNVSD:999:1+@195@HNSHK:2:4+PIN:1+999+9166926+1+1+2::oIm3BlHv6mQBAADYgbPpp?+kWrAQA+1+1+1:999:1+6:10:16+280:15050500:hermes:S:0:0'HIRMG:3:2+0010::Nachricht entgegengenommen.+0100::Dialog beendet.'HNSHA:4:2+9166926''HNHBS:5:1+2'"
 
-## FIXME: Implement :)
 
 FinTS Segment Sequence
 ----------------------
index eca4516ac8954cf810d14de5b0dea5b956022f3f..a6a6899f76e7b3264f10a037703a2c2989bda32d 100644 (file)
@@ -159,7 +159,7 @@ class Field:
 
     def render(self, value):
         if value is None:
-            return ""
+            return None
 
         return self._render_value(value)
 
@@ -351,6 +351,8 @@ class PasswordField(AlphanumericField):
         return str(value)
 
 class SegmentSequence:
+    """A sequence of FinTS3Segment objects"""
+
     def __init__(self, segments = None):
         if isinstance(segments, bytes):
             from .parser import FinTS3Parser
@@ -359,6 +361,10 @@ class SegmentSequence:
             segments = [parser.parse_segment(segment) for segment in data]
         self.segments = segments or []
 
+    def render_bytes(self) -> bytes:
+        from .parser import FinTS3Serializer
+        return FinTS3Serializer().serialize_message(self)
+
     def __repr__(self):
         return "{}({!r})".format(self.__class__.__name__, self.segments)
 
@@ -373,6 +379,24 @@ class SegmentSequence:
             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) )
 
+    def find_segments(self, type=None, version=None, callback=None, recurse=True):
+        """Yields an iterable of all matching segments.
+
+        :param type: Either a str specifying a segment type (such as 'HNHBK'), or a list or tuple of strings.
+                     If a list/tuple is specified, segments returning any matching type will be returned.
+        :param version: Either an int specifying a segment version, or a list or tuple of ints.
+                        If a list/tuple is specified, segments returning any matching version will be returned.
+        :param callback: A callable that will be given the segment as its sole argument and must return a booleans indicating whether to return this segment.
+        :param recurse: If True (the default), recurse into SegmentSequenceField values, otherwise only look at segments in this SegmentSequence.
+
+        The match results of all given parameters will be AND-combined.
+        """
+
+    def find_segment_first(self, *args, **kwargs):
+        """Finds the first matching segment.
+
+        Same parameters as find_segments(), but only returns the first match, or None if no match is found."""
+
 class SegmentSequenceField(DataElementField):
     type = 'sf'
 
@@ -382,6 +406,9 @@ class SegmentSequenceField(DataElementField):
         else:
             return SegmentSequence(value)
 
+    def _render_value(self, value):
+        return value.render_bytes()
+
 
 class ContainerMeta(type):
     @classmethod
index 80ff3f0a272d46453194be943a30153974fd0cc4..5cafd83807382a0b25fdc20cde2ff849828e44d8 100644 (file)
@@ -112,7 +112,11 @@ class ParserState:
         yield Token.EOF, b''
 
 class FinTS3Parser:
-    def parse_message(self, data):
+    """Parser for FinTS/HBCI 3.0 messages
+    """
+
+    def parse_message(self, data: bytes) -> SegmentSequence:
+        "Takes a FinTS 3.0 message as byte array, and returns a parsed segment sequence"
         if isinstance(data, bytes):
             data = self.explode_segments(data)
 
@@ -276,7 +280,11 @@ class FinTS3Parser:
         return segments
 
 class FinTS3Serializer:
-    def serialize_message(self, message):
+    """Serializer for FinTS/HBCI 3.0 messages
+    """
+
+    def serialize_message(self, message: SegmentSequence) -> bytes:
+        "Serialize a message (as SegmentSequence, list of FinTS3Segment, or FinTS3Segment) into a byte array"
         if isinstance(message, FinTS3Segment):
             message = [message]
         if isinstance(message, (list, tuple, Iterable)):
@@ -292,7 +300,7 @@ class FinTS3Serializer:
     def serialize_segment(self, segment):
 
         seg = []
-        skipping_end = False
+        filler = []
 
         for name,field in segment._fields.items():
             repeat = field.count != 1
@@ -310,12 +318,13 @@ class FinTS3Serializer:
                 elif val is None:
                     empty = True
 
-            if skipping_end and not empty:
-                raise ValueError("Inconsistency during serialization: Field {}.{} not empty, but a field before it was".format(segment.__class__.__name__, name))
-
             if empty:
-                skipping_end = True
+                filler.append(None)
                 continue
+            else:
+                if filler:
+                    seg.extend(filler)
+                    filler.clear()
 
             if not constructed:
                 if repeat:
@@ -324,10 +333,8 @@ class FinTS3Serializer:
                     seg.append( field.render(getattr(segment, name)) )
             else:
                 if repeat:
-                    inner = []
                     for val in getattr(segment, name):
-                        inner.extend( self.serialize_deg(val) )
-                    seg.append(inner)
+                        seg.append( self.serialize_deg(val) )
                 else:
                     seg.append( self.serialize_deg(getattr(segment, name)) )
 
@@ -401,8 +408,10 @@ class FinTS3Serializer:
             return re.sub(r"([+:'@?])", r"?\1", val).encode('iso-8859-1')
         elif isinstance(val, bytes):
             return "@{}@".format(len(val)).encode('us-ascii') + val
+        elif val is None:
+            return b''
         else:
-            raise TypeError("Can only escape str and bytes")
+            raise TypeError("Can only escape str, bytes and None")
 
 
 
index 838f3f8df3100fda13962f98a24644bf4aab4a4b..1542c96a9fa5b3883aea94e0c04537b785d05b9f 100644 (file)
@@ -3,6 +3,8 @@ from fints.segments import FinTS3Segment
 from fints.formals import NumericField
 import pytest
 
+from conftest import SIMPLE_EXAMPLE
+
 def test_serialize_1():
     class ITST1(FinTS3Segment):
         a = NumericField(count=3)
@@ -39,6 +41,13 @@ def test_implode_1():
 
     assert FinTS3Parser.explode_segments(s) == m
 
+def test_implode_roundtrip_simple():
+    segments = FinTS3Parser.explode_segments(SIMPLE_EXAMPLE)
+    assert FinTS3Serializer.implode_segments(segments) == SIMPLE_EXAMPLE
+
+    message = FinTS3Parser().parse_message(segments)
+    assert FinTS3Serializer().serialize_message(message) == SIMPLE_EXAMPLE
+
 def test_escape():
     assert b"a" == FinTS3Serializer.escape_value('a')
 
@@ -50,3 +59,10 @@ def test_escape():
 
     with pytest.raises(TypeError):
         FinTS3Serializer.escape_value(1)
+
+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(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), encryption_algorithm=fints.formals.EncryptionAlgorithm(usage_encryption='2', operation_mode='2', encryption_algorithm='13', algorithm_parameter_value=b'00000000', algorithm_parameter_name='5', algorithm_parameter_iv_name='1'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0), compression_function='0'), fints.segments.HNVSD1(header=fints.formals.SegmentHeader('HNVSD', 999, 1), data=SegmentSequence([fints.segments.HNSHK4(header=fints.formals.SegmentHeader('HNSHK', 2, 4), security_profile=fints.formals.SecurityProfile(security_method='PIN', security_method_version=1), security_function='999', security_reference='9166926', security_application_area='1', security_role='1', security_identification_details=fints.formals.SecurityIdentificationDetails(name_party='2', cid=None, identifier_party='oIm3BlHv6mQBAADYgbPpp+kWrAQA'), security_reference_number=1, security_datetime=fints.formals.SecurityDateTime(datetime_type='1'), hash_algorithm=fints.formals.HashAlgorithm(usage_hash='1', hash_algorithm='999', algorithm_parameter_name='1'), signature_algorithm=fints.formals.SignatureAlgorithm(usage_signature='6', signature_algorithm='10', operation_mode='16'), key_name=fints.formals.KeyName(bank_identifier=fints.formals.BankIdentifier(country_identifier='280', bank_code='15050500'), user_id='hermes', key_type='S', key_number=0, key_version=0)), fints.segments.HIRMG2(header=fints.formals.SegmentHeader('HIRMG', 3, 2), response=[fints.formals.Response(response_code='0010', reference_element=None, response_text='Nachricht entgegengenommen.'), fints.formals.Response(response_code='0100', reference_element=None, response_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)])
+    
+    assert FinTS3Serializer().serialize_message(s) == SIMPLE_EXAMPLE