diff --git a/api/ErrorHandler.py b/api/ErrorHandler.py new file mode 100644 index 0000000..a6d2e10 --- /dev/null +++ b/api/ErrorHandler.py @@ -0,0 +1,24 @@ +import logging + +from fastapi import Response + +from api.ErrorResponseContentProvider import ErrorResponseContentProvider +from api.errors import ApiError + + +class ErrorHandler: + + def __init__(self, response_provider: ErrorResponseContentProvider): + self.response_provider = response_provider + self.logger = logging.getLogger(__name__) + + def handle_error(self, error: type[ApiError]) -> Response: + """ + Handles an ApiError and creates an according Response using a ErrorResponseContentProvider. + """ + error.log_error() + return Response( + self.response_provider.provide_error_response_content(error.message), + status_code=error.status_code, + media_type="application/xml; charset=utf-8", + ) diff --git a/api/ErrorHandlerTest.py b/api/ErrorHandlerTest.py new file mode 100644 index 0000000..f983d17 --- /dev/null +++ b/api/ErrorHandlerTest.py @@ -0,0 +1,60 @@ +import logging +import unittest + +from api.ErrorHandler import ErrorHandler +from api.ErrorResponseContentProvider import ErrorResponseContentProvider +from api.errors.ApiError import ApiError +from api.errors.InternalServerError import InternalServerError +from api.ojp1.ErrorResponseContentProviderOjp1 import ErrorResponseContentProviderOjp1 +from api.ojp2.ErrorResponseContentProviderOjp2 import ErrorResponseContentProviderOjp2 + + +class ErrorHandlerTest(unittest.TestCase): + + logger = logging.getLogger(__name__) + + def test_ojp1_WHEN_internal_server_error_EXPECT_error_response(self): + self._catch_internal_server_error(ErrorResponseContentProviderOjp1()) + + def test_ojp2_WHEN_internal_server_EXPECT_error_response(self): + self._catch_internal_server_error(ErrorResponseContentProviderOjp2()) + + def test_ojp1_WHEN_api_error_EXPECT_error_response(self): + self._catch_api_error(ErrorResponseContentProviderOjp1()) + + def _catch_internal_server_error(self, error_response_provider: ErrorResponseContentProvider): + + # prepare test case + error_handler = ErrorHandler(error_response_provider) + self.assertIsNotNone(error_handler) + message = "Oups, terrible failure ;-)" + try: + raise InternalServerError(message) + + # run test case + except InternalServerError as e: + self.logger.info("caught InternalServerError ...") + response = error_handler.handle_error(e) + + # assert expectations + self.assertIsNotNone(response) + + def _catch_api_error(self, error_response_provider: ErrorResponseContentProvider): + + # prepare test case + error_handler = ErrorHandler(error_response_provider) + self.assertIsNotNone(error_handler) + message = "Oups, terrible failure ;-)" + try: + raise InternalServerError(message) + + # run test case + except ApiError as e: + self.logger.info("caught ApiError ...") + response = error_handler.handle_error(e) + + # assert expectations + self.assertIsNotNone(response) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/api/ErrorResponseContentProvider.py b/api/ErrorResponseContentProvider.py new file mode 100644 index 0000000..d2e7d36 --- /dev/null +++ b/api/ErrorResponseContentProvider.py @@ -0,0 +1,10 @@ +from abc import abstractmethod, ABC + + +class ErrorResponseContentProvider(ABC): + @abstractmethod + def provide_error_response_content(self, message: str) -> str: + """ + Provides the error response content for the given error message. + """ + return message diff --git a/api/OjpFareService.py b/api/OjpFareService.py new file mode 100644 index 0000000..82c8057 --- /dev/null +++ b/api/OjpFareService.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +from abc import abstractmethod, ABC + +from fastapi import Response + + +class OjpFareService(ABC): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + @abstractmethod + def handle_request(self, body: bytes) -> Response: + """ + Handles an ojp request. + """ + return Response(body) diff --git a/api/OjpVersionParser.py b/api/OjpVersionParser.py new file mode 100644 index 0000000..56864da --- /dev/null +++ b/api/OjpVersionParser.py @@ -0,0 +1,46 @@ +from xml.etree import ElementTree + +from api.errors.InvalidOjpRequestError import InvalidOjpRequestError + + +class OjpVersionParser: + chunk_size = 8192 # ausreichend gross, um alle Root-Attribute zu erwischen + root_element_name = "OJP" + + def parse_version(self, xml_content: str) -> str: + """ + Reads until first start tag and gets the value of the 'version' attribute + of the OJP root element. + + Raises: + InvalidOjpRequestError upon invalid XML, unexcepted root element or missing version attribute. + """ + + parser = ElementTree.XMLPullParser(events=('start',)) + pos = 0 + + while True: + if pos >= len(xml_content): + # end of the document reached without having seen the root element + try: + parser.close() + except ElementTree.ParseError as e: + raise InvalidOjpRequestError(f"Invalid XML: {e}") from e + raise InvalidOjpRequestError("Root element not found.") + + parser.feed(xml_content[pos:pos + self.chunk_size]) + pos += self.chunk_size + + for event, elem in parser.read_events(): + # first start event is the root + tag = elem.tag + local = tag.split('}', 1)[1] if tag.startswith('{') else tag + + if local != self.root_element_name: + raise InvalidOjpRequestError(f"Root-Element ist '{local}', erwartet '{self.root_element_name}'") + + ver = elem.get("version") + if ver is None or ver.strip() == "": + raise InvalidOjpRequestError(f"'{self.root_element_name}'-Element hat kein 'version'-Attribut.") + + return ver.strip() diff --git a/api/OjpVersionParserTest.py b/api/OjpVersionParserTest.py new file mode 100644 index 0000000..4b0c9b6 --- /dev/null +++ b/api/OjpVersionParserTest.py @@ -0,0 +1,43 @@ +import logging +import unittest + +from api.OjpVersionParser import OjpVersionParser +from api.errors.InvalidOjpRequestError import InvalidOjpRequestError + + +class OjpVersionParserTest(unittest.TestCase): + + logger = logging.getLogger(__name__) + + def test_ojp1_WHEN_parse_EXPECT_1(self): + version_parser = OjpVersionParser() + payload = '' + version = version_parser.parse_version(payload) + self.assertEqual(version, '1.0') + + def test_ojp2_WHEN_parse_EXPECT_2(self): + version_parser = OjpVersionParser() + payload = '' + version = version_parser.parse_version(payload) + self.assertEqual(version, '2.0') + + def test_missing_version_WHEN_parse_EXPECT_failure(self): + version_parser = OjpVersionParser() + payload = '' + with self.assertRaises(InvalidOjpRequestError): + version_parser.parse_version(payload) + + def test_missing_ojp_element_WHEN_parse_EXPECT_failure(self): + version_parser = OjpVersionParser() + payload = '' + with self.assertRaises(InvalidOjpRequestError): + version_parser.parse_version(payload) + + def test_no_xml_WHEN_parse_EXPECT_failure(self): + version_parser = OjpVersionParser() + payload = 'Test' + with self.assertRaises(InvalidOjpRequestError): + version_parser.parse_version(payload) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/api/SerializerUtil.py b/api/SerializerUtil.py new file mode 100644 index 0000000..89e2bc2 --- /dev/null +++ b/api/SerializerUtil.py @@ -0,0 +1,7 @@ +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig + +class SerializerUtil: + ns_map = {"": "http://www.siri.org.uk/siri", "ojp": "http://www.vdv.de/ojp"} + _serializer_config = SerializerConfig(ignore_default_attributes=True, pretty_print=True) + serializer = XmlSerializer(config=_serializer_config) diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/errors/ApiError.py b/api/errors/ApiError.py new file mode 100644 index 0000000..70d6237 --- /dev/null +++ b/api/errors/ApiError.py @@ -0,0 +1,13 @@ +import logging +from abc import abstractmethod, ABC + +class ApiError(Exception, ABC): + def __init__(self, message="An unspecific error occurred.", status_code=500): + self.message = message + self.status_code = status_code + self.logger = logging.getLogger(__name__) + super().__init__(self.message) + + @abstractmethod + def log_error(self): + self.logger.warning(self.message) diff --git a/api/errors/InternalServerError.py b/api/errors/InternalServerError.py new file mode 100644 index 0000000..43d4d93 --- /dev/null +++ b/api/errors/InternalServerError.py @@ -0,0 +1,9 @@ +from api.errors.ApiError import ApiError + + +class InternalServerError(ApiError): + def __init__(self, message="An internal server error occurred."): + super().__init__(message) + + def log_error(self): + self.logger.error(self.message) diff --git a/api/errors/InvalidNovaResponseError.py b/api/errors/InvalidNovaResponseError.py new file mode 100644 index 0000000..59b2522 --- /dev/null +++ b/api/errors/InvalidNovaResponseError.py @@ -0,0 +1,9 @@ +from api.errors.ApiError import ApiError + + +class InvalidNovaResponseError(ApiError): + def __init__(self, message="There was no valid NOVA response."): + super().__init__(message,400) + + def log_error(self): + self.logger.warning(self.message) diff --git a/api/errors/InvalidOjpRequestError.py b/api/errors/InvalidOjpRequestError.py new file mode 100644 index 0000000..0d2c214 --- /dev/null +++ b/api/errors/InvalidOjpRequestError.py @@ -0,0 +1,9 @@ +from api.errors.ApiError import ApiError + + +class InvalidOjpRequestError(ApiError): + def __init__(self, message="There was no (valid) OJP request."): + super().__init__(message, 400) + + def log_error(self): + self.logger.warning(self.message) diff --git a/api/errors/NoNovaResponseError.py b/api/errors/NoNovaResponseError.py new file mode 100644 index 0000000..614f224 --- /dev/null +++ b/api/errors/NoNovaResponseError.py @@ -0,0 +1,9 @@ +from api.errors.ApiError import ApiError + + +class NoNovaResponseError(ApiError): + def __init__(self, message="There was no NOVA response."): + super().__init__(message) + + def log_error(self): + self.logger.warning(self.message) diff --git a/api/errors/OjpRequestParseError.py b/api/errors/OjpRequestParseError.py new file mode 100644 index 0000000..e464a65 --- /dev/null +++ b/api/errors/OjpRequestParseError.py @@ -0,0 +1,10 @@ +from api.errors.ApiError import ApiError + + +class OjpRequestParseError(ApiError): + def __init__(self, cause: Exception = None, message="Failed to parse OJP request."): + self.cause = cause + super().__init__(message, 400) + + def log_error(self): + self.logger.warning(self.message + ": " + str(self.cause)) diff --git a/api/errors/__init__.py b/api/errors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/ojp1/ErrorResponseContentProviderOjp1.py b/api/ojp1/ErrorResponseContentProviderOjp1.py new file mode 100644 index 0000000..b079b58 --- /dev/null +++ b/api/ojp1/ErrorResponseContentProviderOjp1.py @@ -0,0 +1,24 @@ +import datetime + +from xsdata.models.datatype import XmlDateTime + +from api.ErrorResponseContentProvider import ErrorResponseContentProvider +from api.SerializerUtil import SerializerUtil +from ojp import Ojp, Ojpresponse, ServiceDelivery, ServiceDeliveryStructure, OtherError + + +class ErrorResponseContentProviderOjp1(ErrorResponseContentProvider): + ns_map = SerializerUtil.ns_map + serializer = SerializerUtil.serializer + + def provide_error_response_content(self, message: str) -> str: + """ + Provides the ojp1 error response content for the given error message. + """ + ojp = Ojp(ojpresponse=Ojpresponse(service_delivery= + ServiceDelivery(response_timestamp=XmlDateTime.from_datetime( + datetime.datetime.now(datetime.timezone.utc)), + producer_ref="OJP2NOVA", + error_condition=ServiceDeliveryStructure.ErrorCondition( + other_error=OtherError(message))))) + return self.serializer.render(ojp, ns_map=self.ns_map) diff --git a/api/ojp1/OjpFareServiceOjp1.py b/api/ojp1/OjpFareServiceOjp1.py new file mode 100644 index 0000000..95d9732 --- /dev/null +++ b/api/ojp1/OjpFareServiceOjp1.py @@ -0,0 +1,76 @@ +import logging + +from fastapi import Response + +from api.ErrorHandler import ErrorHandler +from api.OjpFareService import OjpFareService +from api.SerializerUtil import SerializerUtil +from api.errors.ApiError import ApiError +from api.errors.InvalidOjpRequestError import InvalidOjpRequestError +from api.errors.OjpRequestParseError import OjpRequestParseError +from api.ojp1.ErrorResponseContentProviderOjp1 import ErrorResponseContentProviderOjp1 +from map_nova_to_ojp import map_nova_reply_to_ojp_fare_delivery +from map_ojp_to_ojp import parse_ojp +from ojp import OjpfareDelivery, Ojpresponse, ServiceDelivery, Ojp +from test_network_flow import test_nova_request_reply, call_ojp_2000 + + +class FareServiceOjp1(OjpFareService): + def __init__(self, *args, **kwargs) -> None: + self.logger = logging.getLogger(__name__) + super().__init__(*args, **kwargs) + + def handle_request(self, body: bytes) -> Response: + """ + Handles an ojp1 request. + """ + error_handler = ErrorHandler(ErrorResponseContentProviderOjp1()) + try: + ojp_fare_request = _parse_request(body) + _validate_request(ojp_fare_request) + self.logger.debug("Request passed validation: " + str(ojp_fare_request)) + + if ojp_fare_request.ojprequest.service_request.ojpfare_request: + self.logger.debug("Fare request - about to query NOVA: " + str(ojp_fare_request)) + nova_response = test_nova_request_reply(ojp_fare_request) + ojp_fare_delivery = map_nova_reply_to_ojp_fare_delivery(nova_response) + self.logger.debug("Workable NOVA response put into OJP: " + str(ojp_fare_delivery)) + return _create_response(ojp_fare_delivery) + else: + self.logger.debug("OJP request - returning the call to the OJP server:" + str(body.decode("utf-8"))) + s, r = call_ojp_2000(body.decode("utf-8")) + return Response(r, media_type="application/xml; charset=utf-8", status_code=s) + + except ApiError as error: + return error_handler.handle_error(error) + + +def _validate_request(ojp_fare_request: Ojp): + if ojp_fare_request.ojprequest is None: + raise InvalidOjpRequestError(message="missing Element OJPRequest."); + + if ojp_fare_request.ojprequest.service_request.ojpfare_request is None: + raise InvalidOjpRequestError() + + +def _parse_request(body: bytes) -> Ojp: + try: + return parse_ojp(body.decode("utf-8")) + except Exception as e: + raise OjpRequestParseError(cause=e) + + +def _create_response(ojp_fare_delivery: OjpfareDelivery) -> Response: + xml = SerializerUtil.serializer.render( + Ojp( + ojpresponse=Ojpresponse( + service_delivery=ServiceDelivery( + response_timestamp=ojp_fare_delivery.response_timestamp, + producer_ref="OJP2NOVA", + ojpfare_delivery=[ojp_fare_delivery], + ) + ) + ), + ns_map=SerializerUtil.ns_map, + ) + return Response(xml, media_type="application/xml; charset=utf-8") diff --git a/api/ojp1/__init__.py b/api/ojp1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/ojp2/ErrorResponseContentProviderOjp2.py b/api/ojp2/ErrorResponseContentProviderOjp2.py new file mode 100644 index 0000000..dda8e55 --- /dev/null +++ b/api/ojp2/ErrorResponseContentProviderOjp2.py @@ -0,0 +1,26 @@ +import datetime + +from xsdata.models.datatype import XmlDateTime + +from api.ErrorResponseContentProvider import ErrorResponseContentProvider +from api.SerializerUtil import SerializerUtil +from ojp2 import Ojp, Ojpresponse, ServiceDelivery, ServiceDeliveryStructure, OtherError, ParticipantRefStructure, \ + ResponseTimestamp + + +class ErrorResponseContentProviderOjp2(ErrorResponseContentProvider): + ns_map = SerializerUtil.ns_map + serializer = SerializerUtil.serializer + + def provide_error_response_content(self, message: str) -> str: + """ + Provides the ojp2 error response content for the given error message. + """ + ojp = Ojp(ojpresponse=Ojpresponse(service_delivery= + ServiceDelivery( + response_timestamp=ResponseTimestamp(XmlDateTime.from_datetime( + datetime.datetime.now(datetime.timezone.utc))), + producer_ref=ParticipantRefStructure("OJP2NOVA"), + error_condition=ServiceDeliveryStructure.ErrorCondition( + other_error=OtherError(message))))) + return self.serializer.render(ojp, ns_map=self.ns_map) \ No newline at end of file diff --git a/api/ojp2/OjpFareServiceOjp2.py b/api/ojp2/OjpFareServiceOjp2.py new file mode 100644 index 0000000..f42e43e --- /dev/null +++ b/api/ojp2/OjpFareServiceOjp2.py @@ -0,0 +1,78 @@ +import logging + +from fastapi import Response + +from api.ErrorHandler import ErrorHandler +from api.OjpFareService import OjpFareService +from api.SerializerUtil import SerializerUtil +from api.errors.ApiError import ApiError +from api.errors.InvalidOjpRequestError import InvalidOjpRequestError +from api.errors.OjpRequestParseError import OjpRequestParseError +from api.ojp2.ErrorResponseContentProviderOjp2 import ErrorResponseContentProviderOjp2 +from map_nova_to_ojp2 import map_nova_reply_to_ojp_fare_delivery +from map_ojp2_to_ojp2 import parse_ojp2 +from ojp2 import Ojp, OjpfareDelivery, Ojpresponse, ServiceDelivery, ParticipantRefStructure +from test_network_flow import test_nova_request_reply_for_ojp2, call_ojp_20 + + +class FareServiceOjp2(OjpFareService): + def __init__(self, *args, **kwargs) -> None: + self.logger = logging.getLogger(__name__) + super().__init__(*args, **kwargs) + + def handle_request(self, body: bytes) -> Response: + """ + Handles an ojp2 request. + """ + error_handler = ErrorHandler(ErrorResponseContentProviderOjp2()) + try: + ojp_fare_request = _parse_request(body) + _validate_request(ojp_fare_request) + self.logger.debug("Request passed validation: " + str(ojp_fare_request)) + + if ojp_fare_request.ojprequest.service_request.ojpfare_request: + self.logger.debug("Fare request - about to query NOVA: " + str(ojp_fare_request)) + nova_response = test_nova_request_reply_for_ojp2(ojp_fare_request) + ojp_fare_delivery = map_nova_reply_to_ojp_fare_delivery(nova_response) + self.logger.debug("Workable NOVA response put into OJP: " + str(ojp_fare_delivery)) + return _create_response(ojp_fare_delivery) + else: + self.logger.debug("OJP request - returning the call to the OJP server:" + str(body.decode("utf-8"))) + s, r = call_ojp_20(body.decode("utf-8")) + return Response(r, media_type="application/xml; charset=utf-8", status_code=s) + + except ApiError as error: + return error_handler.handle_error(error) + + +def _validate_request(ojp_fare_request: Ojp): + if ojp_fare_request.ojprequest is None: + raise InvalidOjpRequestError(message="missing Element OJPRequest."); + + if ojp_fare_request.ojprequest.service_request.ojpfare_request is None: + raise InvalidOjpRequestError() + + +def _parse_request(body: bytes) -> Ojp: + try: + return parse_ojp2(body.decode("utf-8")) + except Exception as e: + raise OjpRequestParseError(cause=e) + + +def _create_response(ojp_fare_delivery: OjpfareDelivery) -> Response: + xml = SerializerUtil.serializer.render( + Ojp( + ojpresponse=Ojpresponse( + service_delivery=ServiceDelivery( + response_timestamp=ojp_fare_delivery.response_timestamp, + producer_ref=ParticipantRefStructure( + value="OJP2NOVA" + ), + ojpfare_delivery=[ojp_fare_delivery], + ) + ) + ), + ns_map=SerializerUtil.ns_map, + ) + return Response(xml, media_type="application/xml; charset=utf-8") diff --git a/api/ojp2/__init__.py b/api/ojp2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configuration.py b/configuration.py index 04a3e28..c2d2cb7 100644 --- a/configuration.py +++ b/configuration.py @@ -46,7 +46,10 @@ SSL_CERTFILE = '' HTTP_HOST = '127.0.0.1' HTTP_PORT = 8000 +HTTP_PORT_OJPFARE = 8081 HTTP_SLUG = "ojp2023" +HTTP_SLUG_OJPFARE = "ojpfare" +HTTP_SLUG_PUNKT = "PUNKT" READTRIPREQUESTFILE = True VATRATE= 8.1 # Percent USE_HTA = True # if in the tests half price should be used diff --git a/map_nova_to_ojp.py b/map_nova_to_ojp.py index 15aeff6..81eb281 100644 --- a/map_nova_to_ojp.py +++ b/map_nova_to_ojp.py @@ -5,6 +5,7 @@ from xsdata.models.datatype import XmlDateTime +from api.errors.InvalidNovaResponseError import InvalidNovaResponseError from nova import ErstellePreisAuskunftResponse, KlassenTypCode, PreisAuspraegung, \ PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput from ojp import OjpfareDelivery, FareResultStructure, FareProductStructure, TripFareResultStructure, \ @@ -52,7 +53,7 @@ def map_preis_auspraegung_to_trip_fare_result(preis_auspraegungen: List[PreisAus def map_nova_reply_to_ojp_fare_delivery(soap: PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput) -> Optional[OjpfareDelivery]: if not soap.body.erstelle_preis_auskunft_response.preis_auskunft_response.preis_auspraegung: - return None + raise InvalidNovaResponseError() bonded_trips: dict[str,PreisAuspraegung] = {} for preis_auspraegung in soap.body.erstelle_preis_auskunft_response.preis_auskunft_response.preis_auspraegung: diff --git a/map_nova_to_ojp2.py b/map_nova_to_ojp2.py index f946707..e9733c2 100644 --- a/map_nova_to_ojp2.py +++ b/map_nova_to_ojp2.py @@ -7,6 +7,7 @@ from xsdata.models.datatype import XmlDateTime +from api.errors.InvalidNovaResponseError import InvalidNovaResponseError from nova import ErstellePreisAuskunftResponse, KlassenTypCode, PreisAuspraegung, \ PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput from ojp2 import OjpfareDelivery, FareResultStructure, FareProductStructure, TripFareResultStructure, \ @@ -47,9 +48,10 @@ def map_preis_auspraegung_to_trip_fare_result(preis_auspraegungen: List[PreisAus return FareResultStructure(id=id, trip_fare_result=tripfareresults) + def map_nova_reply_to_ojp_fare_delivery(soap: PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput) -> Optional[OjpfareDelivery]: if not soap.body.erstelle_preis_auskunft_response.preis_auskunft_response.preis_auspraegung: - return None + raise InvalidNovaResponseError() bonded_trips :Dict[str,PreisAuspraegung]= {} for preis_auspraegung in soap.body.erstelle_preis_auskunft_response.preis_auskunft_response.preis_auspraegung: diff --git a/map_ojp2_to_nova.py b/map_ojp2_to_nova.py index e7d310c..5598add 100644 --- a/map_ojp2_to_nova.py +++ b/map_ojp2_to_nova.py @@ -13,8 +13,8 @@ import uuid def map_timed_leg_to_segment(timed_leg: TimedLegStructure) -> FahrplanVerbindungsSegment: - einstieg = sloid2didok(timed_leg.leg_board.stop_point_ref) - ausstieg = sloid2didok(timed_leg.leg_alight.stop_point_ref) + einstieg = sloid2didok(timed_leg.leg_board.stop_point_ref.value) + ausstieg = sloid2didok(timed_leg.leg_alight.stop_point_ref.value) abfahrts_zeit = timed_leg.leg_board.service_departure.timetabled_time ankunfts_zeit = timed_leg.leg_alight.service_arrival.timetabled_time line_ref = timed_leg.service.line_ref.value @@ -34,8 +34,8 @@ def map_timed_leg_to_segment(timed_leg: TimedLegStructure) -> FahrplanVerbindung verwaltungs_code= process_operating_ref_ojp2(operator_ref) leg_intermediates = timed_leg.leg_intermediate - zwischenhalten = [sloid2didok(timed_leg.leg_board.stop_point_ref)] + [sloid2didok(leg_intermediate.stop_point_ref) - for leg_intermediate in leg_intermediates] + [sloid2didok(timed_leg.leg_alight.stop_point_ref)] + zwischenhalten = [sloid2didok(timed_leg.leg_board.stop_point_ref.value)] + [sloid2didok(leg_intermediate.stop_point_ref.value) + for leg_intermediate in leg_intermediates] + [sloid2didok(timed_leg.leg_alight.stop_point_ref.value)] #handling of Tariff code TC attr2=timed_leg.service.attribute diff --git a/server.py b/server.py index de180cb..3d426f7 100644 --- a/server.py +++ b/server.py @@ -1,118 +1,52 @@ #!/usr/bin/env python3 -from fastapi import FastAPI, Request, Response -from xsdata.formats.dataclass.serializers import XmlSerializer -from xsdata.formats.dataclass.serializers.config import SerializerConfig +import logging -from map_nova_to_ojp import test_nova_to_ojp -from map_ojp_to_ojp import parse_ojp -from support import error_response -from ojp import Ojp, Ojpresponse, ServiceDelivery -from test_network_flow import test_nova_request_reply, call_ojp_2000 -from configuration import HTTP_HOST, HTTP_PORT, HTTPS, SSL_CERTFILE, SSL_KEYFILE, HTTP_SLUG +from fastapi import FastAPI, Request, Response -import logging import app_logging - -# from support import add_error_response +from api.OjpVersionParser import OjpVersionParser +from api.ojp1.OjpFareServiceOjp1 import FareServiceOjp1 +from api.ojp2.OjpFareServiceOjp2 import FareServiceOjp2 +from configuration import HTTP_HOST, HTTP_PORT, HTTPS, SSL_CERTFILE, SSL_KEYFILE, HTTP_SLUG app_logging.setup_logging() logger = logging.getLogger(__name__) - app = FastAPI(title="OJPTONOVA") -serializer_config = SerializerConfig(ignore_default_attributes=True, pretty_print=True) -serializer = XmlSerializer(config=serializer_config) +fare_service1 = FareServiceOjp1() +fare_service2 = FareServiceOjp2() -ns_map = {'': 'http://www.siri.org.uk/siri', 'ojp': 'http://www.vdv.de/ojp'} +version_parser = OjpVersionParser() -# implements basic liveness probe @app.get("/health/liveness", tags=["Health"]) async def liveness(fastapi_req: Request): + """ + Implements liveness probe. + """ return Response("Liveness: OK", media_type="text/plain; charset=utf-8") - -# implements basic readiness probe @app.get("/health/readiness", tags=["Health"]) async def readiness(fastapi_req: Request): + """ + Implements readiness probe. + """ return Response("Readiness: OK", media_type="text/plain; charset=utf-8") @app.post("/" + HTTP_SLUG, tags=["Open Journey Planner"]) -async def post_request(fastapi_req: Request) -> Response: +async def post_request(fastapi_req: Request) ->Response: + """ + Handles OjpFare requests: for ojp1 or ojp2. + """ body = await fastapi_req.body() logger.debug("Received request: " + str(body)) - ojp_fare_request = None - error = None - try: - ojp_fare_request = parse_ojp(body.decode('utf-8')) - except Exception as e: - error = e - - try: - if ojp_fare_request and ojp_fare_request.ojprequest: - # a request was made and it seems legit - if ojp_fare_request.ojprequest.service_request and ojp_fare_request.ojprequest.service_request.ojpfare_request: - # we deal with a OJPFare Request and will ask NOVA - logger.debug("Query to NOVA: " + str(ojp_fare_request)) - nova_response = test_nova_request_reply(ojp_fare_request) - if nova_response: - # we got a valid response - ojp_fare_delivery = test_nova_to_ojp(nova_response) - if ojp_fare_delivery: - # we have a OJPFareDelivery to work with - # we add the warnings - logger.debug("Workable NOVA response put into OJP: " + str(ojp_fare_delivery)) - # ojp_fare_delivery=add_error_response(ojp_fare_delivery) - xml = serializer.render( - Ojp( - ojpresponse=Ojpresponse( - service_delivery=ServiceDelivery( - response_timestamp=ojp_fare_delivery.response_timestamp, producer_ref="OJP2NOVA", ojpfare_delivery=[ojp_fare_delivery] - ) - ) - ), - ns_map=ns_map, - ) - return Response(xml, media_type="application/xml; charset=utf-8") - else: - logger.warning("There was a NOVA response, but it can't be used:" + str(nova_response)) - return Response( - serializer.render(error_response("There was a NOVA response, but it cannot be used"), ns_map=ns_map), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - logger.error("There was no NOVA response") - return Response( - serializer.render(error_response("There was no NOVA response"), ns_map=ns_map), status_code=400, media_type="application/xml; charset=utf-8" - ) - else: - logger.debug("Returning the call to the OJP server:" + str(body.decode('utf-8'))) - s, r = call_ojp_2000(body.decode('utf-8')) - return Response(r, media_type="application/xml; charset=utf-8", status_code=s) - else: - # very general errors - if error: - # an error message was provided in the exception - logger.error("Couldn't extract a valid OJP request") - return Response( - serializer.render(error_response("There was no (valid) OJP request\n" + str(error)), ns_map=ns_map), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - else: - # no error message was provided in the exception - logger.warning("No valid OJP request") - return Response( - serializer.render(error_response("There was no (valid) OJP request"), ns_map=ns_map), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - except Exception as e: - # not yet really sophisticated handling of all other errors during the work (should be regular OJPDeliveries with OtherError set - logger.exception(e) - return Response(serializer.render(error_response(str(e)), ns_map=ns_map), status_code=400, media_type="application/xml; charset=utf-8") - + version = version_parser.parse_version(body.decode("utf-8")) + logger.debug("Received version: " + str(version)) + if version == "1.0": + return fare_service1.handle_request(body) + else: + return fare_service2.handle_request(body) if __name__ == "__main__": import uvicorn diff --git a/server_ojp1.py b/server_ojp1.py new file mode 100644 index 0000000..2975e61 --- /dev/null +++ b/server_ojp1.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import logging + +from fastapi import FastAPI, Request, Response + +import app_logging +from api.ojp1.OjpFareServiceOjp1 import FareServiceOjp1 +from configuration import HTTP_HOST, HTTP_PORT, HTTPS, SSL_CERTFILE, SSL_KEYFILE, HTTP_SLUG + +app_logging.setup_logging() +logger = logging.getLogger(__name__) +app = FastAPI(title="OJPTONOVA") +fare_service1 = FareServiceOjp1() + +@app.get("/health/liveness", tags=["Health"]) +async def liveness(fastapi_req: Request): + """ + Implements liveness probe. + """ + return Response("Liveness: OK", media_type="text/plain; charset=utf-8") + +@app.get("/health/readiness", tags=["Health"]) +async def readiness(fastapi_req: Request): + """ + Implements readiness probe. + """ + return Response("Readiness: OK", media_type="text/plain; charset=utf-8") + +@app.post("/" + HTTP_SLUG, tags=["Open Journey Planner"]) +async def post_request(fastapi_req: Request) ->Response: + """ + Handles OjpFare requests: for ojp1 only. + """ + body = await fastapi_req.body() + logger.debug("Received request: " + str(body)) + return fare_service1.handle_request(body) + +if __name__ == "__main__": + import uvicorn + + if HTTPS: + uvicorn.run(app, host=HTTP_HOST, port=HTTP_PORT, ssl_keyfile=SSL_KEYFILE, ssl_certfile=SSL_CERTFILE) + else: + uvicorn.run(app, host=HTTP_HOST, port=HTTP_PORT) diff --git a/server_ojp2.py b/server_ojp2.py index b37e64b..e416778 100644 --- a/server_ojp2.py +++ b/server_ojp2.py @@ -1,20 +1,11 @@ #!/usr/bin/env python3 -from fastapi import FastAPI, Request, Response -from xsdata.formats.dataclass.serializers import XmlSerializer -from xsdata.formats.dataclass.serializers.config import SerializerConfig +import logging -from map_nova_to_ojp2 import test_nova_to_ojp2 -from map_ojp2_to_ojp2 import parse_ojp2 -from support import error_response +from fastapi import FastAPI, Request, Response -from ojp2 import ( - Ojp, - Ojpresponse, - ServiceDelivery, - ParticipantRefStructure, -) -from test_network_flow import call_ojp_20, test_nova_request_reply_for_ojp2 +import app_logging +from api.ojp2.OjpFareServiceOjp2 import FareServiceOjp2 from configuration import ( HTTP_HOST, HTTP_PORT, @@ -24,143 +15,35 @@ HTTP_SLUG, ) -import logging -import app_logging - app_logging.setup_logging() logger = logging.getLogger(__name__) - app = FastAPI(title="OJP2TONOVA") -serializer_config = SerializerConfig(ignore_default_attributes=True, pretty_print=True) -serializer = XmlSerializer(config=serializer_config) - -ns_map = {"": "http://www.siri.org.uk/siri", "ojp": "http://www.vdv.de/ojp"} +fare_service = FareServiceOjp2() -# implements basic liveness probe @app.get("/health/liveness", tags=["Health"]) async def liveness(fastapi_req: Request): + """ + Implements liveness probe. + """ return Response("Liveness: OK", media_type="text/plain; charset=utf-8") - # implements basic readiness probe @app.get("/health/readiness", tags=["Health"]) async def readiness(fastapi_req: Request): + """ + Implements readiness probe. + """ return Response("Readiness: OK", media_type="text/plain; charset=utf-8") @app.post("/" + HTTP_SLUG, tags=["Open Journey Planner"]) async def post_request(fastapi_req: Request) ->Response: + """ + Handles OjpFare requests: for ojp2 only. + """ body = await fastapi_req.body() logger.debug("Received request: " + str(body)) - - ojp_fare_request = None - error = None - try: - ojp_fare_request = parse_ojp2(body.decode("utf-8")) - except Exception as e: - error = e - - try: - if ojp_fare_request and ojp_fare_request.ojprequest: - # a request was made and it seems legit - if ojp_fare_request.ojprequest.service_request.ojpfare_request: - # we deal with a OJPFare Request and will ask NOVA - logger.debug("Query to NOVA: " + str(ojp_fare_request)) - nova_response = test_nova_request_reply_for_ojp2(ojp_fare_request) - if nova_response: - # we got a valid response - ojp_fare_delivery = test_nova_to_ojp2(nova_response) - if ojp_fare_delivery: - # we have a OJPFareDelivery to work with - # we add the warnings - logger.debug( - "Workable NOVA response put into OJP: " - + str(ojp_fare_delivery) - ) - # ojp_fare_delivery=add_error_response(ojp_fare_delivery) - xml = serializer.render( - Ojp( - ojpresponse=Ojpresponse( - service_delivery=ServiceDelivery( - response_timestamp=ojp_fare_delivery.response_timestamp, - producer_ref=ParticipantRefStructure( - value="OJP2NOVA" - ), - ojpfare_delivery=[ojp_fare_delivery], - ) - ) - ), - ns_map=ns_map, - ) - return Response( - xml, media_type="application/xml; charset=utf-8" - ) - else: - logger.warning( - "There was a NOVA response, but it can't be used:" - + str(nova_response) - ) - return Response( - serializer.render( - error_response( - "There was a NOVA response, but it cannot be used" - ), - ns_map=ns_map, - ), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - logger.error("There was no NOVA response") - return Response( - serializer.render( - error_response("There was no NOVA response"), ns_map=ns_map - ), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - else: - logger.debug( - "Returning the call to the OJP server:" + str(body.decode("utf-8")) - ) - s, r = call_ojp_20(body.decode("utf-8")) - return Response( - r, media_type="application/xml; charset=utf-8", status_code=s - ) - else: - # very general errors - if error: - # an error message was provided in the exception - logger.error("Couldn't extract a valid OJP request") - return Response( - serializer.render( - error_response( - "There was no (valid) OJP request\n" + str(error) - ), - ns_map=ns_map, - ), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - else: - # no error message was provided in the exception - logger.warning("No valid OJP request") - return Response( - serializer.render( - error_response("There was no (valid) OJP request"), - ns_map=ns_map, - ), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - except Exception as e: - # not yet really sophisticated handling of all other errors during the work (should be regular OJPDeliveries with OtherError set - logger.exception(e) - return Response( - serializer.render(error_response(str(e)), ns_map=ns_map), - status_code=400, - media_type="application/xml; charset=utf-8", - ) - + return fare_service.handle_request(body) if __name__ == "__main__": import uvicorn diff --git a/test_network_flow.py b/test_network_flow.py index 01bbccd..444ea13 100644 --- a/test_network_flow.py +++ b/test_network_flow.py @@ -13,6 +13,7 @@ from xsdata.formats.dataclass.serializers.config import SerializerConfig import ojp.fare_result_structure +from api.errors.NoNovaResponseError import NoNovaResponseError from configuration import * from support import OJPError from test_create_ojp_request import * @@ -109,10 +110,12 @@ def test_nova_request_reply(ojp: Ojp)->Optional[PreisAuskunftServicePortTypeSoap nova_request = test_ojp_fare_request_to_nova_request(ojp) nova_client = get_nova_client() nova_response :Optional[PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput] = nova_client.send(nova_request, headers=headers) - if nova_response: - xml_logger.log_object_as_xml('nova_response.xml',nova_response) - return nova_response - return None + if nova_response is None: + raise NoNovaResponseError() + xml_logger.log_object_as_xml('nova_response.xml',nova_response) + return nova_response + + def test_nova_request_reply_for_ojp2(ojp: Ojp2)->Optional[PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput]: oauth_helper = OAuth2Helper(client_id=NOVA_CLIENT_ID, client_secret=NOVA_CLIENT_SECRET, scope=NOVA_SCOPE) @@ -121,10 +124,10 @@ def test_nova_request_reply_for_ojp2(ojp: Ojp2)->Optional[PreisAuskunftServicePo nova_request = test_ojp2_fare_request_to_nova_request(ojp) nova_client = get_nova_client() nova_response : PreisAuskunftServicePortTypeSoapv14ErstellePreisAuskunftOutput = nova_client.send(nova_request, headers=headers) - if nova_response: - xml_logger.log_object_as_xml('nova_response.xml',nova_response) - return nova_response - return None + if nova_response is None: + raise NoNovaResponseError() + xml_logger.log_object_as_xml('nova_response.xml',nova_response) + return nova_response def check_configuration() ->None: if (len(NOVA_CLIENT_SECRET)==0): diff --git a/test_nova_r_r.py b/test_nova_r_r.py index fe41266..59de3c1 100644 --- a/test_nova_r_r.py +++ b/test_nova_r_r.py @@ -73,7 +73,7 @@ def send_nova(url: str, headers: dict, xml_body: str) -> requests.Response: with open(READFILE, 'r', encoding='utf-8') as inputfile: nova_request = inputfile.read() # send it to NOVA - oauth_helper = OAuth2Helper(client_id=NOVA_CLIENT_ID, client_secret=NOVA_CLIENT_SECRET) + oauth_helper = OAuth2Helper(client_id=NOVA_CLIENT_ID, client_secret=NOVA_CLIENT_SECRET, scope=NOVA_SCOPE) access_token = oauth_helper.get_token() headers = {'Authorization': 'Bearer ' + access_token, 'SOAPAction': 'http://nova.voev.ch/services/v14/preisauskunft/erstellePreisAuskunft',