diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | gsxws/content.py | 2 | ||||
-rw-r--r-- | gsxws/core.py | 19 | ||||
-rw-r--r-- | gsxws/lookups.py | 6 | ||||
-rw-r--r-- | gsxws/objectify.py | 133 | ||||
-rw-r--r-- | gsxws/products.py | 27 | ||||
-rw-r--r-- | gsxws/xmltodict.py | 191 | ||||
-rw-r--r-- | requirements.pip | 1 | ||||
-rw-r--r-- | setup.py | 4 |
9 files changed, 168 insertions, 216 deletions
@@ -26,6 +26,7 @@ Requirements ============ - Python 2.7 or later +- lxml LICENSE diff --git a/gsxws/content.py b/gsxws/content.py index b73ca1f..008b46a 100644 --- a/gsxws/content.py +++ b/gsxws/content.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + class Content(GsxObject): def fetch_image(self, url): """ diff --git a/gsxws/core.py b/gsxws/core.py index 6f104b1..b56fa7b 100644 --- a/gsxws/core.py +++ b/gsxws/core.py @@ -33,7 +33,7 @@ import hashlib import logging import httplib import tempfile -import xmltodict +import objectify from urlparse import urlparse import xml.etree.ElementTree as ET @@ -160,7 +160,7 @@ class GsxCache(object): self.key = key self.expires = expires self.now = datetime.now() - filename = os.path.join(self.tmpdir, "gsxws_%s.db" % key) + filename = os.path.join(self.tmpdir, "gsxws_%s" % key) self.shelf = shelve.open(filename, protocol=-1) if not self.shelf.get(key): @@ -238,7 +238,7 @@ class GsxRequest(object): return ws.getresponse() def _submit(self, method, response=None, raw=False): - "Constructs the final SOAP message" + "Constructs and submits the final SOAP message" global GSX_SESSION root = ET.SubElement(self.body, self.obj._namespace + method) @@ -266,10 +266,8 @@ class GsxRequest(object): logging.debug("Response: %s %s %s" % (res.status, res.reason, xml)) response = response or self._response - root = ET.fromstring(xml).find("*//%s" % response) - data = xmltodict.ConvertXmlToDict(root) - self.objects = data[response] - + #root = ET.fromstring(xml).find("*//%s" % response) + self.objects = objectify.parse(xml, response) return self.objects def __str__(self): @@ -393,13 +391,14 @@ class GsxSession(GsxObject): def login(self): global GSX_SESSION + session = self._cache.get("session") - if not self._cache.get("session") is None: - GSX_SESSION = self._cache.get("session") + if not session is None: + GSX_SESSION = session else: self._req = GsxRequest(AuthenticateRequest=self) result = self._req._submit("Authenticate") - self._session_id = result.userSessionId + self._session_id = str(result.userSessionId) GSX_SESSION = self.get_session() self._cache.set("session", GSX_SESSION) diff --git a/gsxws/lookups.py b/gsxws/lookups.py index 3d9814d..7513151 100644 --- a/gsxws/lookups.py +++ b/gsxws/lookups.py @@ -14,7 +14,8 @@ class Lookup(GsxObject): self._namespace = "asp:" def lookup(self, method, response="lookupResponseData"): - return self._submit("lookupRequestData", method, response) + result = self._submit("lookupRequestData", method, response) + return [result] if isinstance(result, dict) else result def parts(self): """ @@ -36,8 +37,7 @@ class Lookup(GsxObject): >>> Lookup(serialNumber='DGKFL06JDHJP').repairs() # doctest: +ELLIPSIS [{'customerName': 'Lepalaan,Filipp',... """ - result = self.lookup("RepairLookup") - return [result] if isinstance(result, dict) else result + return self.lookup("RepairLookup") def invoices(self): """ diff --git a/gsxws/objectify.py b/gsxws/objectify.py new file mode 100644 index 0000000..d001a15 --- /dev/null +++ b/gsxws/objectify.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +import os +import re +import base64 +import tempfile +from lxml import objectify +from lxml.objectify import StringElement + +from datetime import datetime + +BASE64_TYPES = ('packingList', 'proformaFileData', 'returnLabelFileData',) +FLOAT_TYPES = ('totalFromOrder', 'exchangePrice', 'stockPrice', 'netPrice',) +BOOLEAN_TYPES = ('isSerialized', 'popMandatory', 'limitedWarranty', 'partCovered',) + +TZMAP = { + 'GMT': '', # Greenwich Mean Time + 'PDT': '-0700', # Pacific Daylight Time + 'PST': '-0800', # Pacific Standard Time + 'CDT': '-0700', # Central Daylight Time + 'CST': '-0600', # Central Standard Time + 'EST': '-0500', # Eastern Standard Time + 'EDT': '-0400', # Eastern Daylight Time + 'CET': '+0100', # Central European Time + 'CEST': '+0200', # Central European Summer Time + 'IST': '+0530', # Indian Standard Time + 'CCT': '+0800', # Chinese Coast Time + 'JST': '+0900', # Japan Standard Time + 'ACST': '+0930', # Austrailian Central Standard Time + 'AEST': '+1000', # Australian Eastern Standard Time + 'ACDT': '+1030', # Australian Central Daylight Time + 'AEDT': '+1100', # Australian Eastern Daylight Time + 'NZST': '+1200', # New Zealand Standard Time +} + + +class GsxElement(StringElement): + def __str__(self): + return str(self.pyval) + + +class GsxDateElement(GsxElement): + @property + def pyval(self): + # looks like some sort of date, let's try to convert + try: + # standard GSX format: "mm/dd/yy" + return datetime.strptime(self.text, "%m/%d/%y").date() + except ValueError: + pass + + try: + # some dates are formatted as "yyyy-mm-dd" + return datetime.strptime(self.text, "%Y-%m-%d").date() + except (ValueError, TypeError): + pass + + +class GsxBooleanElement(GsxElement): + @property + def pyval(self): + return self.text == 'Y' + + +class GsxPriceElement(GsxElement): + @property + def pyval(self): + return float(re.sub(r'[A-Z ,]', '', self.text)) + + +class GsxAttachment(GsxElement): + @property + def pyval(self): + v = base64.b64decode(self.text) + of = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) + of.write(v) + return of.name + + +class GsxDatetimeElement(GsxElement): + @property + def pyval(self): + #2011-01-27 11:45:01 PST + # Unfortunately we have to chomp off the TZ info... + m = re.search(r'^(\d+\-\d+\-\d+ \d+:\d+:\d+) (\w+)$', self.text) + ts, tz = m.groups() + return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S") + + +class GsxTimestampElement(GsxElement): + @property + def pyval(self): + return datetime.strptime(self.text, "%d-%b-%y %H:%M:%S") + + +class GsxClassLookup(objectify.ObjectifyElementClassLookup): + def lookup(self, node_type, document, namespace, name): + if name == 'dispatchSentDate': + return GsxDatetimeElement + if name == 'acPlusFlag': + return GsxBooleanElement + if name in BOOLEAN_TYPES: + return GsxBooleanElement + if name in BASE64_TYPES: + return GsxAttachment + if name in FLOAT_TYPES: + return GsxPriceElement + if re.search(r'Date$', name): + return GsxDateElement + + return objectify.ObjectifiedElement + + +def parse(root, response): + """ + >>> parse('/tmp/authenticate.xml', 'AuthenticateResponse').userSessionId + Sdt7tXp2XytTEVwHBeDx6lHTXI3w9s+M + """ + parser = objectify.makeparser(remove_blank_text=True) + parser.set_element_class_lookup(GsxClassLookup()) + + if isinstance(root, basestring) and os.path.exists(root): + root = objectify.parse(root, parser) + else: + root = objectify.fromstring(root, parser) + + return root.find('*//%s' % response) + +if __name__ == '__main__': + import doctest + import logging + logging.basicConfig(level=logging.DEBUG) + doctest.testmod() diff --git a/gsxws/products.py b/gsxws/products.py index abffd43..8905b2b 100644 --- a/gsxws/products.py +++ b/gsxws/products.py @@ -30,7 +30,7 @@ class Product(GsxObject): Returns the model description of this Product >>> Product('DGKFL06JDHJP').model().configDescription - u'iMac (27-inch, Mid 2011)' + 'iMac (27-inch, Mid 2011)' """ result = self._submit("productModelRequest", "FetchProductModel") @@ -48,11 +48,15 @@ class Product(GsxObject): warranty status request, the unit level warranty information is returned. >>> Product('DGKFL06JDHJP').warranty().warrantyStatus - u'Out Of Warranty (No Coverage)' - >>> Product('DGKFL06JDHJP').warranty().estimatedPurchaseDate - datetime.date(2011, 6, 2) - >>> Product('WQ8094DW0P1').warranty([(u'661-5070', u'Z26',)]) # doctest: +ELLIPSIS - {'warrantyStatus': 'Out Of Warranty (No Coverage)',... + 'Out Of Warranty (No Coverage)' + >>> Product('DGKFL06JDHJP').warranty().estimatedPurchaseDate.pyval + '06/02/11' + >>> Product('WQ8094DW0P1').warranty().blaa # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: no such child: blaa + >>> Product('WQ8094DW0P1').warranty([(u'661-5070', u'Z26',)]).warrantyStatus + 'Out Of Warranty (No Coverage)' """ if hasattr(self, "alternateDeviceId"): if not hasattr(self, "serialNumber"): @@ -75,9 +79,9 @@ class Product(GsxObject): def parts(self): """ >>> Product('DGKFL06JDHJP').parts() # doctest: +ELLIPSIS - {'exchangePrice': '0', 'isSerialized': 'N', 'partType': 'Other',... + <Element parts at... >>> Product(productName='MacBook Pro (17-inch, Mid 2009)').parts() # doctest: +ELLIPSIS - {'exchangePrice': '0', 'isSerialized': 'N', 'partType': 'Other',... + <Element parts at... """ try: return Lookup(serialNumber=self.serialNumber).parts() @@ -87,7 +91,7 @@ class Product(GsxObject): def repairs(self): """ >>> Product(serialNumber='DGKFL06JDHJP').repairs() # doctest: +ELLIPSIS - [{'customerName': 'Lepalaan,Filipp',... + <Element lookupResponseData at... """ return Lookup(serialNumber=self.serialNumber).repairs() @@ -100,7 +104,10 @@ class Product(GsxObject): def fetch_image(self): """ - >>> Product('DGKFL06JDHJP').fetch_image() + >>> Product('DGKFL06JDHJP').fetch_image() # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + GsxError: No URL to fetch product image """ if not hasattr(self, "imageURL"): raise GsxError("No URL to fetch product image") diff --git a/gsxws/xmltodict.py b/gsxws/xmltodict.py deleted file mode 100644 index f041eb5..0000000 --- a/gsxws/xmltodict.py +++ /dev/null @@ -1,191 +0,0 @@ -## {{{ http://code.activestate.com/recipes/573463/ (r7) - -import re -import base64 -import tempfile -from datetime import datetime -from xml.etree import ElementTree - - -class XmlDictObject(dict): - """ - Adds object like functionality to the standard dictionary. - """ - def __init__(self, initdict=None): - if initdict is None: - initdict = {} - dict.__init__(self, initdict) - - def __getattr__(self, item): - - try: - v = self.__getitem__(item) - except KeyError: - raise AttributeError("Invalid attribute: %s" % item) - - if item in ["packingList", "proformaFileData", "returnLabelFileData"]: - v = base64.b64decode(v) - of = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) - of.write(v) - return of.name - - try: - if isinstance(v, basestring): - v = unicode(v) # "must be unicode, not str" - - # convert Y and N to boolean - if re.search(r'^[YN]$', v): - v = (v == "Y") - - # convert true/false to boolean - if re.search(r'^(true)|(false)$', v): - v = (v == "true") - - # strip currency prefix and munge into float - if re.search(r'Price$', item): - v = float(re.sub(r'[A-Z ,]', '', v)) - - # Convert timestamps to native Python type - # 18-Jan-13 14:38:04 - if re.search(r'TimeStamp$', item): - v = datetime.strptime(v, "%d-%b-%y %H:%M:%S") - - if re.search(r'Date$', item): - # looks like some sort of date, let's try to convert - try: - # standard GSX format: "mm/dd/yy" - dt = datetime.strptime(v, "%m/%d/%y") - v = dt.date() - except ValueError: - pass - - try: - # some dates are formatted as "yyyy-mm-dd" - dt = datetime.strptime(v, "%Y-%m-%d") - v = dt.date() - except (ValueError, TypeError): - pass - except TypeError: - pass - - return v - - def __setattr__(self, item, value): - self.__setitem__(item, value) - - def __str__(self): - if self.has_key('_text'): - return self.__getitem__('_text') - else: - return '' - - @staticmethod - def Wrap(x): - """ - Static method to wrap a dictionary recursively as an XmlDictObject - """ - - if isinstance(x, dict): - return XmlDictObject((k, XmlDictObject.Wrap(v)) for (k, v) in x.iteritems()) - elif isinstance(x, list): - return [XmlDictObject.Wrap(v) for v in x] - else: - return x - - @staticmethod - def _UnWrap(x): - if isinstance(x, dict): - return dict((k, XmlDictObject._UnWrap(v)) for (k, v) in x.iteritems()) - elif isinstance(x, list): - return [XmlDictObject._UnWrap(v) for v in x] - else: - return x - - def UnWrap(self): - """ - Recursively converts an XmlDictObject to a standard dictionary and returns the result. - """ - - return XmlDictObject._UnWrap(self) - - -def _ConvertDictToXmlRecurse(parent, dictitem): - assert type(dictitem) is not type([]) - - if isinstance(dictitem, dict): - for (tag, child) in dictitem.iteritems(): - if str(tag) == '_text': - parent.text = str(child) - elif type(child) is type([]): - # iterate through the array and convert - for listchild in child: - elem = ElementTree.Element(tag) - parent.append(elem) - _ConvertDictToXmlRecurse(elem, listchild) - else: - elem = ElementTree.Element(tag) - parent.append(elem) - _ConvertDictToXmlRecurse(elem, child) - else: - parent.text = str(dictitem) - - -def ConvertDictToXml(xmldict): - """ - Converts a dictionary to an XML ElementTree Element - """ - roottag = xmldict.keys()[0] - root = ElementTree.Element(roottag) - _ConvertDictToXmlRecurse(root, xmldict[roottag]) - return root - - -def _ConvertXmlToDictRecurse(node, dictclass): - nodedict = dictclass() - - if len(node.items()) > 0: - # if we have attributes, set them - nodedict.update(dict(node.items())) - - for child in node: - # recursively add the element's children - newitem = _ConvertXmlToDictRecurse(child, dictclass) - if child.tag in nodedict.keys(): - # found duplicate tag, force a list - if type(nodedict[child.tag]) is type([]): - # append to existing list - nodedict[child.tag].append(newitem) - else: - # convert to list - nodedict[child.tag] = [nodedict[child.tag], newitem] - else: - # only one, directly set the dictionary - nodedict[child.tag] = newitem - - if node.text is None: - text = '' - else: - text = node.text.strip() - - if len(nodedict) > 0: - # if we have a dictionary add the text as a dictionary value (if there is any) - if len(text) > 0: - nodedict['_text'] = text - else: - # if we don't have child nodes or attributes, just set the text - nodedict = text - - return nodedict - - -def ConvertXmlToDict(root, dictclass=XmlDictObject): - """ - Converts an XML file or ElementTree Element to a dictionary - """ - # If a string is passed in, try to open it as a file - if type(root) == type(''): - root = ElementTree.parse(root).getroot() - elif not isinstance(root, ElementTree.Element): - raise TypeError("Expected ElementTree.Element or file path string") - - return dictclass({root.tag: _ConvertXmlToDictRecurse(root, dictclass)}) diff --git a/requirements.pip b/requirements.pip index 8786e3f..6c0a1cb 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1 +1,2 @@ +lxml yaml @@ -3,8 +3,8 @@ from setuptools import setup, find_packages setup( name="gsxws", version="0.4", - description="Apple GSX integration.", - install_requires=['PyYAML'], + description="Library for communicating with GSX Web Services API.", + install_requires=['PyYAML', 'lxml'], classifiers=[ "Environment :: Web Environment", "Intended Audience :: Developers", |