aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--gsxws/content.py2
-rw-r--r--gsxws/core.py19
-rw-r--r--gsxws/lookups.py6
-rw-r--r--gsxws/objectify.py133
-rw-r--r--gsxws/products.py27
-rw-r--r--gsxws/xmltodict.py191
-rw-r--r--requirements.pip1
-rw-r--r--setup.py4
9 files changed, 168 insertions, 216 deletions
diff --git a/README.md b/README.md
index b18ebca..cd29b41 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/setup.py b/setup.py
index 1ca66e1..83ed1d9 100644
--- a/setup.py
+++ b/setup.py
@@ -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",