diff --git a/.gitignore b/.gitignore index c3d2992..53b258f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ # Add any directories, files, or patterns you don't want to be tracked by version control +# Compiled python modules. *.pyc -.idea \ No newline at end of file + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info + +.idea +*.ipynb* +*.pdf +venv/ +/tests/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..965a418 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 The SaleGroup Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..44a2766 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Despatch Bay Python SDK diff --git a/despatchbay/despatchbay_entities.py b/despatchbay/despatchbay_entities.py new file mode 100644 index 0000000..0825c57 --- /dev/null +++ b/despatchbay/despatchbay_entities.py @@ -0,0 +1,957 @@ +""" +Entities relating to types in the Despatchbay v15 api +https://github.com/despatchbay/despatchbay-api-v15/wiki +""" + + +class Entity: + """Base class for Despatchbay entities""" + def __init__(self, soap_type, soap_client, soap_map): + self.soap_type = soap_type + self.soap_client = soap_client + self.soap_map = soap_map + + def to_soap_object(self): + """ + Creates a SOAP client object representation of this entity. + """ + suds_object = self.soap_client.factory.create(self.soap_type) + for soap_property in self.soap_map: + if self.soap_map[soap_property]['type'] == 'entity': + setattr( + suds_object, + soap_property, + getattr( + self, + self.soap_map[soap_property]['property']).to_soap_object() + ) + elif self.soap_map[soap_property]['type'] == 'entityArray': + entity_list = [] + for entity in getattr(self, self.soap_map[soap_property]['property']): + entity_list.append(entity.to_soap_object()) + soap_array = self.soap_client.factory.create(self.soap_map[soap_property]['soap_type']) + soap_array.item = entity_list + soap_array._arrayType = 'urn:ArrayType[]' + setattr(suds_object, soap_property, soap_array) + else: + setattr( + suds_object, soap_property, getattr( + self, + self.soap_map[soap_property]['property'] + ) + ) + return suds_object + + def __repr__(self): + return str(self.to_soap_object()) + + +class Account(Entity): + """ + Represents a Despactchbay API AccountType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Account-Service#accounttype + """ + SOAP_MAP = { + 'AccountID': { + 'property': 'account_id', + 'type': 'integer' + }, + 'AccountName': { + 'property': 'name', + 'type': 'string' + }, + 'AccountBalance': { + 'property': 'balance', + 'type': 'entity', + 'soap_type': 'ns1:AccountBalanceType', + } + } + SOAP_TYPE = 'ns1:AccountType' + + def __init__(self, client, account_id=None, name=None, balance=None): + super().__init__(self.SOAP_TYPE, client.account_client, self.SOAP_MAP) + self.account_id = account_id + self.name = name + self.balance = balance + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + account_id=soap_dict.get('AccountID', None), + name=soap_dict.get('AccountName', None), + balance=AccountBalance.from_dict( + client, + client.account_client.dict(soap_dict.get('AccountBalance', None)) + ) + ) + + +class AccountBalance(Entity): + """ + Represents a Despactchbay API AccountBalanceType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Account-Service#accountbalancetype + """ + SOAP_MAP = { + 'Balance': { + 'property': 'balance', + 'type': 'float' + }, + 'AvailableBalance': { + 'property': 'available', + 'type': 'float' + } + } + SOAP_TYPE = 'ns1:AccountBalanceType' + + def __init__(self, client, balance=None, available=None): + super().__init__(self.SOAP_TYPE, client.account_client, self.SOAP_MAP) + self.balance = balance + self.available = available + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + balance=soap_dict.get('Balance', None), + available=soap_dict.get('AvailableBalance', None) + ) + + +class Address(Entity): + """ + Represents a Despactchbay API AddressType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Account-Service#addresstype + """ + SOAP_MAP = { + 'CompanyName': { + 'property': 'company_name', + 'type': 'string', + }, + 'Street': { + 'property': 'street', + 'type': 'string', + }, + 'Locality': { + 'property': 'locality', + 'type': 'string', + }, + 'TownCity': { + 'property': 'town_city', + 'type': 'string', + }, + 'County': { + 'property': 'county', + 'type': 'string', + }, + 'PostalCode': { + 'property': 'postal_code', + 'type': 'string', + }, + 'CountryCode': { + 'property': 'country_code', + 'type': 'string', + } + } + SOAP_TYPE = 'ns1:AddressType' + + def __init__(self, client, company_name=None, street=None, locality=None, town_city=None, county=None, + postal_code=None, country_code=None): + super().__init__(self.SOAP_TYPE, client.addressing_client, self.SOAP_MAP) + self.company_name = company_name + self.street = street + self.locality = locality + self.town_city = town_city + self.county = county + self.postal_code = postal_code + self.country_code = country_code + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative initialiser, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + company_name=soap_dict.get('CompanyName', None), + street=soap_dict.get('Street', None), + locality=soap_dict.get('Locality', None), + town_city=soap_dict.get('TownCity', None), + county=soap_dict.get('County', None), + postal_code=soap_dict.get('PostalCode', None), + country_code=soap_dict.get('CountryCode', None) + ) + + +class AddressKey(Entity): + """ + Represents a Despactchbay API AddressKeyType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Addressing-Service#addresskeytype + """ + SOAP_MAP = { + 'Key': { + 'property': 'key', + 'type': 'string', + }, + 'Address': { + 'property': 'address', + 'type': 'string', + } + } + SOAP_TYPE = 'ns1:AddressKeyType' + + def __init__(self, client, key, address): + super().__init__(self.SOAP_TYPE, client.addressing_client, self.SOAP_MAP) + self.key = key + self.address = address + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + key=soap_dict.get('Key', None), + address=soap_dict.get('Address', None) + ) + + +class AutomaticTopupSettings(Entity): + """ + Represents a Despactchbay API AutomaticTopupSettings entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Account-Service#automatictopupsettings + """ + SOAP_MAP = { + "MinimumBalance": { + "property": "minimum_balance", + "type": "float" + }, + "TopupAmount": { + "property": "topup_amount", + "type": "float" + }, + "PaymentMethodID": { + "property": "payment_method_id", + "type": "string" + } + } + SOAP_TYPE = 'ns1:AutomaticTopupsSettingsRequestType' + + def __init__(self, client, minimum_balance=None, topup_amount=None, payment_method_id=None): + super().__init__(self.SOAP_TYPE, client.account_client, self.SOAP_MAP) + self.minimum_balance = minimum_balance + self.topup_amount = topup_amount + self.payment_method_id = payment_method_id + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + minimum_balance=soap_dict.get('MinimumBalance', None), + topup_amount=soap_dict.get('TopupAmount', None), + payment_method_id=soap_dict.get('PaymentMethodID', None) + ) + + +class Collection(Entity): + """ + Represents a Despactchbay API CollectionType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#collectionreturntype + """ + SOAP_MAP = { + "CollectionID": { + "property": "collection_id", + "type": "string" + }, + "CollectionDocumentID": { + "property": "document_id", + "type": "string" + }, + "CollectionType": { + "property": "collection_type", + "type": "string" + }, + "CollectionDate": { + "property": "date", + "type": "string" + }, + "SenderAddress": { + "property": "sender_address", + "type": "string" + }, + "Courier": { + "property": "collection_courier", + "type": "string" + }, + "LabelsURL": { + "property": "labels_url", + "type": "string" + }, + "Manifest": { + "property": "manifest_url", + "type": "string" + } + } + SOAP_TYPE = 'ns1:CollectionReturnType' + + def __init__(self, client, collection_id=None, document_id=None, collection_type=None, date=None, + sender_address=None, collection_courier=None, labels_url=None, manifest_url=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.despatchbay_client = client + self.collection_id = collection_id + self.document_id = document_id + self.collection_type = collection_type + self.date = date + self.sender_address = sender_address + self.collection_courier = collection_courier + self.labels_url = labels_url + self.manifest_url = manifest_url + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + collection_id=soap_dict.get('CollectionID'), + document_id=soap_dict.get('CollectionDocumentID'), + collection_type=soap_dict.get('CollectionType'), + date=CollectionDate.from_dict( + client, + client.shipping_client.dict(soap_dict.get('CollectionDate')) + ), + sender_address=Sender.from_dict( + client, + client.shipping_client.dict(soap_dict.get('SenderAddress', None)) + ), + collection_courier=Courier.from_dict( + client, + client.shipping_client.dict(soap_dict.get('Courier', None)) + ), + labels_url=soap_dict.get('LabelsURL', None), + manifest_url=soap_dict.get('Manifest', None) + ) + + def get_labels(self, **kwargs): + """ + Fetches label pdf through the Despatch Bay API client. + """ + return self.despatchbay_client.get_labels(self.document_id, **kwargs) + + def get_manifest(self, **kwargs): + """ + Fetches menifest pdf through the Despatch Bay API client. + """ + return self.despatchbay_client.get_manifest(self.document_id, **kwargs) + + +class CollectionDate(Entity): + """ + Represents a Despactchbay API CollectionDateType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#collectiondatetype + """ + SOAP_MAP = { + "CollectionDate": { + "property": "date", + "type": "string" + } + } + SOAP_TYPE = 'ns1:CollectionDateType' + + def __init__(self, client, date=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.date = date + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + date=soap_dict.get('CollectionDate', None) + ) + + +class Courier(Entity): + """ + Represents a Despactchbay API CourierType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#couriertype + """ + SOAP_MAP = { + 'CourierID': { + 'property': 'courier_id', + 'type': 'integer' + }, + 'CourierName': { + 'property': 'name', + 'type': 'string' + } + } + SOAP_TYPE = 'ns1:CourierType' + + def __init__(self, client, courier_id=None, name=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.courier_id = courier_id + self.name = name + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + courier_id=soap_dict.get('CourierID', None), + name=soap_dict.get('CourierName', None) + ) + + +class Parcel(Entity): + """ + Represents a Despactchbay API ParcelType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#parceltype + """ + SOAP_MAP = { + 'Weight': { + 'property': 'weight', + 'type': 'float' + }, + 'Length': { + 'property': 'length', + 'type': 'float' + }, + 'Width': { + 'property': 'width', + 'type': 'float' + }, + 'Height': { + 'property': 'height', + 'type': 'float' + }, + 'Contents': { + 'property': 'contents', + 'type': 'string' + }, + 'Value': { + 'property': 'value', + 'type': 'float' + }, + 'TrackingNumber': { + 'property': 'tracking_number', + 'type': 'string' + } + } + SOAP_TYPE = 'ns1:ParcelType' + + def __init__(self, client, weight=None, length=None, width=None, height=None, + contents=None, value=None, tracking_number=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.weight = weight + self.length = length + self.width = width + self.height = height + self.contents = contents + self.value = value + self.tracking_number = tracking_number + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + weight=soap_dict.get('Weight', None), + length=soap_dict.get('Length', None), + width=soap_dict.get('Width', None), + height=soap_dict.get('Height', None), + contents=soap_dict.get('Contents', None), + value=soap_dict.get('Value', None), + tracking_number=soap_dict.get('TrackingNumber', None) + ) + + +class PaymentMethod(Entity): + """ + Represents a Despactchbay API PaymentMethodType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Account-Service#paymentmethodtype + """ + SOAP_MAP = { + 'PaymentMethodID': { + 'property': 'payment_method_id', + 'type': 'integer' + }, + 'Type': { + 'property': 'payment_method_type', + 'type': 'string' + }, + 'Description': { + 'property': 'description', + 'type': 'string' + } + } + SOAP_TYPE = 'ns1:PaymentMethodType' + + def __init__(self, client, payment_method_id=None, payment_method_type=None, description=None): + super().__init__(self.SOAP_TYPE, client.account_client, self.SOAP_MAP) + self.payment_method_id = payment_method_id + self.payment_method_type = payment_method_type + self.description = description + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + payment_method_id=soap_dict.get('PaymentMethodID', None), + payment_method_type=soap_dict.get('Type', None), + description=soap_dict.get('Description', None) + ) + + +class Recipient(Entity): + """ + Represents a Despactchbay API RecipientAddressType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#recipientaddresstype + """ + SOAP_MAP = { + 'RecipientName': { + 'property': 'name', + 'type': 'string', + }, + 'RecipientTelephone': { + 'property': 'telephone', + 'type': 'string', + }, + 'RecipientEmail': { + 'property': 'email', + 'type': 'string', + }, + 'RecipientAddress': { + 'property': 'recipient_address', + 'type': 'entity', + 'soap_type': 'ns1:RecipientAddressType', + }, + } + SOAP_TYPE = 'ns1:RecipientAddressType' + + def __init__(self, client, name=None, telephone=None, email=None, recipient_address=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.name = name + self.telephone = telephone + self.email = email + self.recipient_address = recipient_address + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + name=soap_dict.get('RecipientName', None), + telephone=soap_dict.get('RecipientTelephone', None), + email=soap_dict.get('RecipientEmail', None), + recipient_address=Address.from_dict( + client, + client.shipping_client.dict(soap_dict.get('RecipientAddress', None)) + ) + ) + + +class Sender(Entity): + """ + Represents a Despactchbay API SenderAddressType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#senderaddresstype + """ + SOAP_MAP = { + 'SenderName': { + 'property': 'name', + 'type': 'string', + }, + 'SenderTelephone': { + 'property': 'telephone', + 'type': 'string', + }, + 'SenderEmail': { + 'property': 'email', + 'type': 'string', + }, + 'SenderAddress': { + 'property': 'sender_address', + 'type': 'entity', + 'soap_type': 'ns1:SenderAddress', + + }, + 'SenderAddressID': { + 'property': 'address_id', + 'type': 'integer', + }, + } + SOAP_TYPE = 'ns1:SenderAddressType' + + def __init__(self, client, name=None, telephone=None, email=None, sender_address=None, address_id=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.name = name + self.telephone = telephone + self.email = email + if sender_address: + self.sender_address = sender_address + else: + self.sender_address = Address(client) + self.address_id = address_id + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + name=soap_dict.get('SenderName', None), + telephone=soap_dict.get('SenderTelephone', None), + email=soap_dict.get('SenderEmail', None), + sender_address=Address.from_dict( + client, + client.shipping_client.dict(soap_dict.get('SenderAddress', None)) + ), + address_id=soap_dict.get('SenderAddressID') + ) + + def to_soap_object(self): + """ + Creates a SOAP client object representation of this entity. + + Removes sender address property if a sender address id is provided. + """ + soap_object = super().to_soap_object() + if soap_object.SenderAddressID: + soap_object.SenderAddress = None + return soap_object + + +class Service(Entity): + """ + Represents a Despactchbay API ServiceType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#servicetype + """ + SOAP_MAP = { + 'ServiceID': { + 'property': 'service_id', + 'type': 'integer' + }, + 'Format': { + 'property': 'service_format', + 'type': 'string' + }, + 'Name': { + 'property': 'name', + 'type': 'string' + }, + 'Cost': { + 'property': 'cost', + 'type': 'currency', + }, + 'Courier': { + 'property': 'courier', + 'type': 'entity', + 'soap_type': 'ns1:CourierType', + + }, + } + SOAP_TYPE = 'ns1:ServiceType' + + def __init__(self, client, service_id, service_format, name, cost, courier): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.service_id = service_id + self.format = service_format + self.name = name + self.cost = cost + self.courier = courier + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + return cls( + client=client, + service_id=soap_dict.get('ServiceID', None), + service_format=soap_dict.get('Format', None), + name=soap_dict.get('Name', None), + cost=soap_dict.get('Cost', None), + courier=Courier.from_dict( + client, + client.shipping_client.dict(soap_dict.get('Courier', None)) + ) + ) + + +class ShipmentRequest(Entity): + """ + Represents a Despactchbay API ShipmentRequest entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#shipmentrequesttype + """ + SOAP_MAP = { + 'ServiceID': { + 'property': 'service_id', + 'type': 'string', + }, + 'Parcels': { + 'property': 'parcels', + 'type': 'entityArray', + 'soap_type': 'ns1:ArrayOfParcelType', + }, + 'ClientReference': { + 'property': 'client_reference', + 'type': 'string', + }, + 'CollectionDate': { + 'property': 'collection_date', + 'type': 'entity', + 'soap_type': 'ns1:CollectionDateType', + }, + 'RecipientAddress': { + 'property': 'recipient_address', + 'type': 'entity', + 'soap_type': 'ns1:RecipientAddressType', + }, + 'SenderAddress': { + 'property': 'sender_address', + 'type': 'entity', + 'soap_type': 'ns1:SenderAddressType', + }, + 'FollowShipment': { + 'property': 'follow_shipment', + 'type': 'boolean', + } + } + SOAP_TYPE = 'ns1:ShipmentRequestType' + + def __init__(self, client, service_id=None, parcels=None, client_reference=None, collection_date=None, + sender_address=None, recipient_address=None, follow_shipment=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.despatchbay_client = client + self.service_id = service_id + self.parcels = parcels + self.client_reference = client_reference + self._collection_date = self.validate_collection_date_object(collection_date) + self.sender_address = sender_address + self.recipient_address = recipient_address + self.follow_shipment = follow_shipment + + def validate_collection_date_object(self, collection_date): + """ + Converts a string timestamp to a CollectionDate object. + """ + if isinstance(collection_date, str): + return CollectionDate(self.despatchbay_client, date=collection_date) + return collection_date + + @property + def collection_date(self): + """ + Returns the private attribute collection_date + """ + return self._collection_date + + @collection_date.setter + def collection_date(self, collection_date): + """ + Allows the collection date to be set from just a timestamp string but limits the + type of the _collection_date attribute to a CollectionDate object. + """ + self._collection_date = self.validate_collection_date_object(collection_date) + + +class ShipmentReturn(Entity): + """ + Represents a Despactchbay API ShipmentReturnType entity. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Shipping-Service#shipmentreturntype + """ + SOAP_MAP = { + 'ShipmentID': { + 'property': 'shipment_id', + 'type': 'string', + }, + 'ShipmentDocumentID': { + 'property': 'shipment_document_id', + 'type': 'string', + }, + 'CollectionID': { + 'property': 'collection_id', + 'type': 'string', + }, + 'CollectionDocumentID': { + 'property': 'collection_document_id', + 'type': 'string', + }, + 'ServiceID': { + 'property': 'service_id', + 'type': 'string', + }, + 'Parcels': { + 'property': 'parcels', + 'type': 'entityArray', + 'soap_type': 'ns1:ArrayOfParcelType', + }, + 'ClientReference': { + 'property': 'client_reference', + 'type': 'string', + }, + 'RecipientAddress': { + 'property': 'recipient_address', + 'type': 'entity', + 'soap_type': 'ns1:RecipientAddressType', + }, + 'IsFollowed': { + 'property': 'is_followed', + 'type': 'boolean', + }, + 'IsPrinted': { + 'property': 'is_printed', + 'type': 'boolean', + }, + 'IsDespatched': { + 'property': 'is_despatched', + 'type': 'boolean', + }, + 'IsDelivered': { + 'property': 'is_delivered', + 'type': 'boolean', + }, + 'IsCancelled': { + 'property': 'is_cancelled', + 'type': 'boolean', + }, + 'LabelsURL': { + 'property': 'labels_url', + 'type': 'boolean', + }, + } + SOAP_TYPE = 'ns1:ShipmentReturnType' + + def __init__(self, client, shipment_id=None, shipment_document_id=None, collection_id=None, + collection_document_id=None, service_id=None, parcels=None, client_reference=None, + recipient_address=None, is_followed=None, is_printed=None, is_despatched=None, + is_delivered=None, is_cancelled=None, labels_url=None): + super().__init__(self.SOAP_TYPE, client.shipping_client, self.SOAP_MAP) + self.despatchbay_client = client + self.shipment_id = shipment_id + self.shipment_document_id = shipment_document_id + self.collection_id = collection_id + self.collection_document_id = collection_document_id + self.service_id = service_id + self.parcels = parcels + self.client_reference = client_reference + self.recipient_address = recipient_address + self.is_followed = is_followed + self.is_printed = is_printed + self.is_despatched = is_despatched + self.is_delivered = is_delivered + self.is_cancelled = is_cancelled + self.labels_url = labels_url + + @classmethod + def from_dict(cls, client, soap_dict): + """ + Alternative constructor, builds entity object from a dictionary representation of + a SOAP response created by the SOAP client. + """ + parcel_array = [] + for parcel_item in soap_dict.get('Parcels'): + parcel_array.append( + Parcel.from_dict( + client, + client.shipping_client.dict(parcel_item) + ) + ) + return cls( + client=client, + shipment_id=soap_dict.get('ShipmentID'), + shipment_document_id=soap_dict.get('ShipmentDocumentID'), + collection_id=soap_dict.get('CollectionID'), + collection_document_id=soap_dict.get('CollectionDocumentID'), + service_id=soap_dict.get('ServiceID'), + parcels=parcel_array, + client_reference=soap_dict.get('ClientReference'), + recipient_address=Recipient.from_dict( + client, + client.shipping_client.dict(soap_dict.get('RecipientAddress', None)) + ), + is_followed=soap_dict.get('IsFollowed'), + is_printed=soap_dict.get('IsPrinted'), + is_despatched=soap_dict.get('IsDespatched'), + is_delivered=soap_dict.get('IsDelivered'), + is_cancelled=soap_dict.get('IsCancelled'), + labels_url=soap_dict.get('LabelsURL', None) + ) + + def book(self): + """ + Makes a BookShipment request through the Despatch Bay API client. + """ + book_return = self.despatchbay_client.book_shipments([self.shipment_id]) + if book_return: + self.shipment_document_id = book_return[0].shipment_document_id + self.collection_id = book_return[0].collection_id + self.collection_document_id = book_return[0].collection_document_id + self.labels_url = book_return[0].labels_url + return book_return[0] + + def cancel(self): + """ + Makes a CancelShipment request through the Despatch Bay API client. + """ + cancel_return = self.despatchbay_client.cancel_shipment(self.shipment_id) + if cancel_return: + self.is_cancelled = True + return cancel_return + + def get_labels(self, **kwargs): + """ + Fetches label pdf through the Despatch Bay API client. + """ + return self.despatchbay_client.get_labels(self.shipment_document_id, **kwargs) diff --git a/despatchbay/despatchbay_sdk.py b/despatchbay/despatchbay_sdk.py new file mode 100644 index 0000000..a1a9e24 --- /dev/null +++ b/despatchbay/despatchbay_sdk.py @@ -0,0 +1,380 @@ +""" +Client for the Despatchbay v15 api +https://github.com/despatchbay/despatchbay-api-v15/wiki +""" + + +from suds.client import Client +from suds.transport.http import HttpAuthenticated +import suds +from . import despatchbay_entities, documents_client, exceptions + +MIN_SOAP_API_VERSION = 15 +MIN_DOCUMENTS_API_VERSION = 1 + +def handle_suds_fault(error): + """ + Throws despatchbaysdk exceptions in response to suds exceptions + """ + try: + exception_info = error.args[0].decode() + except AttributeError: + exception_info = error.args[0] + if 'Unauthorized' in exception_info: + raise exceptions.AuthorizationException('Invalid API credentials') from error + if 'Could not connect to host' in exception_info: + raise exceptions.ConnectionException('Failed to connect to the Despatch Bay API') from error + if 'Your access rate limit for this service has been exceeded' in exception_info: + raise exceptions.RateLimitException(exception_info) + # Re-raise general suds exceptions as exceptions.ApiException + raise exceptions.ApiException(error) from error + + +def handle_suds_generic_fault(error): + """ + Throws despatchbaysdk exceptions in response to suds generic 'Exception' exceptions + """ + try: + exception_info = error.args[0].decode() + except AttributeError: + exception_info = error.args[0] + if 401 in exception_info: + raise exceptions.AuthorizationException('Invalid API credentials') from error + if 429 in exception_info: + raise exceptions.RateLimitException(exception_info[1]) + # Re-raise original error + raise error + + +def try_except(function): + """ + A decorator to catch suds exceptions + """ + def wrapped(*args, **kwargs): + try: + return function(*args, **kwargs) + except suds.WebFault as detail: + handle_suds_fault(detail) + except Exception as detail: + handle_suds_generic_fault(detail) + + return wrapped + + +class DespatchBaySDK: + """ + Client for despatchbay v15 api. + + https://github.com/despatchbay/despatchbay-api-v15/wiki + """ + def __init__(self, api_user, api_key, api_domain='api.despatchbay.com', despactchbay_api_version=MIN_SOAP_API_VERSION, documents_api_version=MIN_DOCUMENTS_API_VERSION): + if despactchbay_api_version < MIN_SOAP_API_VERSION: + raise exceptions.ApiException("DespatchBay API version must be 15 or higher.") + if documents_api_version < MIN_DOCUMENTS_API_VERSION: + raise exceptions.ApiException("Documents API version must be 1 or higher.") + soap_url_template = 'http://{}/soap/v{}/{}?wsdl' + documents_url = 'http://{}/documents/v{}'.format(api_domain, documents_api_version) + account_url = soap_url_template.format(api_domain, despactchbay_api_version, 'account') + shipping_url = soap_url_template.format(api_domain, despactchbay_api_version, 'shipping') + addressing_url = soap_url_template.format(api_domain, despactchbay_api_version, 'addressing') + tracking_url = soap_url_template.format(api_domain, despactchbay_api_version, 'tracking') + self.account_client = Client( + account_url, transport=self.create_transport(api_user, api_key)) + self.addressing_client = Client( + addressing_url, transport=self.create_transport(api_user, api_key)) + self.shipping_client = Client( + shipping_url, transport=self.create_transport(api_user, api_key)) + self.tracking_client = Client( + tracking_url, transport=self.create_transport(api_user, api_key)) + self.pdf_client = documents_client.DocumentsClient(api_url=documents_url) + + @staticmethod + def create_transport(username, password): + """ + HTTP transport providing authentication for suds. + """ + return HttpAuthenticated(username=username, password=password) + + # Shipping entities + + def parcel(self, **kwargs): + """ + Creates a dbp parcel entity + """ + return despatchbay_entities.Parcel(self, **kwargs) + + def address(self, **kwargs): + """ + Creates a dbp address entity + """ + return despatchbay_entities.Address(self, **kwargs) + + def recipient(self, **kwargs): + """ + Creates a dbp recipient address entity + """ + return despatchbay_entities.Recipient(self, **kwargs) + + def sender(self, **kwargs): + """ + Creates a dbp sender address entity + """ + return despatchbay_entities.Sender(self, **kwargs) + + def shipment_request(self, **kwargs): + """ + Creates a dbp shipment entity + """ + return despatchbay_entities.ShipmentRequest(self, **kwargs) + + # Account Services + + @try_except + def get_account(self): + """Calls GetAccount from the Despatch Bay Account Service.""" + account_dict = self.account_client.dict(self.account_client.service.GetAccount()) + return despatchbay_entities.Account.from_dict( + self, + account_dict + ) + + @try_except + def get_account_balance(self): + """ + Calls GetBalance from the Despatch Bay Account Service. + """ + balance_dict = self.account_client.dict(self.account_client.service.GetAccountBalance()) + return despatchbay_entities.AccountBalance.from_dict( + self, + balance_dict + ) + + @try_except + def get_sender_addresses(self): + """ + Calls GetSenderAddresses from the Despatch Bay Account Service. + """ + sender_addresses_dict_list = [] + for sender_address in self.account_client.service.GetSenderAddresses(): + sender_address_dict = self.account_client.dict(sender_address) + sender_addresses_dict_list.append(despatchbay_entities.Sender.from_dict( + self, + sender_address_dict)) + return sender_addresses_dict_list + + @try_except + def get_services(self): + """ + Calls GetServices from the Despatch Bay Account Service. + """ + service_list = [] + for account_service in self.account_client.service.GetServices(): + service_list.append( + despatchbay_entities.Service.from_dict( + self, + self.account_client.dict(account_service) + )) + return service_list + + @try_except + def get_payment_methods(self): + """ + Calls GetPaymentMethods from the Despatch Bay Account Service. + """ + payment_methods = [] + for payment_method in self.account_client.service.GetPaymentMethods(): + payment_methods.append( + despatchbay_entities.PaymentMethod.from_dict( + self, + self.account_client.dict(payment_method) + ) + ) + return payment_methods + + @try_except + def enable_automatic_topups(self, minimum_balance=None, topup_amount=None, + payment_method_id=None, automatic_topup_settings_object=None): + """ + Calls EnableAutomaticTopups from the Despatch Bay Account Service. + + Passing an automatic_topup_settings object takes priority over using individual arguments. + """ + if not automatic_topup_settings_object: + automatic_topup_settings_object = despatchbay_entities.AutomaticTopupSettings( + self, minimum_balance, topup_amount, payment_method_id) + return self.account_client.service.EnableAutomaticTopups( + automatic_topup_settings_object.to_soap_object()) + + @try_except + def disable_automatic_topups(self): + """ + Calls DisableAutomaticTopups from the Despatch Bay Account Service. + """ + return self.account_client.service.DisableAutomaticTopups() + + # Addressing Services + + @try_except + def find_address(self, postcode, property_string): + """ + Calls FindAddress from the Despatch Bay Addressing Service. + """ + found_address_dict = self.addressing_client.dict( + self.addressing_client.service.FindAddress( + postcode, property_string + )) + return despatchbay_entities.Address.from_dict( + self, + found_address_dict + ) + + @try_except + def get_address_by_key(self, key): + """ + Calls GetAddressByKey from the Despatch Bay Addressing Service. + """ + found_address_dict = self.addressing_client.dict( + self.addressing_client.service.GetAddressByKey(key) + ) + return despatchbay_entities.Address.from_dict( + self, + found_address_dict + ) + + @try_except + def get_address_keys_by_postcode(self, postcode): + """ + Calls GetAddressKeysFromPostcode from the Despatch Bay Addressing Service. + """ + address_keys_dict_list = [] + for soap_address_key in self.addressing_client.service.GetAddressKeysByPostcode(postcode): + address_key_dict = self.account_client.dict(soap_address_key) + address_keys_dict_list.append(despatchbay_entities.AddressKey.from_dict( + self, + address_key_dict)) + return address_keys_dict_list + + # Shipping services + + @try_except + def get_available_services(self, shipment_request): + """ + Calls GetAvailableServices from the Despatch Bay Shipping Service. + """ + available_service_dict_list = [] + for available_service in self.shipping_client.service.GetAvailableServices( + shipment_request.to_soap_object()): + available_service_dict = self.shipping_client.dict(available_service) + available_service_dict_list.append(despatchbay_entities.Service.from_dict( + self, + available_service_dict)) + return available_service_dict_list + + @try_except + def get_available_collection_dates(self, sender_address, courier_id): + """ + Calls GetAvailableCollectionDates from the Despatch Bay Shipping Service. + """ + available_collection_dates_response = self.shipping_client.service.GetAvailableCollectionDates( + sender_address.to_soap_object(), courier_id) + available_collection_dates_list = [] + for collection_date in available_collection_dates_response: + collection_date_dict = self.shipping_client.dict(collection_date) + available_collection_dates_list.append( + despatchbay_entities.CollectionDate.from_dict(self, collection_date_dict)) + return available_collection_dates_list + + @try_except + def get_collection(self, collection_id): + """ + Calls GetCollection from the Despatch Bay Shipping Service. + """ + collection_dict = self.shipping_client.dict( + self.shipping_client.service.GetCollection(collection_id)) + return despatchbay_entities.Collection.from_dict( + self, + collection_dict + ) + + @try_except + def get_collections(self): + """ + Calls GetCollections from the Despatch Bay Shipping Service. + """ + collections_dict_list = [] + for found_collection in self.shipping_client.service.GetCollections(): + collection_dict = self.shipping_client.dict(found_collection) + collections_dict_list.append( + despatchbay_entities.Collection.from_dict( + self, + collection_dict + ) + ) + return collections_dict_list + + @try_except + def get_shipment(self, shipment_id): + """ + Calls GetShipment from the Despatch Bay Shipping Service. + """ + shipment_dict = self.shipping_client.dict( + self.shipping_client.service.GetShipment(shipment_id)) + return despatchbay_entities.ShipmentReturn.from_dict( + self, + shipment_dict + ) + + @try_except + def add_shipment(self, shipment_request): + """ + Calls AddShipment from the Despatch Bay Shipping Service. + """ + return self.shipping_client.service.AddShipment(shipment_request.to_soap_object()) + + @try_except + def book_shipments(self, shipment_ids): + """ + Calls BookShipments from the Despatch Bay Shipping Service. + """ + array_of_shipment_id = self.shipping_client.factory.create('ns1:ArrayOfShipmentID') + array_of_shipment_id.item = shipment_ids + booked_shipments_list = [] + for booked_shipment in self.shipping_client.service.BookShipments(array_of_shipment_id): + booked_shipment_dict = self.shipping_client.dict(booked_shipment) + booked_shipments_list.append( + despatchbay_entities.ShipmentReturn.from_dict( + self, + booked_shipment_dict + ) + ) + return booked_shipments_list + + @try_except + def cancel_shipment(self, shipment_id): + """ + Calls CancelShipment from the Despatch Bay Shipping Service. + """ + return self.shipping_client.service.CancelShipment(shipment_id) + + # Tracking services + + @try_except + def get_tracking(self, tracking_number): + """ + Calls GetTracking from the Despatch Bay Tracking Service. + """ + return self.tracking_client.service.GetTracking(tracking_number) + + # Documents services + + def get_labels(self, document_ids, **kwargs): + """ + Fetches labels from the Despatch Bay documents API. + """ + return self.pdf_client.get_labels(document_ids, **kwargs) + + def get_manifest(self, collection_document_id, **kwargs): + """ + Fetches manifests from the Despatch Bay documents API. + """ + return self.pdf_client.get_manifest(collection_document_id, **kwargs) diff --git a/despatchbay/documents_client.py b/despatchbay/documents_client.py new file mode 100644 index 0000000..8825345 --- /dev/null +++ b/despatchbay/documents_client.py @@ -0,0 +1,105 @@ +""" +Classes for working with the despatchbay documents api + +https://github.com/despatchbay/despatchbay-api-v15/wiki/Documents-API +""" + +from urllib.parse import urlencode +import base64 + +import requests + +from . import exceptions + + +class Document: + """ + A document (label/manifest) in the despatchbay documents api. + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Documents-API + """ + def __init__(self, data): + self.data = data + + def get_raw(self): + """ + Returns the raw data used to create the entity. + """ + return self.data + + def get_base64(self): + """ + Base 64 encodes the document data before returning it. + """ + return base64.b64encode(self.data) + + def download(self, path): + """ + Saves the file to the specified location. + """ + with open(path, 'wb') as document_file: + document_file.write(self.data) + + +class DocumentsClient: + """ + Client for the despatchbay documents api + + https://github.com/despatchbay/despatchbay-api-v15/wiki/Documents-API + """ + def __init__(self, api_url='http://api.despatchbay.com/documents/v1'): + self.api_url = api_url + + @staticmethod + def handle_response_code(code, response): + """ + Returns true if code is 200, otherwise raises an appropriate exception. + """ + if code == 200: + return True + if code == 400: + raise exceptions.InvalidArgumentException( + 'The Documents API was unable to process the request: {}'.format(response)) + if code == 401: + raise exceptions.AuthorizationException('Unauthorized') + if code == 402: + raise exceptions.PaymentException('Insufficient Despatch Bay account balance') + if code == 404: + raise exceptions.ApiException('Unknown shipment ID') + raise exceptions.ApiException('An unexpected error occurred (HTTP {})'.format(code)) + + def get_labels(self, document_ids, label_layout=None, label_format=None, label_dpi=None): + """ + Returns a document entity of the shipment labels identified by the document_ids arg + which can be a comma separated string of shipment IDs or a single collection ID. + """ + if isinstance(document_ids, list): + shipment_string = ','.join(document_ids) + else: + shipment_string = document_ids + query_dict = {} + if label_layout: + query_dict['layout'] = label_layout + if label_format: + query_dict['format'] = label_format + if label_format == 'png_base64' and label_dpi: + query_dict['dpi'] = label_dpi + label_request_url = '{}/labels/{}'.format(self.api_url, + shipment_string) + if query_dict: + query_string = urlencode(query_dict) + label_request_url = label_request_url + '?' + query_string + response = requests.get(label_request_url) + self.handle_response_code(response.status_code, response.text) + return Document(response.content) + + def get_manifest(self, collection_document_id, manifest_format=None): + """ + Returns a document entity of the shipment manifest identified by collection_document_id. + """ + manifest_request_url = '{}/manifest/{}'.format(self.api_url, collection_document_id) + if manifest_format: + manifest_request_url = '{}?format={}'.format(manifest_request_url, manifest_format) + response = requests.get(manifest_request_url) + self.handle_response_code(response.status_code, response.text) + return Document(response.content) diff --git a/despatchbay/exceptions.py b/despatchbay/exceptions.py new file mode 100644 index 0000000..ac227b5 --- /dev/null +++ b/despatchbay/exceptions.py @@ -0,0 +1,29 @@ +"""Despatchbay SDK exceptions""" + + +class Error(Exception): + """Base class for other exceptions""" + + +class InvalidArgumentException(Error): + """Exception to raise when invalid arguments are passed.""" + + +class AuthorizationException(Error): + """Exception to raise when a 401 error is returned.""" + + +class PaymentException(Error): + """Exception to raise when an operation fails due to insufficient funds.""" + + +class ApiException(Error): + """General API exception""" + + +class ConnectionException(Error): + """General connection error exception.""" + + +class RateLimitException(Error): + """Exception to raise when an operation fails due rate limits.""" diff --git a/examples/accounts.py b/examples/accounts.py new file mode 100644 index 0000000..8ed79bb --- /dev/null +++ b/examples/accounts.py @@ -0,0 +1,25 @@ +from despatchbay.despatchbay_sdk import DespatchBaySDK + +client = DespatchBaySDK(api_user='', api_key='') + +account_return = client.get_account() +print(account_return) + +services_return = client.get_services() +print(services_return) + +account_balance = client.get_account_balance() +print(account_balance) + +sender_addresses = client.get_sender_addresses() +print(sender_addresses) + +payment_methods = client.get_payment_methods() +print(payment_methods) + +automatic_topup_enabled = client.enable_automatic_topups( + '100', payment_methods[0].payment_method_id, payment_methods[0].payment_method_id) +print(automatic_topup_enabled) + +automatic_topup_disabled = client.disable_automatic_topups() +print(automatic_topup_disabled) diff --git a/examples/addresses.py b/examples/addresses.py new file mode 100644 index 0000000..05b1a30 --- /dev/null +++ b/examples/addresses.py @@ -0,0 +1,12 @@ +from despatchbay.despatchbay_sdk import DespatchBaySDK + +client = DespatchBaySDK(api_user='', api_key='') + +address_1 = client.find_address('DN227AY', '1') +print(address_1) + +address_2 = client.get_address_by_key('DN227AY0001') +print(address_2) + +address_3 = client.get_address_keys_by_postcode('DN22 7AY') +print(address_3) diff --git a/examples/shipping_and_documents.py b/examples/shipping_and_documents.py new file mode 100644 index 0000000..3bee718 --- /dev/null +++ b/examples/shipping_and_documents.py @@ -0,0 +1,63 @@ +from despatchbay.despatchbay_sdk import DespatchBaySDK + +client = DespatchBaySDK(api_user='', api_key='') + +my_parcel_1 = client.parcel( + weight=3, + length=14, + width=15, + height=92, + contents='Apples', + value=65 +) +my_parcel_2 = client.parcel( + weight=30, + length=100, + width=100, + height=100, + contents='Oranges', + value=1 +) +recipient_address = client.address( + company_name="Acme", + country_code="GB", + county="Theshire", + locality="Placeton", + postal_code="ps76de", + town_city="Cityville", + street="123 Fake Street" +) + +recipient = client.recipient( + name="Bonnie Bobbins", + telephone="01632987654", + email="bonnie@example.com", + recipient_address=recipient_address + +) +# Sender using address_id +sender = client.sender( + name="Joe Bloggs", + telephone="01632123456", + email="acme@example.com", + address_id='123456' +) + +shipment_request = client.shipment_request( + parcels=[my_parcel_1, my_parcel_2], + client_reference='puchacz', + collection_date='2019-04-01', + sender_address=sender, + recipient_address=recipient, + follow_shipment='true' +) + +services = client.get_available_services(shipment_request) +shipment_request.service_id = services[0].service_id +dates = client.get_available_collection_dates(sender, services[0].courier.courier_id) +shipment_request.collection_date = dates[0] +added_shipment = client.add_shipment(shipment_request) +client.book_shipments([added_shipment]) +shipment_return = client.get_shipment(added_shipment) +label_pdf = client.get_labels(shipment_return.shipment_document_id) +label_pdf.download('./new_label.pdf') diff --git a/examples/tracking.py b/examples/tracking.py new file mode 100644 index 0000000..6f094d5 --- /dev/null +++ b/examples/tracking.py @@ -0,0 +1,6 @@ +from despatchbay.despatchbay_sdk import DespatchBaySDK + +client = DespatchBaySDK(api_user='', api_key='