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',