diff options
-rw-r--r-- | gsxws/objectify.py | 129 | ||||
-rw-r--r-- | tests/fixtures/ios_activation.xml | 41 | ||||
-rw-r--r-- | tests/fixtures/onsite_dispatch_detail.xml | 76 | ||||
-rw-r--r-- | tests/fixtures/parts_lookup.xml | 47 | ||||
-rw-r--r-- | tests/test_gsxws.py | 67 |
5 files changed, 287 insertions, 73 deletions
diff --git a/gsxws/objectify.py b/gsxws/objectify.py index 3a48858..800e7e9 100644 --- a/gsxws/objectify.py +++ b/gsxws/objectify.py @@ -5,15 +5,12 @@ import re import base64 import tempfile -from lxml import objectify, etree -from lxml.objectify import StringElement - +from lxml import objectify from datetime import datetime +DATETIME_TYPES = ('dispatchSentDate',) BASE64_TYPES = ('packingList', 'proformaFileData', 'returnLabelFileData',) FLOAT_TYPES = ('totalFromOrder', 'exchangePrice', 'stockPrice', 'netPrice',) -BOOLEAN_TYPES = ('isSerialized', 'popMandatory', 'limitedWarranty', 'partCovered', 'acPlusFlag',) -DATETIME_TYPES = ('dispatchSentDate',) TZMAP = { 'GMT': '', # Greenwich Mean Time @@ -36,92 +33,90 @@ TZMAP = { } -class GsxElement(StringElement): - def __unicode__(self): - return self.pyval +def gsx_date(value): + try: + # standard GSX format: "mm/dd/yy" + return datetime.strptime(value, "%m/%d/%y").date() + except ValueError: + pass - def __str__(self): - return unicode(self).encode('utf-8') + try: + # some dates are formatted as "yyyy-mm-dd" + return datetime.strptime(value, "%Y-%m-%d").date() + except (ValueError, TypeError): + pass - def __repr__(self): - return str(self.text) +def gsx_boolean(value): + return value == 'Y' -class GsxDateElement(GsxElement): + +class GsxPriceElement(): @property def pyval(self): - try: - # standard GSX format: "mm/dd/yy" - return datetime.strptime(self.text, "%m/%d/%y").date() - except ValueError: - pass + return float(re.sub(r'[A-Z ,]', '', self.text)) - try: - # some dates are formatted as "yyyy-mm-dd" - return datetime.strptime(self.text, "%Y-%m-%d").date() - except (ValueError, TypeError): - pass - def __repr__(self): - return str(datetime.strftime(self.pyval, '%Y-%m-%d')) +def gsx_attachment(value): + v = base64.b64decode(value) + of = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) + of.write(v) + return of.name -class GsxBooleanElement(GsxElement): - @property - def pyval(self): - return self.text == 'Y' +def gsx_datetime(value): + # 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+)$', value) + ts, tz = m.groups() + return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S") -class GsxPriceElement(GsxElement): - @property - def pyval(self): - return float(re.sub(r'[A-Z ,]', '', self.text)) - +def gsx_timestamp(value): + return datetime.strptime(value, "%d-%b-%y %H:%M:%S") -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 GsxElement(objectify.ObjectifiedElement): + def __getattribute__(self, 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") + result = super(GsxElement, self).__getattribute__(name) + if isinstance(result, objectify.NumberElement): + return result.pyval -class GsxTimestampElement(GsxElement): - @property - def pyval(self): - return datetime.strptime(self.text, "%d-%b-%y %H:%M:%S") + if isinstance(result, objectify.StringElement): + name = result.tag + if not result.text: + return -class GsxClassLookup(etree.CustomElementClassLookup): - def lookup(self, node_type, document, namespace, name): - if name in DATETIME_TYPES: - return GsxDatetimeElement - 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 + if name in DATETIME_TYPES: + return gsx_datetime(result.text) + if name in BASE64_TYPES: + return gsx_attachment(result.text) + if name in FLOAT_TYPES: + return GsxPriceElement + if re.search(r'Date$', name): + return gsx_date(result.text) + if re.search(r'^[YN]$', result.text): + return gsx_boolean(result.text) - return objectify.ObjectifiedElement + return result def parse(root, response): + """ + >>> parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo').warrantyStatus + 'Apple Limited Warranty' + >>> parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo').estimatedPurchaseDate + datetime.date(2010, 8, 25) + >>> parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo').limitedWarranty + True + >>> parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo').isPersonalized + """ parser = objectify.makeparser(remove_blank_text=True) - parser.set_element_class_lookup(GsxClassLookup()) + lookup = objectify.ObjectifyElementClassLookup(tree_class=GsxElement) + parser.set_element_class_lookup(lookup) if isinstance(root, basestring) and os.path.exists(root): root = objectify.parse(root, parser) diff --git a/tests/fixtures/ios_activation.xml b/tests/fixtures/ios_activation.xml new file mode 100644 index 0000000..7b18d6b --- /dev/null +++ b/tests/fixtures/ios_activation.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> + <S:Body> + <ns3:FetchIOSActivationDetailsResponse + xmlns:ns2="http://asp.core.endpoint.ws.gsx.ist.apple.com/" + xmlns:ns3="http://gsxws.apple.com/elements/global" + xmlns:ns4="http://gsxws.apple.com/elements/core/asp" + xmlns:ns5="http://gsxws.apple.com/elements/core/asp/am" + xmlns:ns6="http://gsxws.apple.com/elements/core"> + <FetchIOSActivationDetailsResponse> + <operationId>1e62913276430159203556d</operationId> + <activationDetailsInfo> + <serialNumber>2J141331A4S</serialNumber> + <imeiNumber>010648001526755</imeiNumber> + <meid></meid> + <iccID>897010266884541917</iccID> + <firstUnbrickDate>01/16/12</firstUnbrickDate> + <lastUnbrickDate>01/18/12</lastUnbrickDate> + <lastRestoreDate></lastRestoreDate> + <unbricked>true</unbricked> + <unlocked>true</unlocked> + <unlockDate>01/18/12</unlockDate> + <productVersion>4.2.1</productVersion> + <initialActivationPolicyID>1</initialActivationPolicyID> + <initialActivationPolicyDetails> + Unassigned factory default - Apple carriers only. + SIM Swap triggers rebricking.</initialActivationPolicyDetails> + <appliedActivationPolicyID>152</appliedActivationPolicyID> + <appliedActivationDetails>Russia MegaFon.</appliedActivationDetails> + <nextTetherPolicyID>1</nextTetherPolicyID> + <nextTetherPolicyDetails> + Unassigned factory default - Apple carriers only. + SIM Swap triggers rebricking.</nextTetherPolicyDetails> + <macAddress></macAddress> + <bluetoothMacAddress></bluetoothMacAddress> + <partDescription>iPhone4 16GB BLACKE</partDescription> + </activationDetailsInfo> + </FetchIOSActivationDetailsResponse> + </ns3:FetchIOSActivationDetailsResponse> + </S:Body> +</S:Envelope> diff --git a/tests/fixtures/onsite_dispatch_detail.xml b/tests/fixtures/onsite_dispatch_detail.xml new file mode 100644 index 0000000..4025dd0 --- /dev/null +++ b/tests/fixtures/onsite_dispatch_detail.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> + <S:Body> + <ns4:OnsiteDispatchDetailResponse xmlns:ns2="http://asp.core.endpoint.ws.gsx.ist.apple.com/" xmlns:ns3="http://gsxws.apple.com/elements/global" xmlns:ns4="http://gsxws.apple.com/elements/core/asp" xmlns:ns5="http://gsxws.apple.com/elements/core/asp/am" xmlns:ns6="http://gsxws.apple.com/elements/core"> + <OnsiteDispatchDetailResponse> + <operationId>8a6231310158348248</operationId> + <onsiteDispatchDetails> + <dispatchId>G101260028</dispatchId> + <serialNumber>YM1432NELFB</serialNumber> + <dispatchSentDate/> + <serviceAccountId>0000511590</serviceAccountId> + <assignedToShipTo>0000022296</assignedToShipTo> + <coverageStatusDescription>AppleCare Protection Plan</coverageStatusDescription> + <productName>iMac (Summer 2001)</productName> + <configuration>iMac 500MHz/128M/20G/CD-RW/RUltra/56K/FW</configuration> + <purchaseOrderNumber>503017</purchaseOrderNumber> + <notes>Customer Reported Symptom: + System crashes after logging in. + Sold-To Diagnosis: + System crashes after logging in. + Notes:</notes> + <zone>TR01</zone> + <primaryAddress> + <addressLine1>2323 E. Mountain Gate Pass2323 E. Mountain Gate Pass</addressLine1> + <country>US</country> + <zipCode>85024</zipCode> + <county>Madera</county> + <city>PHOENIX</city> + <state>AZ</state> + <region>005</region> + <firstName>Christopher</firstName> + <lastName>Howard</lastName> + <companyName>PVUSD</companyName> + <primaryPhone>480-650-3876</primaryPhone> + </primaryAddress> + <secondaryAddress> + <addressLine1>2323 E. Mountain Gate Pass2323 E. Mountain Gate Pass</addressLine1> + <country>US</country> + <zipCode>85024</zipCode> + <county>Madera</county> + <city>PHOENIX</city> + <state>AZ</state> + <region>005</region> + <firstName>Hogan</firstName> + <lastName>Patrick</lastName> + <companyName>PVUSD</companyName> + <primaryPhone>602-867-5252</primaryPhone> + </secondaryAddress> + <assignedToLocationAddress> + <addressLine1>8545 N BLACK CANYON HWY</addressLine1> + <country>United States</country> + <zipCode>85021</zipCode> + <county>Madera</county> + <city>PHOENIX</city> + <state>AZ</state> + <region>005</region> + <firstName>CDS GROUP INC</firstName> + <lastName></lastName> + <primaryPhone>602-242-5101</primaryPhone> + </assignedToLocationAddress> + <dispatchOrderLines> + <partNumber>661-2447</partNumber> + <partDescription>Board, Logic, 500 MHz</partDescription> + <partCoverageDescription>AppleCare Protection Plan</partCoverageDescription> + <comptiaCode>581</comptiaCode> + <comptiaModifier>B</comptiaModifier> + <returnStatus>Known Bad Board</returnStatus> + <isSerialized>Y</isSerialized> + <notaFiscalNumber>123456789</notaFiscalNumber> + </dispatchOrderLines> + </onsiteDispatchDetails> + <communicationMessage/> + </OnsiteDispatchDetailResponse> + </ns4:OnsiteDispatchDetailResponse> + </S:Body> +</S:Envelope> diff --git a/tests/fixtures/parts_lookup.xml b/tests/fixtures/parts_lookup.xml new file mode 100644 index 0000000..c1ffcb6 --- /dev/null +++ b/tests/fixtures/parts_lookup.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> + <S:Body> + <ns3:PartsLookupResponse xmlns:ns3="http://gsxws.apple.com/elements/core"> + <PartsLookupResponse> + <operationId>AnK6lhP8j4Zw8hHYswYMGCthSKGnzy9O</operationId> + <parts> + <eeeCode>DC18,DC19,DC20,YLW,YVL</eeeCode> + <exchangePrice>14.4</exchangePrice> + <isSerialized>Y</isSerialized> + <laborTier></laborTier> + <partDescription>SVC,REMOTE</partDescription> + <partNumber>661-4448</partNumber> + <partType>Module</partType> + <stockPrice>17.1</stockPrice> + <stockPrice>17.1</stockPrice> + <componentCode>5</componentCode> + <originalPartNumber>661-2549</originalPartNumber> + </parts> + <parts> + <eeeCode>59T</eeeCode> + <exchangePrice>19</exchangePrice> + <isSerialized>Y</isSerialized> + <laborTier></laborTier> + <partDescription>Power Adapter w/Plug, Ultra-Compact, USB, iPhone/iPod-US/CAN/JPN/TWN</partDescription> + <partNumber>661-4954</partNumber> + <partType>Module</partType> + <stockPrice>26.1</stockPrice> + <componentCode>3</componentCode> + <originalPartNumber></originalPartNumber> + </parts> + <parts> + <eeeCode></eeeCode> + <exchangePrice>19</exchangePrice> + <isSerialized>N</isSerialized> + <laborTier></laborTier> + <partDescription>SVC,STEREO HEADSET</partDescription> + <partNumber>661-5028</partNumber> + <partType>Module</partType> + <stockPrice>26.1</stockPrice> + <componentCode>6</componentCode> + <originalPartNumber>922-8629</originalPartNumber> + </parts> + </PartsLookupResponse> + </ns3:PartsLookupResponse> + </S:Body> +</S:Envelope> diff --git a/tests/test_gsxws.py b/tests/test_gsxws.py index d7feac6..089d916 100644 --- a/tests/test_gsxws.py +++ b/tests/test_gsxws.py @@ -1,21 +1,76 @@ # -*- coding: utf-8 -*- -import unittest from datetime import date +from unittest import main, TestCase from gsxws.objectify import parse -class TestWarrantyFunctions(unittest.TestCase): +class TestWarrantyFunctions(TestCase): def setUp(self): - self.wty = parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo') + self.data = parse('tests/fixtures/warranty_status.xml', 'warrantyDetailInfo') def test_purchase_date(self): - self.assertIsInstance(self.wty.estimatedPurchaseDate.pyval, date) + self.assertIsInstance(self.data.estimatedPurchaseDate, date) def test_config_description(self): - self.assertEqual(self.wty.configDescription, 'IPHONE 4,16GB BLACK') + self.assertEqual(self.data.configDescription, 'IPHONE 4,16GB BLACK') + def test_limited_warranty(self): + self.assertTrue(self.data.limitedWarranty) + + def test_parts_covered(self): + self.assertIsInstance(self.data.partCovered, bool) + self.assertTrue(self.data.partCovered) + + +class TestActivation(TestCase): + def setUp(self): + self.data = parse('tests/fixtures/ios_activation.xml', 'activationDetailsInfo') + + def test_unlock_date(self): + self.assertIsInstance(self.data.unlockDate, date) + + def test_unlocked(self): + self.assertIs(type(self.data.unlocked), bool) + self.assertTrue(self.data.unlocked) + + +class TestPartsLookup(TestCase): + def setUp(self): + self.data = parse('tests/fixtures/parts_lookup.xml', 'PartsLookupResponse') + self.part = self.data.parts[0] + + def test_parts(self): + self.assertEqual(len(self.data.parts), 3) + + def test_exchange_price(self): + self.assertEqual(self.part.exchangePrice, 14.4) + + def test_stock_price(self): + self.assertEqual(self.part.stockPrice, 17.1) + + def test_serialized(self): + self.assertIsInstance(self.part.isSerialized, bool) + self.assertTrue(self.part.isSerialized) + + def test_description(self): + self.assertEqual(self.part.partDescription, 'SVC,REMOTE') + + +class TestOnsiteDispatchDetail(TestCase): + def setUp(self): + self.data = parse('tests/fixtures/onsite_dispatch_detail.xml', 'onsiteDispatchDetails') + + def test_details(self): + self.assertEqual(self.data.dispatchId, 'G101260028') + + def test_address(self): + self.assertEqual(self.data.primaryAddress.zipCode, 85024) + self.assertEqual(self.data.primaryAddress.firstName, 'Christopher') + + def test_orderlines(self): + self.assertIsInstance(self.data.dispatchOrderLines.isSerialized, bool) if __name__ == '__main__': - unittest.main() + main() |