# HG changeset patch # User Gustavo Andres Morero # Date 1379452472 10800 # Tue Sep 17 18:14:32 2013 -0300 # Node ID 4b2d11ed5ff22157ebb871b0e63f1c081ce8c34d # Parent 0000000000000000000000000000000000000000 Adding initial classes: connection, utils and kunaki diff --git a/LICENSE b/LICENSE new file mode 100644 diff --git a/README.rst b/README.rst new file mode 100644 diff --git a/kunaki/__init__.py b/kunaki/__init__.py new file mode 100644 --- /dev/null +++ b/kunaki/__init__.py @@ -0,0 +1,3 @@ +from .kunaki import ShippingProduct, ShippingOptions, Order, OrderStatus + +__all__ = ['ShippingProduct', 'ShippingOptions', 'Order', 'OrderStatus'] diff --git a/kunaki/connection.py b/kunaki/connection.py new file mode 100644 --- /dev/null +++ b/kunaki/connection.py @@ -0,0 +1,41 @@ +import urllib +import urlparse +import httplib + + +class Connection(object): + """ Basic class to send requests to Kunaki XML Web service. + """ + def __init__(self, url): + url_scheme = urlparse.urlparse(url) + self.scheme = url_scheme.scheme + self.host = url_scheme.netloc + self.port = url_scheme.port or (443 if self.scheme == 'https' else 80) + self.path = url_scheme.path + self.is_secure = self.scheme == 'https' + + def send_request(self, body, status=200, ctype='text/xml'): + """ Sends a POST request to the connection host. + Returns success status and response as a tuple. + """ + headers = { + 'Host': '%s' % self.host, + 'Content-Type': ctype, + } + if isinstance(body, dict): + body = urllib.urlencode(body) + + if self.is_secure: + conn = httplib.HTTPSConnection(self.host, int(self.port)) + else: + conn = httplib.HTTPConnection(self.host, int(self.port)) + + conn.request('POST', self.path, body, headers) + res = conn.getresponse() + data = res.read() + conn.close() + if res.status != status: + msg = '%s - %s' % (res.status, res.reason) + return False, msg + + return True, data diff --git a/kunaki/kunaki.py b/kunaki/kunaki.py new file mode 100644 --- /dev/null +++ b/kunaki/kunaki.py @@ -0,0 +1,148 @@ +import xml.etree.cElementTree as ET +from .utils import KunakiElement, KunakiRequest + + +class ShippingProduct(KunakiElement): + """ Kunaki Product. Used in Shipping Options and Order requests. + """ + def __init__(self, product_id, quantity): + self.product_id = product_id + self.quantity = quantity + + def get_tree(self): + """ Builds tree structure for the Product. + """ + product = ET.Element('Product') + ET.SubElement(product, 'ProductId').text = unicode(self.product_id) + ET.SubElement(product, 'Quantity').text = unicode(self.quantity) + return product + + +class ShippingOptions(KunakiRequest): + """ Kunaki Shipping Options request. + """ + def __init__(self, country, state, postal_code, products, *args, **kwargs): + super(ShippingOptions, self).__init__(*args, **kwargs) + self.country = country + self.state = state + self.postal_code = postal_code + self.products = products + + def get_tree(self): + """ Builds tree structure for the Shipping Options. + """ + options = ET.Element('ShippingOptions') + ET.SubElement(options, 'Country').text = unicode(self.country) + ET.SubElement(options, 'State_Province').text = unicode(self.state) + ET.SubElement(options, 'PostalCode').text = unicode(self.postal_code) + for product in self.products: + options.append(product.get_tree()) + return options + + def get_options(self): + """ Returns options retrieved from Kunaki as a list of tuples + (, , ) + """ + assert self.response is not None + options = [] + for opt in self.response.findall('Option'): + options.append(tuple(x.text for x in opt.getchildren())) + return options + + def add_product(self, product): + """ Adds a Kunaki Product to the Shipping Options `products` list. + Expects `product` to be a ShippingProduct instance. + """ + self.products.append(product) + + +class Order(KunakiRequest): + """ Kunaki Order request. + """ + def __init__(self, username, password, name, address1, city, postal_code, + country, shipping_description, products, state='', + address2='', company='', mode='Live', *args, **kwargs): + super(Order, self).__init__(*args, **kwargs) + self.username = username + self.password = password + self.name = name + self.address1 = address1 + self.city = city + self.postal_code = postal_code + self.country = country + self.shipping_description = shipping_description + self.products = products + self.state = state + self.address2 = address2 + self.company = company + self.mode = mode + + def get_tree(self): + """ Builds tree structure for the Order. + """ + order = ET.Element('Order') + ET.SubElement(order, 'UserId').text = unicode(self.username) + ET.SubElement(order, 'Password').text = unicode(self.password) + ET.SubElement(order, 'Mode').text = unicode(self.mode) + ET.SubElement(order, 'Name').text = unicode(self.name) + ET.SubElement(order, 'Company').text = unicode(self.company) + ET.SubElement(order, 'Address1').text = unicode(self.address1) + ET.SubElement(order, 'Address2').text = unicode(self.address2) + ET.SubElement(order, 'City').text = unicode(self.city) + ET.SubElement(order, 'State_Province').text = unicode(self.state) + ET.SubElement(order, 'PostalCode').text = unicode(self.postal_code) + ET.SubElement(order, 'Country').text = unicode(self.country) + sd_elem = ET.SubElement(order, 'ShippingDescription') + sd_elem.text = unicode(self.shipping_description) + for product in self.products: + order.append(product.get_tree()) + return order + + def add_product(self, product): + """ Adds a Kunaki Product to the Order `products` list. + Expects `product` to be a ShippingProduct instance. + """ + self.products.append(product) + + def get_order_id(self): + """ Returns the order id from the Kunaki Order response. + """ + assert self.response is not None + return self.response.find('OrderId').text + + +class OrderStatus(KunakiRequest): + """ Kunaki Order Status request. + """ + def __init__(self, username, password, order_id, *args, **kwargs): + super(OrderStatus, self).__init__(*args, **kwargs) + self.username = username + self.password = password + self.order_id = order_id + + def get_tree(self): + """ Builds tree structure for the Order Status. + """ + order_status = ET.Element('OrderStatus') + ET.SubElement(order_status, 'UserId').text = unicode(self.username) + ET.SubElement(order_status, 'Password').text = unicode(self.password) + ET.SubElement(order_status, 'OrderId').text = unicode(self.order_id) + return order_status + + def get_status(self): + """ Returns the status from the Order Status response. + """ + assert self.response is not None + return self.response.find('OrderStatus').text + + def get_tracking_type(self): + """ Returns the tracking type from the Order Status response. + """ + assert self.response is not None + return self.response.find('TrackingType').text + + def get_tracking_id(self): + """ Returns the tracking id from the Order Status response. + """ + assert self.response is not None + return self.response.find('TrackingId').text diff --git a/kunaki/utils.py b/kunaki/utils.py new file mode 100644 --- /dev/null +++ b/kunaki/utils.py @@ -0,0 +1,67 @@ +import xml.etree.cElementTree as ET +from .connection import Connection + +REQUEST_URL = 'https://Kunaki.com/XMLService.ASP' + + +class KunakiElement(object): + """ Base class for Kunaki request elements. + """ + def get_tree(self): + """ Used to build ElementTree structure for the request. + Returns root Element. + """ + raise NotImplementedError + + def get_xml(self): + """ Raw XML version of the request structure. + """ + return ET.tostring(self.get_tree()) + + +class KunakiRequest(KunakiElement): + """ Base class for Kunaki requests. + """ + def __init__(self): + self.conn = Connection(REQUEST_URL) + self.response = None + self.success = False + self.error_msg = '' + + def parse(self, data): + """ Used to parse raw response to XML. + Sets success and error_msg as appropriated. + """ + try: + self.response = ET.XML(data) + except SyntaxError: + # Needed for malformed XML response from Kunaki + data = data.replace('\r\n\r\n\r\n', '') + self.response = ET.XML(data) + self.success = self.is_success() + self.error_msg = self.get_error_msg() + + def send(self): + """ Sends XML request through the connection. + """ + self.parse(self.conn.send_request(self.get_xml())[1]) + + def is_success(self): + """ Determines if request was successful. + """ + assert self.response is not None + ec = self.response.find('ErrorCode') + return ec.text == '0' + + def get_error_msg(self): + """ Retrieves request error message. + """ + assert self.response is not None + et = self.response.find('ErrorText') + return et.text + + def get_response_xml(self): + """ Returns request response as raw XML. + """ + assert self.response is not None + return ET.tostring(self.response)