From: Henryk Plötz Date: Fri, 10 Aug 2018 22:01:42 +0000 (+0200) Subject: Fix and document serializer X-Git-Tag: v2.0.0~1^2~124 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2af2fb93c826ae204e270e0f5bf4b3261fae9cfb;p=thirdparty%2Fpython-fints.git Fix and document serializer --- diff --git a/docs/developer.rst b/docs/developer.rst index 89da01b..e49346c 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -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 ---------------------- diff --git a/fints/formals.py b/fints/formals.py index eca4516..a6a6899 100644 --- a/fints/formals.py +++ b/fints/formals.py @@ -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 diff --git a/fints/parser.py b/fints/parser.py index 80ff3f0..5cafd83 100644 --- a/fints/parser.py +++ b/fints/parser.py @@ -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") diff --git a/tests/test_message_serializer.py b/tests/test_message_serializer.py index 838f3f8..1542c96 100644 --- a/tests/test_message_serializer.py +++ b/tests/test_message_serializer.py @@ -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