From 6ef275bda2139be5f8939ea235394085f83d42bf Mon Sep 17 00:00:00 2001 From: Dylann Cordel Date: Tue, 19 Jan 2021 08:16:41 +0100 Subject: [PATCH] migrate DPD to new roulier and python 3 --- roulier/api.py | 70 ++++---- roulier/carriers/__init__.py | 2 +- roulier/carriers/dpd/__init__.py | 6 - roulier/carriers/dpd/dpd.py | 30 ---- roulier/carriers/dpd/dpd_decoder.py | 65 -------- roulier/carriers/dpd/dpd_encoder.py | 98 ----------- roulier/carriers/dpd_fr/__init__.py | 5 + .../{dpd/dpd_api.py => dpd_fr/api.py} | 46 ++++- roulier/carriers/dpd_fr/carrier_action.py | 28 ++++ roulier/carriers/dpd_fr/decoder.py | 34 ++++ roulier/carriers/dpd_fr/encoder.py | 64 +++++++ .../{dpd => dpd_fr}/templates/dpd_address.xml | 0 .../templates/dpd_addressInfo.xml | 0 .../dpd_createShipmentWithLabels.xml | 0 .../{dpd => dpd_fr}/templates/dpd_header.xml | 0 .../{dpd => dpd_fr}/templates/dpd_soap.xml | 0 roulier/carriers/dpd_fr/tests/__init__.py | 1 + .../carriers/dpd_fr/tests/credential_demo.py | 12 ++ roulier/carriers/dpd_fr/tests/data.py | 49 ++++++ roulier/carriers/dpd_fr/tests/test_dpd.py | 157 ++++++++++++++++++ .../dpd_transport.py => dpd_fr/transport.py} | 38 +---- 21 files changed, 438 insertions(+), 267 deletions(-) delete mode 100755 roulier/carriers/dpd/__init__.py delete mode 100755 roulier/carriers/dpd/dpd.py delete mode 100755 roulier/carriers/dpd/dpd_decoder.py delete mode 100755 roulier/carriers/dpd/dpd_encoder.py create mode 100755 roulier/carriers/dpd_fr/__init__.py rename roulier/carriers/{dpd/dpd_api.py => dpd_fr/api.py} (66%) create mode 100755 roulier/carriers/dpd_fr/carrier_action.py create mode 100755 roulier/carriers/dpd_fr/decoder.py create mode 100755 roulier/carriers/dpd_fr/encoder.py rename roulier/carriers/{dpd => dpd_fr}/templates/dpd_address.xml (100%) rename roulier/carriers/{dpd => dpd_fr}/templates/dpd_addressInfo.xml (100%) rename roulier/carriers/{dpd => dpd_fr}/templates/dpd_createShipmentWithLabels.xml (100%) rename roulier/carriers/{dpd => dpd_fr}/templates/dpd_header.xml (100%) rename roulier/carriers/{dpd => dpd_fr}/templates/dpd_soap.xml (100%) create mode 100644 roulier/carriers/dpd_fr/tests/__init__.py create mode 100644 roulier/carriers/dpd_fr/tests/credential_demo.py create mode 100644 roulier/carriers/dpd_fr/tests/data.py create mode 100644 roulier/carriers/dpd_fr/tests/test_dpd.py rename roulier/carriers/{dpd/dpd_transport.py => dpd_fr/transport.py} (66%) diff --git a/roulier/api.py b/roulier/api.py index 168dccc..15a0e27 100644 --- a/roulier/api.py +++ b/roulier/api.py @@ -12,7 +12,7 @@ def _normalize_coerce_zpl(self, value): Remove ZPL ctrl caraters Remove accents """ - if not isinstance(value, basestring): + if not isinstance(value, str): return value ctrl_cars = [ @@ -28,41 +28,43 @@ def _normalize_coerce_zpl(self, value): def _normalize_coerce_accents(self, value): """Sanitize accents for some WS.""" - if not isinstance(value, basestring): + if not isinstance(value, str): return value sanitized = ( - value - # quick and dirty replacement - # of common accentued chars in french - # because some ws don't handle well utf8 - .replace(u"é", "e") - .replace(u"è", "e") - .replace(u"ë", "e") - .replace(u"ê", "e") - .replace(u"ô", "o") - .replace(u"ï", "i") - .replace(u"ö", "o") - .replace(u"à", "a") - .replace(u"â", "a") - .replace(u"ç", "c") - .replace(u"û", "u") - .replace(u"ù", "u") - .replace(u"É", "E") - .replace(u"È", "E") - .replace(u"Ë", "E") - .replace(u"Ê", "E") - .replace(u"Ô", "O") - .replace(u"Ï", "I") - .replace(u"Ö", "O") - .replace(u"À", "A") - .replace(u"Â", "A") - .replace(u"Ç", "C") - .replace(u"Û", "U") - .replace(u"Ù", "U") - .replace(u"œ", "oe") - .replace(u"Œ", "OE") - ).encode( - "ascii", "ignore" + ( + value + # quick and dirty replacement + # of common accentued chars in french + # because some ws don't handle well utf8 + .replace("é", "e") + .replace("è", "e") + .replace("ë", "e") + .replace("ê", "e") + .replace("ô", "o") + .replace("ï", "i") + .replace("ö", "o") + .replace("à", "a") + .replace("â", "a") + .replace("ç", "c") + .replace("û", "u") + .replace("ù", "u") + .replace("É", "E") + .replace("È", "E") + .replace("Ë", "E") + .replace("Ê", "E") + .replace("Ô", "O") + .replace("Ï", "I") + .replace("Ö", "O") + .replace("À", "A") + .replace("Â", "A") + .replace("Ç", "C") + .replace("Û", "U") + .replace("Ù", "U") + .replace("œ", "oe") + .replace("Œ", "OE") + ) + .encode("ascii", "ignore") + .decode("ascii") ) # cut remaining chars return sanitized diff --git a/roulier/carriers/__init__.py b/roulier/carriers/__init__.py index 0bc41c1..27e9507 100755 --- a/roulier/carriers/__init__.py +++ b/roulier/carriers/__init__.py @@ -1,6 +1,6 @@ from . import laposte_fr from . import gls_fr from . import chronopost_fr +from . import dpd_fr # from . import geodis -# from . import dpd diff --git a/roulier/carriers/dpd/__init__.py b/roulier/carriers/dpd/__init__.py deleted file mode 100755 index c32099f..0000000 --- a/roulier/carriers/dpd/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -from . import dpd -from . import dpd_decoder -from . import dpd_encoder -from . import dpd_transport -from . import dpd_api diff --git a/roulier/carriers/dpd/dpd.py b/roulier/carriers/dpd/dpd.py deleted file mode 100755 index 2615b8b..0000000 --- a/roulier/carriers/dpd/dpd.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -"""Implementation for Dpd.""" - -from .dpd_decoder import DpdDecoder -from .dpd_encoder import DpdEncoder -from .dpd_transport import DpdTransport -from roulier.carrier import Carrier - - -class Dpd(Carrier): - """Implementation for Dpd.""" - - encoder = DpdEncoder() - decoder = DpdDecoder() - ws = DpdTransport() - - def api(self): - """Expose how to communicate with a Dpd.""" - return self.encoder.api() - - def get(self, data, action): - """Run an action with data against Dpd WS.""" - request = self.encoder.encode(data, action) - response = self.ws.send(request) - return self.decoder.decode(response["body"], request["output_format"]) - - # shortcuts - def get_label(self, data): - """Genereate a createShipmentWithLabels.""" - return self.get(data, "createShipmentWithLabels") diff --git a/roulier/carriers/dpd/dpd_decoder.py b/roulier/carriers/dpd/dpd_decoder.py deleted file mode 100755 index bec42ff..0000000 --- a/roulier/carriers/dpd/dpd_decoder.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -"""Dpd XML -> Python.""" -from lxml import objectify -from roulier.codec import Decoder -from roulier import ws_tools as tools -import base64 - - -class DpdDecoder(Decoder): - """Dpd XML -> Python.""" - - def decode(self, body, output_format): - """Dpd XML -> Python.""" - - def create_shipment_with_labels(msg): - """Understand a CreateShipmentWithLabelsResponse.""" - shipments, labels = msg.CreateShipmentWithLabelsResult.getchildren() - shipment = shipments.getchildren()[0] - label, attachment = labels.getchildren() - label_data = self.handle_zpl(label.label.text, output_format) - # .text because we want str instead of objectify.StringElement - summary_data = base64.b64decode(attachment.label.text) - summary_format = output_format == "ZPL" and "png" or output_format - x = { - "tracking": {"number": shipment.barcode.text,}, - "parcels": [ - { - "id": 1, - "reference": "", - "number": shipment.parcelnumber.text, - "label": { # same as main label - "data": label_data, - "name": "label 1", - "type": output_format, - }, - } - ], - "label": { # main label - "data": label_data, - "name": "label", - "type": output_format, - }, - "annexes": [ - {"data": summary_data, "name": "Summary", "type": summary_format} - ], - } - return x - - xml = objectify.fromstring(body) - tag = xml.tag - lookup = { - "{http://www.cargonet.software}CreateShipmentWithLabelsResponse": create_shipment_with_labels, - } - return lookup[tag](xml) - - def handle_zpl(self, png, label_format): - """Convert a png in zpl. - - if labelFormat was asked as ZPL, WS returns a png - This function rotate it and convert it an suitable zpl format - """ - if label_format == "ZPL": - return tools.png_to_zpl(png, True) - else: - return base64.b64decode(png) diff --git a/roulier/carriers/dpd/dpd_encoder.py b/roulier/carriers/dpd/dpd_encoder.py deleted file mode 100755 index 1e74edf..0000000 --- a/roulier/carriers/dpd/dpd_encoder.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -"""Transform input to dpd compatible xml.""" -from jinja2 import Environment, PackageLoader -from roulier.codec import Encoder -from datetime import datetime -from .dpd_api import DpdApi -from roulier.exception import InvalidApiInput -import logging - -DPD_ACTIONS = "createShipmentWithLabels" -log = logging.getLogger(__name__) - - -class DpdEncoder(Encoder): - """Transform input to dpd compatible xml.""" - - def encode(self, api_input, action): - """Transform input to dpd compatible xml.""" - if not (action in DPD_ACTIONS): - raise InvalidApiInput( - "action %s not in %s" % (action, ", ".join(DPD_ACTIONS)) - ) - - api = DpdApi() - if not api.validate(api_input): - raise InvalidApiInput("Input error : %s" % api.errors(api_input)) - data = api.normalize(api_input) - - # add some rules which are hard to implement with - # cerberus. - # TODO: add additional schemas for that - if data["service"]["product"] == "DPD_Predict": - if len(data["service"]["dropOffLocation"]) > 0: - raise InvalidApiInput("dropOffLocation can't be used with predict") - if data["service"]["notifications"] != "Predict": - log.info("Notification forced to predict because of product") - data["service"]["notifications"] = "Predict" - - if data["service"]["product"] == "DPD_Classic": - if len(data["service"]["dropOffLocation"]) > 0: - raise InvalidApiInput("dropOffLocation can't be used with classic") - if data["service"]["notifications"] == "Predict": - raise InvalidApiInput( - "Predict notifications can't be used with classic" - ) - - if data["service"]["product"] == "DPD_Relais": - if len(data["service"]["dropOffLocation"]) < 1: - raise InvalidApiInput("dropOffLocation is mandatory for this product") - if data["service"]["notifications"] == "Predict": - raise InvalidApiInput("Predict notifications can't be used with Relais") - - data["service"]["shippingDate"] = datetime.strptime( - data["service"]["shippingDate"], "%Y/%M/%d" - ).strftime("%d/%M/%Y") - - def reduce_address(address): - """Concat some fields. - - Because there is no street2 nor company in DPD api. - append street2 to street1 and truncate at 70 - append company to name - """ - address["street1"] = ("%s, %s" % (address["street1"], address["street2"]))[ - 0:70 - ] - address["name"] = ("%s, %s" % (address["name"], address["company"]))[0:35] - - reduce_address(data["to_address"]) - reduce_address(data["from_address"]) - - output_format = data["service"]["labelFormat"] - if data["service"]["labelFormat"] in ("PNG", "ZPL"): - # WS doesn't handle zpl yet, we convert it later - # png is named Default, WTF DPD? - data["service"]["labelFormat"] = "Default" - - env = Environment( - loader=PackageLoader("roulier", "/carriers/dpd/templates"), - extensions=["jinja2.ext.with_", "jinja2.ext.autoescape"], - autoescape=True, - ) - - template = env.get_template("dpd_%s.xml" % action) - return { - "body": template.render( - service=data["service"], - parcel=data["parcels"][0], - sender_address=data["from_address"], - receiver_address=data["to_address"], - ), - "headers": data["auth"], - "output_format": output_format, - } - - def api(self): - """Return API we are expecting.""" - return DpdApi().api_values() diff --git a/roulier/carriers/dpd_fr/__init__.py b/roulier/carriers/dpd_fr/__init__.py new file mode 100755 index 0000000..d375209 --- /dev/null +++ b/roulier/carriers/dpd_fr/__init__.py @@ -0,0 +1,5 @@ +from . import carrier_action +from . import decoder +from . import encoder +from . import transport +from . import api diff --git a/roulier/carriers/dpd/dpd_api.py b/roulier/carriers/dpd_fr/api.py similarity index 66% rename from roulier/carriers/dpd/dpd_api.py rename to roulier/carriers/dpd_fr/api.py index 3b93fae..e0f429b 100644 --- a/roulier/carriers/dpd/dpd_api.py +++ b/roulier/carriers/dpd_fr/api.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Implementation of Dpd Api.""" -from roulier.api import Api +from roulier.api import ApiParcel +from roulier.api import MyValidator DPD_LABEL_FORMAT = ( "PDF", @@ -22,7 +23,45 @@ ) -class DpdApi(Api): +class DpdValidator(MyValidator): + def _validate_product(self, _, field, value): + """ + Tests some complex constraints relate to the product type and other fields values + + The rule's arguments are validated against this schema: + {'type': 'boolean'} + """ + product = self.document.get("product") + pickup_id = self.document.get("pickupLocationId", "").strip() + notifications = self.document.get("notifications") + if pickup_id and product in ("DPD_Predict", "DPD_Classic"): + self._error( + "pickupLocationId", "pickupLocationId can't be used with %s" % product + ) + if product == "DPD_Predict": + if notifications and "notifications" != "Predict": + self._error( + "notifications", "must be set to Predict when using Predict" + ) + else: + if notifications == "Predict": + self._error( + "notifications", + "Predict notifications can't be used with %s" % product, + ) + if product == "DPD_Relais" and not pickup_id: + self._error( + "pickupLocationId", "pickupLocationId is mandatory for Relais" + ) + + +class DpdApi(ApiParcel): + def _validator(self): + v = DpdValidator() + v.allow_unknown = True + # v.purge_unknown = True + return v + def _service(self): schema = super(DpdApi, self)._service() schema["labelFormat"]["allowed"] = DPD_LABEL_FORMAT @@ -61,10 +100,11 @@ def _service(self): "default": DPD_PRODUCTS[0], # 'description': 'Type de produit', "allowed": DPD_PRODUCTS, + "product": True, } ) - schema["dropOffLocation"] = { + schema["pickupLocationId"] = { "default": "", "empty": True, "required": False, diff --git a/roulier/carriers/dpd_fr/carrier_action.py b/roulier/carriers/dpd_fr/carrier_action.py new file mode 100755 index 0000000..1f26039 --- /dev/null +++ b/roulier/carriers/dpd_fr/carrier_action.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""Implementation for Dpd.""" + +from .api import DpdApi +from .decoder import DpdDecoder +from .encoder import DpdEncoder +from .transport import DpdTransport +from ...roulier import factory +from ...carrier_action import CarrierGetLabel + + +class DpdGetabel(CarrierGetLabel): + """Implementation for Dpd.""" + + ws_url = ( + "https://e-station.cargonet.software/dpd-eprintwebservice/eprintwebservice.asmx" + ) + ws_test_url = ( + "http://92.103.148.116/exa-eprintwebservice/eprintwebservice.asmx?WSDL" + ) + encoder = DpdEncoder + decoder = DpdDecoder + transport = DpdTransport + api = DpdApi + manage_multi_label = False + + +factory.register_builder("dpd_fr", "get_label", DpdGetabel) diff --git a/roulier/carriers/dpd_fr/decoder.py b/roulier/carriers/dpd_fr/decoder.py new file mode 100755 index 0000000..8b5ab4d --- /dev/null +++ b/roulier/carriers/dpd_fr/decoder.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Dpd XML -> Python.""" +from lxml import objectify +from roulier.codec import DecoderGetLabel +from roulier import ws_tools as tools +import base64 + + +class DpdDecoder(DecoderGetLabel): + """Dpd XML -> Python.""" + + def decode(self, response, input_payload): + """Understand a CreateShipmentWithLabelsResponse.""" + output_format = input_payload["output_format"] + xml = objectify.fromstring(response["body"]) + shipments, files = xml.CreateShipmentWithLabelsResult.getchildren() + shipment = shipments.getchildren()[0] + label_file, summary_file = files.getchildren() + parcel = { + "id": 1, + "reference": self._get_parcel_number(input_payload) + or shipment.parcelnumber.text, + "tracking": {"number": shipment.barcode.text, "url": "", "partner": "",}, + "label": { # same as main label + "data": label_file.label.text, + "name": "label 1", + "type": output_format, + }, + } + annexes = [ + {"data": summary_file.label.text, "name": "Summary", "type": output_format} + ] + self.result["parcels"].append(parcel) + self.result["annexes"] += annexes diff --git a/roulier/carriers/dpd_fr/encoder.py b/roulier/carriers/dpd_fr/encoder.py new file mode 100755 index 0000000..bb0e30d --- /dev/null +++ b/roulier/carriers/dpd_fr/encoder.py @@ -0,0 +1,64 @@ +"""Transform input to dpd compatible xml.""" +from jinja2 import Environment, PackageLoader +from roulier.codec import Encoder +from datetime import datetime +from roulier.exception import InvalidApiInput +import logging + +DPD_ACTIONS = "createShipmentWithLabels" +log = logging.getLogger(__name__) + + +class DpdEncoder(Encoder): + """Transform input to dpd compatible xml.""" + + def transform_input_to_carrier_webservice(self, data): + """Transform input to dpd compatible xml.""" + + if data["service"]["product"] == "DPD_Predict": + data["service"]["notifications"] = "Predict" + if "pickupLocationId" in data["service"]: + data["service"]["dropOffLocation"] = data["service"].pop("pickupLocationId") + data["service"]["shippingDate"] = datetime.strptime( + data["service"]["shippingDate"], "%Y/%M/%d" + ).strftime("%d/%M/%Y") + + def reduce_address(address): + """Concat some fields. + + Because there is no street2 nor company in DPD api. + append street2 to street1 and truncate at 70 + append company to name + """ + address["street1"] = ("%s, %s" % (address["street1"], address["street2"]))[ + 0:70 + ] + address["name"] = ("%s, %s" % (address["name"], address["company"]))[0:35] + + reduce_address(data["to_address"]) + reduce_address(data["from_address"]) + + output_format = data["service"]["labelFormat"] + # if data["service"]["labelFormat"] in ("PNG", "ZPL"): + if data["service"]["labelFormat"] == "PNG": + # WS doesn't handle zpl yet, we convert it later + # png is named Default, WTF DPD? + data["service"]["labelFormat"] = "Default" + + env = Environment( + loader=PackageLoader("roulier", "/carriers/dpd_fr/templates"), + extensions=["jinja2.ext.with_", "jinja2.ext.autoescape"], + autoescape=True, + ) + + template = env.get_template("dpd_createShipmentWithLabels.xml") + return { + "body": template.render( + service=data["service"], + parcel=data["parcels"][0], + sender_address=data["from_address"], + receiver_address=data["to_address"], + ), + "headers": data["auth"], + "output_format": output_format, + } diff --git a/roulier/carriers/dpd/templates/dpd_address.xml b/roulier/carriers/dpd_fr/templates/dpd_address.xml similarity index 100% rename from roulier/carriers/dpd/templates/dpd_address.xml rename to roulier/carriers/dpd_fr/templates/dpd_address.xml diff --git a/roulier/carriers/dpd/templates/dpd_addressInfo.xml b/roulier/carriers/dpd_fr/templates/dpd_addressInfo.xml similarity index 100% rename from roulier/carriers/dpd/templates/dpd_addressInfo.xml rename to roulier/carriers/dpd_fr/templates/dpd_addressInfo.xml diff --git a/roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml b/roulier/carriers/dpd_fr/templates/dpd_createShipmentWithLabels.xml similarity index 100% rename from roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml rename to roulier/carriers/dpd_fr/templates/dpd_createShipmentWithLabels.xml diff --git a/roulier/carriers/dpd/templates/dpd_header.xml b/roulier/carriers/dpd_fr/templates/dpd_header.xml similarity index 100% rename from roulier/carriers/dpd/templates/dpd_header.xml rename to roulier/carriers/dpd_fr/templates/dpd_header.xml diff --git a/roulier/carriers/dpd/templates/dpd_soap.xml b/roulier/carriers/dpd_fr/templates/dpd_soap.xml similarity index 100% rename from roulier/carriers/dpd/templates/dpd_soap.xml rename to roulier/carriers/dpd_fr/templates/dpd_soap.xml diff --git a/roulier/carriers/dpd_fr/tests/__init__.py b/roulier/carriers/dpd_fr/tests/__init__.py new file mode 100644 index 0000000..85f73bc --- /dev/null +++ b/roulier/carriers/dpd_fr/tests/__init__.py @@ -0,0 +1 @@ +from . import test_dpd diff --git a/roulier/carriers/dpd_fr/tests/credential_demo.py b/roulier/carriers/dpd_fr/tests/credential_demo.py new file mode 100644 index 0000000..0817d48 --- /dev/null +++ b/roulier/carriers/dpd_fr/tests/credential_demo.py @@ -0,0 +1,12 @@ +""" fake credentials + duplicate as credential.py with real values for real tests +""" + +credentials = { + "login": "test-DPDFrance", + "password": "test-DPDFrance", + "customerId": 18026, + "agencyId": 77, + "customerCountry": 250, + "isTest": True, +} diff --git a/roulier/carriers/dpd_fr/tests/data.py b/roulier/carriers/dpd_fr/tests/data.py new file mode 100644 index 0000000..5e43453 --- /dev/null +++ b/roulier/carriers/dpd_fr/tests/data.py @@ -0,0 +1,49 @@ +import logging +from datetime import date + +logger = logging.getLogger(__name__) + +try: + from .credential import credentials +except ImportError: + from .credential_demo import credentials + + logger.debug( + "To test with real credentials copy and paste " + "tests/credential_demo.py to tests/credential.py and " + "fill it with real values" + ) + +DATA = { + "auth": { + "login": credentials["login"], + "password": credentials["password"], + "isTest": credentials["isTest"], + }, + "service": { + "shippingDate": date.today().strftime("%Y/%m/%d"), + "customerId": credentials["customerId"], + "customerCountry": credentials["customerCountry"], + "agencyId": credentials["agencyId"], + "product": "DPD_Classic", + "labelFormat": "PDF", + }, + "parcels": [{"weight": 1.2, "comment": "Fake comment"}], + "from_address": { + "name": "Fr", + "street1": "27 rue Léon CAMET", + "city": "Villeurbanne", + "country": "FR", + "zip": "69100", + "phone": "+330123456789", + }, + "to_address": { + "name": "Fr", + "firstName": "Hpar", + "street1": "27 rue Léon CAMET", + "city": "Villeurbanne", + "country": "FR", + "zip": "69100", + "email": "test@example.com", + }, +} diff --git a/roulier/carriers/dpd_fr/tests/test_dpd.py b/roulier/carriers/dpd_fr/tests/test_dpd.py new file mode 100644 index 0000000..9270db7 --- /dev/null +++ b/roulier/carriers/dpd_fr/tests/test_dpd.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import logging +import copy +import base64 +import requests +import shutil +import pytest +import re + +from roulier import roulier +from roulier.exception import InvalidApiInput, CarrierError + +from .data import DATA + +logger = logging.getLogger(__name__) + + +EXCEPTION_MESSAGE = "Failed call with parameters %s" + + +def test_methods(): + assert ( + "dpd_fr" in roulier.get_carriers_action_available().keys() + ), "DPD carrier unavailable" + assert roulier.get_carriers_action_available()["dpd_fr"] == [ + "get_label", + ], "get_label() method unavailable" + + +def test_label_basic_checks(): + vals = copy.deepcopy(DATA) + vals["service"]["product"] = "whatisitproduct" + + with assert_raises(InvalidApiInput, {"service": [{"product": "unallowed"}]}): + roulier.get("dpd_fr", "get_label", vals) + + del vals["service"]["product"] + result = roulier.get("dpd_fr", "get_label", vals) + assert_result(vals, result, 1, 1) + + +def test_common_failed_get_label(): + vals = copy.deepcopy(DATA) + # Weight + orig_weight = vals["parcels"][0].pop("weight") + with assert_raises(InvalidApiInput, {"parcels": [{0: [{"weight": "float"}]}]}): + roulier.get("dpd_fr", "get_label", vals) + + vals["parcels"][0]["weight"] = 999.99 + with assert_raises(CarrierError, "Invalid weight"): + roulier.get("dpd_fr", "get_label", vals) + + vals["parcels"][0]["weight"] = 0 + with assert_raises(CarrierError, "Invalid weight"): + roulier.get("dpd_fr", "get_label", vals) + vals["parcels"][0]["weight"] = orig_weight + + # Country + vals["to_address"].pop("country") + with assert_raises(InvalidApiInput, {"to_address": [{"country": "empty"}]}): + roulier.get("dpd_fr", "get_label", vals) + + vals["to_address"]["country"] = "ZZ" + with assert_raises(CarrierError, [{"id": "InvalidCountryPrefix"}]): + roulier.get("dpd_fr", "get_label", vals) + + # no address + del vals["to_address"] + with assert_raises(InvalidApiInput, {"to_address": "empty"}): + roulier.get("dpd_fr", "get_label", vals) + + +def test_auth(): + vals = copy.deepcopy(DATA) + vals["auth"]["login"] = "test" + with assert_raises(CarrierError, [{"id": "PermissionDenied"}]): + roulier.get("dpd_fr", "get_label", vals) + + +def test_relai(): + vals = copy.deepcopy(DATA) + vals["service"]["product"] = "DPD_Relais" + with assert_raises( + InvalidApiInput, {"service": [{"pickupLocationId": "mandatory"}]} + ): + roulier.get("dpd_fr", "get_label", vals) + vals["service"]["pickupLocationId"] = "P62025" + result = roulier.get("dpd_fr", "get_label", vals) + assert_result(vals, result, 1, 1) + + +def assert_result(vals, result, parcels, annexes, label_type="PDF"): + assert sorted(result.keys()) == ["annexes", "parcels"], EXCEPTION_MESSAGE % vals + assert len(result["annexes"]) == annexes + assert len(result["parcels"]) == parcels + for parcel in result["parcels"]: + assert_parcel(vals, parcel, label_type) + + +def assert_parcel(vals, parcel, label_type="PDF"): + if label_type: + expected = ["id", "label", "reference", "tracking"] + else: + expected = ["id", "reference", "tracking"] + assert sorted(parcel.keys()) == expected, EXCEPTION_MESSAGE % vals + assert sorted(parcel["tracking"].keys()) == ["number", "partner", "url"], ( + EXCEPTION_MESSAGE % vals + ) + assert parcel["tracking"]["number"] + if label_type: + assert_label(parcel, label_type) + + +def assert_label(parcel, label_type="PDF"): + assert "label" in parcel + assert "data" in parcel["label"] + assert len(parcel["label"]["data"]) > 1024 + assert parcel["label"]["type"] == label_type + + +class assert_raises(object): + def __init__(self, exc_type, expected=None): + self.expected = expected + self.exc_type = exc_type + + def _check_expected(self, errors, expected): + if isinstance(expected, str): + error_repr = "%s" % errors + assert re.search(expected, error_repr), "invalid inputs are not as expected" + return + assert type(errors) == type(expected) + if isinstance(expected, list): + assert len(errors) >= len(expected), "invalid inputs are not as expected" + keys = range(0, len(expected)) + elif isinstance(expected, dict): + error_keys = set(errors.keys()) + expected_keys = set(expected.keys()) + assert not expected_keys - error_keys, "invalid inputs are not as expected" + keys = expected_keys + else: + raise ValueError("invalid expected format") + for k in keys: + if expected is not None: + self._check_expected(errors[k], expected[k]) + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, traceback): + assert exc_type == self.exc_type + if self.expected: + if self.exc_type == InvalidApiInput: + errors = exc_val.args[0]["api_call_exception"] + else: + errors = exc_val.args[0] + self._check_expected(errors, self.expected) + return True diff --git a/roulier/carriers/dpd/dpd_transport.py b/roulier/carriers/dpd_fr/transport.py similarity index 66% rename from roulier/carriers/dpd/dpd_transport.py rename to roulier/carriers/dpd_fr/transport.py index c579374..6ffa9be 100755 --- a/roulier/carriers/dpd/dpd_transport.py +++ b/roulier/carriers/dpd_fr/transport.py @@ -3,7 +3,7 @@ import requests from lxml import objectify, etree from jinja2 import Environment, PackageLoader -from roulier.transport import Transport +from roulier.transport import RequestsTransport from roulier.ws_tools import remove_empty_tags from roulier.exception import CarrierError @@ -12,13 +12,9 @@ log = logging.getLogger(__name__) -class DpdTransport(Transport): +class DpdTransport(RequestsTransport): """Implement Dpd WS communication.""" - DPD_WS = ( - "https://e-station.cargonet.software/dpd-eprintwebservice/eprintwebservice.asmx" - ) - def send(self, payload): """Call this function. @@ -42,7 +38,7 @@ def send(self, payload): def soap_wrap(self, body, auth): """Wrap body in a soap:Enveloppe.""" env = Environment( - loader=PackageLoader("roulier", "/carriers/dpd/templates"), + loader=PackageLoader("roulier", "/carriers/dpd_fr/templates"), extensions=["jinja2.ext.with_"], ) @@ -53,23 +49,17 @@ def soap_wrap(self, body, auth): data = template.render(body=body_stripped, header=header_xml) return data.encode("utf8") - def send_request(self, body): + def _get_requests_headers(self): """Send body to dpd WS.""" - return requests.post( - self.DPD_WS, headers={"content-type": "text/xml"}, data=body - ) + return {"content-type": "text/xml"} def handle_500(self, response): """Handle reponse in case of ERROR 500 type.""" log.warning("Dpd error 500") obj = objectify.fromstring(response.content) - errors = [ - { - "id": obj.xpath("//faultcode")[0], - "message": obj.xpath("//faultstring")[0], - } - ] - raise CarrierError(response, errors) + error_id = (obj.xpath("//ErrorId") or obj.xpath("//faultcode"))[0] + error_message = (obj.xpath("//ErrorMessage") or obj.xpath("//faultstring"))[0] + raise CarrierError(response, [{"id": error_id, "message": error_message,}]) def handle_200(self, response): """Handle response type 200 (success).""" @@ -84,15 +74,3 @@ def extract_soap(response_xml): "body": body_xml, "response": response, } - - def handle_response(self, response): - """Handle response of webservice.""" - if response.status_code == 200: - return self.handle_200(response) - elif response.status_code == 500: - return self.handle_500(response) - else: - raise CarrierError( - response, - [{"id": None, "message": "Unexpected status code from server",}], - )