Adding initial classes: connection, utils and kunaki
6 files changed, 259 insertions(+), 0 deletions(-)

A => LICENSE
A => README.rst
A => kunaki/__init__.py
A => kunaki/connection.py
A => kunaki/kunaki.py
A => kunaki/utils.py
A => LICENSE +0 -0

A => README.rst +0 -0

A => kunaki/__init__.py +3 -0
@@ 0,0 1,3 @@ 
+from .kunaki import ShippingProduct, ShippingOptions, Order, OrderStatus
+
+__all__ = ['ShippingProduct', 'ShippingOptions', 'Order', 'OrderStatus']

          
A => kunaki/connection.py +41 -0
@@ 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

          
A => kunaki/kunaki.py +148 -0
@@ 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
+        (<Description>, <DeliveryTime>, <Price>)
+        """
+        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

          
A => kunaki/utils.py +67 -0
@@ 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<HTML>\r\n<BODY>\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)