diff options
-rw-r--r-- | README.md | 3 | ||||
-rwxr-xr-x | gsxws.py | 1116 | ||||
-rw-r--r-- | gsxws/__init__.py | 0 | ||||
-rw-r--r-- | gsxws/comms.py | 13 | ||||
-rw-r--r-- | gsxws/comptia.json (renamed from comptia.json) | 0 | ||||
-rw-r--r-- | gsxws/comptia.py | 89 | ||||
-rw-r--r-- | gsxws/content.py | 12 | ||||
-rw-r--r-- | gsxws/core.py | 469 | ||||
-rw-r--r-- | gsxws/diagnostics.py | 29 | ||||
-rw-r--r-- | gsxws/escalations.py | 18 | ||||
-rw-r--r-- | gsxws/langs.json (renamed from langs.json) | 8 | ||||
-rw-r--r-- | gsxws/lookups.py | 64 | ||||
-rw-r--r-- | gsxws/orders.py | 20 | ||||
-rw-r--r-- | gsxws/parts.py | 25 | ||||
-rw-r--r-- | gsxws/products.py | 103 | ||||
-rw-r--r-- | gsxws/repairs.py | 257 | ||||
-rw-r--r-- | gsxws/returns.py (renamed from returns.py) | 0 | ||||
-rw-r--r-- | lookups.py | 7 | ||||
-rw-r--r-- | products.py | 65 | ||||
-rw-r--r-- | repairs.py | 115 |
20 files changed, 1107 insertions, 1306 deletions
@@ -2,7 +2,7 @@ py-gsxws ====== py-gsxws is a Python library designed to work with Apple's GSX Web Services API. -The goel is for it to support all the features of the API. +The goal is for it to support all the features of the API. Currently it supports most of them. Installation: @@ -26,7 +26,6 @@ Requirements ============ - Python 2.7 or later -- suds LICENSE diff --git a/gsxws.py b/gsxws.py deleted file mode 100755 index da917d6..0000000 --- a/gsxws.py +++ /dev/null @@ -1,1116 +0,0 @@ -#!/usr/bin/env python - -#coding=utf-8 - -""" -Copyright (c) 2013, Filipp Lepalaan All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. -- Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED -AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" - -import re -import os -import json -import suds -import base64 -import urllib -import hashlib -import logging -import tempfile - -from suds.client import Client -from suds.cache import ObjectCache -import xml.etree.ElementTree as ET -from datetime import datetime, date, time - -# Must use a few module-level global variables -CLIENT = None -SESSION = dict() -LOCALE = "en_XXX" -CACHE = ObjectCache(minutes=20) -COMPTIA_CACHE = ObjectCache(months=1) - -TIMEZONES = ( - ('GMT', "UTC (Greenwich Mean Time)"), - ('PDT', "UTC - 7h (Pacific Daylight Time)"), - ('PST', "UTC - 8h (Pacific Standard Time)"), - ('CDT', "UTC - 5h (Central Daylight Time)"), - ('CST', "UTC - 6h (Central Standard Time)"), - ('EDT', "UTC - 4h (Eastern Daylight Time)"), - ('EST', "UTC - 5h (Eastern Standard Time)"), - ('CEST', "UTC + 2h (Central European Summer Time)"), - ('CET', "UTC + 1h (Central European Time)"), - ('JST', "UTC + 9h (Japan Standard Time)"), - ('IST', "UTC + 5.5h (Indian Standard Time)"), - ('CCT', "UTC + 8h (Chinese Coast Time)"), - ('AEST', "UTC + 10h (Australian Eastern Standard Time)"), - ('AEDT', "UTC + 11h (Australian Eastern Daylight Time)"), - ('ACST', "UTC + 9.5h (Austrailian Central Standard Time)"), - ('ACDT', "UTC + 10.5h (Australian Central Daylight Time)"), - ('NZST', "UTC + 12h (New Zealand Standard Time)"), -) - -REGIONS = ( - ('002', "Asia/Pacific"), - ('003', "Japan"), - ('004', "Europe"), - ('005', "United States"), - ('006', "Canadia"), - ('007', "Latin America"), -) - -REGION_CODES = ('apac', 'am', 'la', 'emea',) - -ENVIRONMENTS = ( - ('pr', "Production"), - ('ut', "Development"), - ('it', "Testing"), -) - - -def validate(value, what=None): - """ - Tries to guess the meaning of value or validate that - value looks like what it's supposed to be. - """ - result = None - - if not isinstance(value, basestring): - raise ValueError('%s is not valid input') - - rex = { - 'partNumber': r'^([A-Z]{1,2})?\d{3}\-?(\d{4}|[A-Z]{2})(/[A-Z])?$', - 'serialNumber': r'^[A-Z0-9]{11,12}$', - 'eeeCode': r'^[A-Z0-9]{3,4}$', - 'returnOrder': r'^7\d{9}$', - 'repairNumber': r'^\d{12}$', - 'dispatchId': r'^G\d{9}$', - 'alternateDeviceId': r'^\d{15}$', - 'diagnosticEventNumber': r'^\d{23}$', - 'productName': r'^i?Mac', - } - - for k, v in rex.items(): - if re.match(v, value): - result = k - - return (result == what) if what else result - - -def get_format(locale=LOCALE): - df = open(os.path.join(os.path.dirname(__file__), 'langs.json'), 'r') - data = json.load(df) - - return data[locale] - - -class GsxObject(object): - """ - The thing that gets sent to and from GSX - """ - dt = 'ns3:authenticateRequestType' # The GSX datatype matching this object - request_dt = 'ns3:authenticateRequestType' # The GSX datatype matching this request - method = 'Authenticate' # The SOAP method to call on the GSX server - - def __init__(self, *args, **kwargs): - - formats = get_format() - - # native types are not welcome here :) - for k, v in kwargs.items(): - if isinstance(v, date): - kwargs[k] = v.strftime(formats['df']) - if isinstance(v, time): - kwargs[k] = v.strftime(formats['tf']) - if isinstance(v, bool): - kwargs[k] = 'Y' if v else 'N' - - self.data = kwargs - - if CLIENT is not None: - self.dt = CLIENT.factory.create(self.dt) - self.request_dt = CLIENT.factory.create(self.request_dt) - - def set_method(self, new_method): - self.method = new_method - - def set_type(self, new_dt): - """ - Sets the object's primary data type to new_dt - """ - self.dt = self._make_type(new_dt) - - try: - for k, v in self.data.items(): - setattr(self.dt, k, v) - except Exception, e: - pass - - def set_request(self, new_dt=None, field=None): - """ - Sets the field of this object's request datatype to the new value - """ - if new_dt is not None: - self.request_dt = self._make_type(new_dt) - - setattr(self.request_dt, field, self.dt) - - def submit(self, method, data, attr=None): - """ - Submits the SOAP envelope - """ - f = getattr(CLIENT.service, method) - - try: - result = f(data) - return getattr(result, attr) if attr else result - except suds.WebFault, e: - raise GsxError(fault=e) - - def _make_type(self, new_dt): - """ - Creates the top-level datatype for the API call - """ - dt = CLIENT.factory.create(new_dt) - - if SESSION: - dt.userSession = SESSION - - return dt - - def _process(self, data): - """ - Tries to coerce some types to their Python counterparts - """ - for k, v in data: - # decode binary data - if k in ['packingList', 'proformaFileData', 'returnLabelFileData']: - v = base64.b64decode(v) - - if isinstance(v, basestring): - # convert dates to native Python types - if re.search(r'^\d{2}/\d{2}/\d{2}$', v): - m, d, y = v.split('/') - v = date(2000+int(y), int(m), int(d)) - - # strip currency prefix and munge into float - if re.search(r'Price$', k): - v = float(re.sub('[A-Z ,]', '', v)) - - setattr(data, k, v) - - return data - - def get_response(self, xml_el): - - if isinstance(xml_el, list): - out = [] - for i in xml_el: - out.append(self.get_response(i)) - - return out - - if isinstance(xml_el, dict): - out = [] - for i in xml_el.items(): - out.append(self.get_response(i)) - - return out - - class ReturnData(dict): - pass - - rd = ReturnData() - - for r in xml_el.iter(): - k, v = r.tag, r.text - if k in ['packingList', 'proformaFileData', 'returnLabelFileData']: - v = base64.b64decode(v) - - setattr(rd, k, v) - - return rd - - def __getattr__(self, name): - return self.data[name] - - -class Content(GsxObject): - def fetch_image(self, url): - """ - The Fetch Image API allows users to get the image file from GSX, - for the content articles, using the image URL. - The image URLs will be obtained from the image html tags - in the data from all content APIs. - """ - dt = self._make_type('ns3:fetchImageRequestType') - dt.imageRequest = {'imageUrl': url} - - return self.submit('FetchImage', dt, 'contentResponse') - - -class CompTIA(object): - "Stores and accesses CompTIA codes." - - MODIFIERS = ( - ("A", "Not Applicable"), - ("B", "Continuous"), - ("C", "Intermittent"), - ("D", "Fails After Warm Up"), - ("E", "Environmental"), - ("F", "Configuration: Peripheral"), - ("G", "Damaged"), - ) - - GROUPS = ( - ('0', 'General'), - ('1', 'Visual'), - ('2', 'Displays'), - ('3', 'Mass Storage'), - ('4', 'Input Devices'), - ('5', 'Boards'), - ('6', 'Power'), - ('7', 'Printer'), - ('8', 'Multi-function Device'), - ('9', 'Communication Devices'), - ('A', 'Share'), - ('B', 'iPhone'), - ('E', 'iPod'), - ('F', 'iPad'), - ) - - def __init__(self): - """ - Initialize CompTIA symptoms from JSON file - """ - df = open(os.path.join(os.path.dirname(__file__), 'comptia.json')) - self.data = json.load(df) - - def fetch(self): - """ - The CompTIA Codes Lookup API retrieves a list of CompTIA groups and modifiers. - - Here we must resort to raw XML parsing since SUDS throws this: - suds.TypeNotFound: Type not found: 'comptiaDescription' - when calling CompTIACodes()... - - >>> CompTIA().fetch() - {'A': {'972': 'iPod not recognized',... - """ - global COMPTIA_CACHE - if COMPTIA_CACHE.get("comptia"): - return COMPTIA_CACHE.get("comptia") - - CLIENT.set_options(retxml=True) - dt = CLIENT.factory.create("ns3:comptiaCodeLookupRequestType") - dt.userSession = SESSION - - try: - xml = CLIENT.service.CompTIACodes(dt) - except suds.WebFault, e: - raise GsxError(fault=e) - - root = ET.fromstring(xml).findall('.//%s' % 'comptiaInfo')[0] - - for el in root.findall(".//comptiaGroup"): - group = {} - comp_id = el[0].text - - for ci in el.findall('comptiaCodeInfo'): - group[ci[0].text] = ci[1].text - - self.data[comp_id] = group - - COMPTIA_CACHE.put("comptia", self.data) - return self.data - - def symptoms(self, component=None): - """ - Returns all known CompTIA symptom codes or just the ones - belonging to the given component code. - """ - r = dict() - - for g, codes in self.data.items(): - r[g] = list() - for k, v in codes.items(): - r[g].append((k, v,)) - - return r[component] if component else r - - -class GsxResponse(dict): - """ - This contains the data returned by a raw GSX query - """ - def __getattr__(self, item): - return self.__getitem__(item) - - def __setattr__(self, item, value): - self.__setitem__(item, value) - - @classmethod - def Process(cls, node): - nodedict = cls() - - for child in node: - k, v = child.tag, child.text - newitem = cls.Process(child) - - if nodedict.has_key(k): - # found duplicate tag - if isinstance(nodedict[k], list): - # append to existing list - nodedict[k].append(newitem) - else: - # convert to list - nodedict[k] = [nodedict[k], newitem] - else: - # unique tag -> set the dictionary - nodedict[k] = newitem - - if k in ('packingList', 'proformaFileData', 'returnLabelFileData'): - nodedict[k] = base64.b64decode(v) - - if isinstance(v, basestring): - # convert dates to native Python type - if re.search('^\d{2}/\d{2}/\d{2}$', v): - m, d, y = v.split('/') - v = date(2000+int(y), int(m), int(d)).isoformat() - - # strip currency prefix and munge into float - if re.search('Price$', k): - v = float(re.sub('[A-Z ,]', '', v)) - - # Convert timestamps to native Python type - # 18-Jan-13 14:38:04 - if re.search('TimeStamp$', k): - v = datetime.strptime(v, '%d-%b-%y %H:%M:%S') - - # convert Y and N to corresponding boolean - if re.search('^[YN]$', k): - v = (v == 'Y') - - nodedict[k] = v - - return nodedict - - -class GsxError(Exception): - def __init__(self, message=None, code=None, fault=None): - if isinstance(fault, suds.WebFault): - self.code = fault.fault.faultcode - self.message = fault.fault.faultstring - else: - self.code = code - self.message = message - - self.data = (self.code, self.message) - - def __getitem__(self, idx): - return self.data[idx] - - def __repr__(self): - print self.data - - def __str__(self): - return self.data[1] - - -class Lookup(GsxObject): - def lookup(self, dt, method): - dt = self._make_type(dt) - dt.lookupRequestData = self.data - return self.submit(method, dt, "lookupResponseData") - - def parts(self): - """ - The Parts Lookup API allows users to access part and part pricing data prior to - creating a repair or order. Parts lookup is also a good way to search for - part numbers by various attributes of a part - (config code, EEE code, serial number, etc.). - """ - dt = self._make_type("ns0:partsLookupRequestType") - dt.lookupRequestData = self.data - return self.submit("PartsLookup", dt, "parts") - - def repairs(self): - """ - The Repair Lookup API mimics the front-end repair search functionality. - It fetches up to 2500 repairs in a given criteria. - Subsequently, the extended Repair Status API can be used - to retrieve more details of the repair. - """ - dt = CLIENT.factory.create('ns6:repairLookupInfoType') - request = CLIENT.factory.create('ns1:repairLookupRequestType') - request.userSession = SESSION - request.lookupRequestData = self.data - return self.submit("RepairLookup", request, "lookupResponseData") - - def invoices(self): - """ - The Invoice ID Lookup API allows AASP users - to fetch the invoice generated for last 24 hrs - - >>> Lookup(shipTo=677592, invoiceDate='02/06/12').invoices() - """ - result = self.lookup('ns1:invoiceIDLookupRequestType', 'InvoiceIDLookup') - return result.invoiceID # This is actually a list of Invoice ID's... - - def invoice_details(self): - """ - The Invoice Details Lookup API allows AASP users to - download invoice for a given invoice id. - """ - result = self.lookup('ns1:invoiceDetailsLookupRequestType', 'InvoiceDetailsLookup') - pdf = base64.b64decode(result.invoiceData) - outfile = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) - outfile.write(pdf) - result.invoiceData = outfile.name - return result - - -class Diagnostics(GsxObject): - def fetch(self): - """ - The Fetch Repair Diagnostics API allows the service providers/depot/carriers - to fetch MRI/CPU diagnostic details from the Apple Diagnostic Repository OR - diagnostic test details of iOS Devices. - The ticket is generated within GSX system. - - >>> Diagnostics(diagnosticEventNumber='12942008007242012052919').fetch() - """ - # Using raw XML to avoid TypeNotFound: Type not found: 'toolID' or operationID - CLIENT.set_options(retxml=True) - - if "alternateDeviceId" in self.data: - dt = self._make_type("ns3:fetchIOSDiagnosticRequestType") - dt.lookupRequestData = self.data - - try: - result = CLIENT.service.FetchIOSDiagnostic(dt) - except suds.WebFault, e: - raise GsxError(fault=e) - - root = ET.fromstring(result).findall("*//FetchIOSDiagnosticResponse")[0] - else: - dt = self._make_type("ns3:fetchRepairDiagnosticRequestType") - dt.lookupRequestData = self.data - - try: - result = CLIENT.service.FetchRepairDiagnostic(dt) - except suds.WebFault, e: - raise GsxError(fault=e) - - root = ET.fromstring(result).findall("*//FetchRepairDiagnosticResponse")[0] - - return GsxResponse.Process(root) - - def events(self): - """ - The Fetch Diagnostic Event Numbers API allows users to retrieve all - diagnostic event numbers associated with provided input - (serial number or alternate device ID). - """ - dt = self._make_type("ns3:fetchDiagnosticEventNumbersRequestType") - dt.lookupRequestData = self.data - pass - - -class Order(GsxObject): - def __init__(self, type='stocking', *args, **kwargs): - super(Order, self).__init__(*args, **kwargs) - self.data['orderLines'] = list() - - def add_part(self, part_number, quantity): - self.data['orderLines'].append({ - 'partNumber': part_number, 'quantity': quantity - }) - - def submit(self): - dt = CLIENT.factory.create('ns1:createStockingOrderRequestType') - dt.userSession = SESSION - dt.orderData = self.data - - try: - result = CLIENT.service.CreateStockingOrder(dt) - return result.orderConfirmation - except suds.WebFault, e: - raise GsxError(fault=e) - - -class Returns(GsxObject): - - RETURN_TYPES = ( - (1, "Dead On Arrival"), - (2, "Good Part Return"), - (3, "Convert To Stock"), - (4, "Transfer to Out of Warranty"), - ) - - def __init__(self, order_number=None, *args, **kwargs): - super(Returns, self).__init__(*args, **kwargs) - - if order_number is not None: - self.data['returnOrderNumber'] = order_number - - def get_pending(self): - """The Parts Pending Return API returns a list of all parts that - are pending for return, based on the search criteria. - - >>> Returns(repairType='CA').get_pending() # doctest: +SKIP - """ - dt = self._make_type('ns1:partsPendingReturnRequestType') - dt.repairData = self.data - - return self.submit('PartsPendingReturn', dt, 'partsPendingResponse') - - def get_report(self): - """The Return Report API returns a list of all parts that are returned - or pending for return, based on the search criteria. - """ - dt = self._make_type('ns1:returnReportRequestType') - dt.returnRequestData = self.data - - return self.submit('ReturnReport', dt, 'returnResponseData') - - def get_label(self, part_number): - """The Return Label API retrieves the Return Label for a given Return Order Number. - (Type not found: 'comptiaCode') - so we're parsing the raw SOAP response and creating a "fake" return object from that. - """ - if not validate(part_number, 'partNumber'): - raise ValueError('%s is not a valid part number' % part_number) - - class ReturnData(dict): - pass - - rd = ReturnData() - - CLIENT.set_options(retxml=True) - - dt = CLIENT.factory.create('ns1:returnLabelRequestType') - dt.returnOrderNumber = self.data['returnOrderNumber'] - dt.partNumber = part_number - dt.userSession = SESSION - - try: - result = CLIENT.service.ReturnLabel(dt) - except suds.WebFault, e: - raise GsxError(fault=e) - - el = ET.fromstring(result).findall('*//%s' % 'returnLabelData')[0] - - for r in el.iter(): - - k, v = r.tag, r.text - - if k in ['packingList', 'proformaFileData', 'returnLabelFileData']: - v = base64.b64decode(v) - of = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) - of.write(v) - v = of.name - - setattr(rd, k, v) - - return rd - - def get_proforma(self): - """The View Bulk Return Proforma API allows you to view the proforma label - for a given Bulk Return Id. You can create a parts bulk return - by using the Register Parts for Bulk Return API. - """ - pass - - def register_parts(self, parts): - """The Register Parts for Bulk Return API creates a bulk return for - the registered parts. - The API returns the Bulk Return Id with the packing list. - """ - dt = self._make_type("ns1:registerPartsForBulkReturnRequestType") - self.data['bulkReturnOrder'] = parts - dt.bulkPartsRegistrationRequest = self.data - - result = self.submit("RegisterPartsForBulkReturn", dt, "bulkPartsRegistrationData") - - pdf = base64.b64decode(result.packingList) - of = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) - of.write(pdf) - result.packingList = of.name - - return result - - def update_parts(self, confirmation, parts): - """ - The Parts Return Update API allows you to mark a part - with the status GPR(2), DOA(1), CTS(3), or TOW(4). - The API can be used only by ASP. - - >>> Returns().update_parts('G135877430',\ - [{'partNumber': '661-5174',\ - 'comptiaCode': 'Z29',\ - 'comptiaModifier': 'A',\ - 'returnType': 2}]) - """ - dt = self._make_type("ns1:partsReturnUpdateRequestType") - repairData = { - 'repairConfirmationNumber': confirmation, - 'orderLines': parts - } - dt.repairData = repairData - result = self.submit("PartsReturnUpdate", dt) - print result - return result - - -class Part(GsxObject): - def lookup(self): - lookup = Lookup(**self.data) - return lookup.parts() - - def fetch_image(self): - """ - Tries the fetch the product image for this service part - """ - if self.partNumber is None: - raise GsxError('Cannot fetch part image without part number') - - image = '%s_350_350.gif' % self.partNumber - url = 'https://km.support.apple.com.edgekey.net/kb/imageService.jsp?image=%s' % image - tmpfile = tempfile.mkstemp(suffix=image) - - try: - return urllib.urlretrieve(url, tmpfile[1])[0] - except Exception, e: - raise GsxError('Failed to fetch part image: %s' % e) - - -class Escalation(GsxObject): - def create(self): - """ - The Create General Escalation API allows users to create - a general escalation in GSX. The API was earlier known as GSX Help. - """ - dt = self._make_type("ns1:createGenEscRequestType") - dt.escalationRequest = self.data - return self.submit("CreateGeneralEscalation", dt, "escalationConfirmation") - - def update(self): - """ - The Update General Escalation API allows Depot users to - update a general escalation in GSX. - """ - dt = self._make_type("ns1:updateGeneralEscRequestType") - dt.escalationRequest = self.data - return self.submit("UpdateGeneralEscalation", dt, "escalationConfirmation") - - -class Repair(GsxObject): - - dt = 'ns6:repairLookupInfoType' - request_dt = 'ns1:repairLookupRequestType' - - def __init__(self, number=None, *args, **kwargs): - super(Repair, self).__init__(*args, **kwargs) - - if number is not None: - self.data['dispatchId'] = number - self.data['repairConfirmationNumber'] = number - - def create_carryin(self): - """ - GSX validates the information and if all of the validations go through, - it obtains a quote for the repair and creates the carry-in repair. - """ - dt = self._make_type('ns2:carryInRequestType') - dt.repairData = self.data - - return self.submit('CreateCarryInRepair', dt, 'repairConfirmation') - - def create_cnd(self): - """ - The Create CND Repair API allows Service Providers to create a repair - whenever the reported issue cannot be duplicated, and the repair - requires no parts replacement. - N01 Unable to Replicate - N02 Software Update/Issue - N03 Cable/Component Reseat - N05 SMC Reset - N06 PRAM Reset - N07 Third Party Part - N99 Other - """ - pass - - def update_carryin(self, newdata): - """ - Description - The Update Carry-In Repair API allows the service providers - to update the existing open carry-in repairs. - This API assists in addition/deletion of parts and addition of notes - to a repair. On successful update, the repair confirmation number and - quote for any newly added parts would be returned. - In case of any validation error or unsuccessful update, a fault code is issued. - - Carry-In Repair Update Status Codes: - AWTP Awaiting Parts - AWTR Parts Allocated - BEGR In Repair - RFPU Ready for Pickup - """ - dt = self._make_type('ns1:updateCarryInRequestType') - - # Merge old and new data (old data should have Dispatch ID) - dt.repairData = dict(self.data.items() + newdata.items()) - - return self.submit('CarryInRepairUpdate', dt, 'repairConfirmation') - - def update_sn(self, parts): - """ - Description - The Update Serial Number API allows the service providers to - update the module serial numbers. - Context: - The API is not applicable for whole unit replacement - serial number entry (see KGB serial update). - """ - dt = self._make_type('ns1:updateSerialNumberRequestType') - repairData = {'repairConfirmationNumber': self.data.get('dispatchId')} - repairData['partInfo'] = parts - dt.repairData = repairData - - return self.submit('UpdateSerialNumber', dt, 'repairConfirmation') - - def update_kgb_sn(self, sn): - """ - Description: - The KGB Serial Number Update API is always to be used on - whole unit repairs that are in a released state. - This API allows users to provide the KGB serial number for the - whole unit exchange repairs. It also checks for the privilege - to create/ update whole unit exchange repairs - before updating the whole unit exchange repair. - - Context: - The API is to be used on whole unit repairs that are in a released state. - This API can be invoked only after carry-in repair creation API. - """ - - # Using raw XML to avoid: - # Exception: <UpdateKGBSerialNumberResponse/> not mapped to message part - CLIENT.set_options(retxml=True) - dt = self._make_type('ns1:updateKGBSerialNumberRequestType') - dt.repairConfirmationNumber = self.data['dispatchId'] - dt.serialNumber = sn - - try: - result = CLIENT.service.KGBSerialNumberUpdate(dt) - except suds.WebFault, e: - raise GsxError(fault=e) - - root = ET.fromstring(result).findall('*//%s' % 'UpdateKGBSerialNumberResponse') - return GsxResponse.Process(root[0]) - - def lookup(self): - """ - Description: - The Repair Lookup API mimics the front-end repair search functionality. - It fetches up to 2500 repairs in a given criteria. - Subsequently, the extended Repair Status API can be used - to retrieve more details of the repair. - - >>> Repair(repairStatus='Open').lookup() - """ - return Lookup(**self.data).repairs() - - def mark_complete(self, numbers=None): - """ - The Mark Repair Complete API allows a single or an array of - repair confirmation numbers to be submitted to GSX to be marked as complete. - """ - dt = self._make_type('ns1:markRepairCompleteRequestType') - dt.repairConfirmationNumbers = [self.data['dispatchId']] - - try: - result = CLIENT.service.MarkRepairComplete(dt) - return result.repairConfirmationNumbers - except suds.WebFault, e: - raise GsxError(fault=e) - - def delete(self): - """ - The Delete Repair API allows the service providers to delete - the existing GSX Initiated Carry-In, Return Before Replace & Onsite repairs - which are in Declined-Rejected By TSPS Approver state, - that do not have an active repair id. - """ - pass - - def get_status(self, numbers=None): - """ - The Repair Status API retrieves the status - for the submitted repair confirmation number(s). - """ - dt = self._make_type('ns1:repairStatusRequestType') - dt.repairConfirmationNumbers = [self.data['dispatchId']] - result = CLIENT.service.RepairStatus(dt) - - if len(result.repairStatus) == 1: - return result.repairStatus[0] - else: - return result.repairStatus - - def get_details(self): - """ - The Repair Details API includes the shipment information - similar to the Repair Lookup API. - - >>> Repair('G135773004').get_details() - asd - """ - dt = self._make_type('ns0:repairDetailsRequestType') - dt.dispatchId = self.data['dispatchId'] - results = CLIENT.service.RepairDetails(dt) - details = results.lookupResponseData[0] - - # fix tracking URL if available - for i, p in enumerate(details.partsInfo): - try: - url = re.sub('<<TRKNO>>', p.deliveryTrackingNumber, p.carrierURL) - details.partsInfo[i].carrierURL = url - except AttributeError: - pass - - return details - - -class Communication(GsxObject): - def get_content(): - """ - The Fetch Communication Content API allows the service providers/depot/carriers - to fetch the communication content by article ID from the service news channel. - """ - - def get_articles(): - """ - The Fetch Communication Articles API allows the service partners - to fetch all the active communication message IDs. - """ - - -class Product(GsxObject): - - dt = 'ns7:unitDetailType' - serialNumber = "" - alternateDeviceId = "" - - def __init__(self, serialNumber, *args, **kwargs): - super(Product, self).__init__(*args, **kwargs) - - dt = {'serialNumber': serialNumber} - - if validate(serialNumber, 'alternateDeviceId'): - self.alternateDeviceId = serialNumber - dt = {'alternateDeviceId': serialNumber} - else: - self.serialNumber = serialNumber - - if SESSION: - self.dt = dt - self.lookup = Lookup(**dt) - - def get_model(self): - """ - This API allows Service Providers/Carriers to fetch - Product Model information for the given serial number. - - >>> Product('W874939YX92').get_model().configDescription - MacBook Pro (15-inch 2.4/2.2GHz) - """ - #self.set_request('ns3:fetchProductModelRequestType', 'productModelRequest') - dt = self._make_type("ns3:fetchProductModelRequestType") - dt.productModelRequest = self.dt - result = self.submit('FetchProductModel', dt, "productModelResponse") - return result[0] - - def get_warranty(self, date_received=None, parts=[]): - """ - The Warranty Status API retrieves the same warranty details - displayed on the GSX Coverage screen. - If part information is provided, the part warranty information is returned. - If you do not provide the optional part information in the - warranty status request, the unit level warranty information is returned. - - >>> Product('013348005376007').get_warranty().warrantyStatus - Apple Limited Warranty - >>> Product('W874939YX92').get_warranty().warrantyStatus - Out Of Warranty (No Coverage) - """ - dt = self._make_type("ns3:warrantyStatusRequestType") - - if not self.serialNumber: - activation = self.get_activation() - self.serialNumber = activation.serialNumber - self.dt = {'serialNumber': self.serialNumber} - - dt.unitDetail = self.dt - - result = self.submit("WarrantyStatus", dt, "warrantyDetailInfo") - return self._process(result) - - def get_activation(self): - """ - The Fetch iOS Activation Details API is used to - fetch activation details of iOS Devices. - - >>> Product('013348005376007').get_activation().unlocked - true - >>> Product('W874939YX92').get_activation().unlocked - Traceback (most recent call last): - ... - GsxError: Provided serial number does not belong to an iOS Device. - ... - """ - dt = self._make_type("ns3:fetchIOSActivationDetailsRequestType") - - if self.serialNumber: - dt.serialNumber = self.serialNumber - else: - dt.alternateDeviceId = self.alternateDeviceId - - return self.submit('FetchIOSActivationDetails', dt, 'activationDetailsInfo') - - def get_parts(self): - return self.lookup.parts() - - def get_repairs(self): - return self.lookup.repairs() - - def get_diagnostics(self): - diags = Diagnostics(serialNumber=self.serialNumber) - return diags.fetch() - - def fetch_image(self): - if not self.imageURL: - raise GsxError('Cannot fetch product image with image URL') - - try: - result = urllib.urlretrieve(self.imageURL) - return result[0] - except Exception, e: - raise GsxError('Failed to fetch product image: %s' % e) - - -def init(env='ut', region='emea'): - """ - Initialize the SOAP client - """ - - global CLIENT, REGION_CODES - - envs = ('pr', 'it', 'ut',) - hosts = {'pr': 'ws2', 'it': 'wsit', 'ut': 'wsut'} - - if region not in REGION_CODES: - raise ValueError('Region should be one of: %s' % ','.join(REGION_CODES)) - - if env not in envs: - raise ValueError('Environment should be one of: %s' % ','.join(envs)) - - url = "https://gsx{env}.apple.com/wsdl/{region}Asp/gsx-{region}Asp.wsdl" - url = url.format(env=hosts[env], region=region) - - CLIENT = Client(url) - cache = CLIENT.options.cache - cache.setduration(weeks=1) - - -def connect( - user_id, - password, - sold_to, - language='en', - timezone='CEST', - environment='ut', - region='emea', - locale=LOCALE): - """ - Establishes connection with GSX Web Services. - Returns the session ID of the new connection. - """ - - global CACHE - global LOCALE - global SESSION - - SESSION = {} - LOCALE = LOCALE - - md5 = hashlib.md5() - md5.update(user_id + str(sold_to) + environment) - cache_key = md5.hexdigest() - - if CACHE.get(cache_key) is not None: - SESSION = CACHE.get(cache_key) - init(environment, region) - - return SESSION - - init(environment, region) - - account = CLIENT.factory.create('ns3:authenticateRequestType') - - account.userId = user_id - account.password = password - account.languageCode = language - account.userTimeZone = timezone - account.serviceAccountNo = sold_to - - try: - result = CLIENT.service.Authenticate(account) - SESSION['userSessionId'] = result.userSessionId - CACHE.put(cache_key, SESSION) - return SESSION - except suds.WebFault, e: - raise GsxError(fault=e) - - -def logout(): - CLIENT.service.Logout() - -if __name__ == '__main__': - import doctest - import argparse - - parser = argparse.ArgumentParser(description='Communicate with GSX Web Services') - - parser.add_argument('user_id') - parser.add_argument('password') - parser.add_argument('sold_to') - parser.add_argument('--language', default='en') - parser.add_argument('--timezone', default='CEST') - parser.add_argument('--environment', default='pr') - parser.add_argument('--region', default='emea') - - args = parser.parse_args() - connect(**vars(args)) - #connect(args.user_id, args.password, args.sold_to. args.environment) - doctest.testmod() diff --git a/gsxws/__init__.py b/gsxws/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/gsxws/__init__.py diff --git a/gsxws/comms.py b/gsxws/comms.py new file mode 100644 index 0000000..e18193f --- /dev/null +++ b/gsxws/comms.py @@ -0,0 +1,13 @@ +class Communication(GsxObject): + def get_content(): + """ + The Fetch Communication Content API allows the service providers/depot/carriers + to fetch the communication content by article ID from the service news channel. + """ + + def get_articles(): + """ + The Fetch Communication Articles API allows the service partners + to fetch all the active communication message IDs. + """ +
\ No newline at end of file diff --git a/comptia.json b/gsxws/comptia.json index 92d6f1c..92d6f1c 100644 --- a/comptia.json +++ b/gsxws/comptia.json diff --git a/gsxws/comptia.py b/gsxws/comptia.py new file mode 100644 index 0000000..f7e7ff7 --- /dev/null +++ b/gsxws/comptia.py @@ -0,0 +1,89 @@ +MODIFIERS = ( + ("A", "Not Applicable"), + ("B", "Continuous"), + ("C", "Intermittent"), + ("D", "Fails After Warm Up"), + ("E", "Environmental"), + ("F", "Configuration: Peripheral"), + ("G", "Damaged"), +) + +GROUPS = ( + ('0', 'General'), + ('1', 'Visual'), + ('2', 'Displays'), + ('3', 'Mass Storage'), + ('4', 'Input Devices'), + ('5', 'Boards'), + ('6', 'Power'), + ('7', 'Printer'), + ('8', 'Multi-function Device'), + ('9', 'Communication Devices'), + ('A', 'Share'), + ('B', 'iPhone'), + ('E', 'iPod'), + ('F', 'iPad'), +) + + +class CompTIA(object): + "Stores and accesses CompTIA codes." + + def __init__(self): + """ + Initialize CompTIA symptoms from JSON file + """ + df = open(os.path.join(os.path.dirname(__file__), 'comptia.json')) + self.data = json.load(df) + + def fetch(self): + """ + The CompTIA Codes Lookup API retrieves a list of CompTIA groups and modifiers. + + Here we must resort to raw XML parsing since SUDS throws this: + suds.TypeNotFound: Type not found: 'comptiaDescription' + when calling CompTIACodes()... + + >>> CompTIA().fetch() + {'A': {'972': 'iPod not recognized',... + """ + global COMPTIA_CACHE + if COMPTIA_CACHE.get("comptia"): + return COMPTIA_CACHE.get("comptia") + + CLIENT.set_options(retxml=True) + dt = CLIENT.factory.create("ns3:comptiaCodeLookupRequestType") + dt.userSession = SESSION + + try: + xml = CLIENT.service.CompTIACodes(dt) + except suds.WebFault, e: + raise GsxError(fault=e) + + root = ET.fromstring(xml).findall('.//%s' % 'comptiaInfo')[0] + + for el in root.findall(".//comptiaGroup"): + group = {} + comp_id = el[0].text + + for ci in el.findall('comptiaCodeInfo'): + group[ci[0].text] = ci[1].text + + self.data[comp_id] = group + + COMPTIA_CACHE.put("comptia", self.data) + return self.data + + def symptoms(self, component=None): + """ + Returns all known CompTIA symptom codes or just the ones + belonging to the given component code. + """ + r = dict() + + for g, codes in self.data.items(): + r[g] = list() + for k, v in codes.items(): + r[g].append((k, v,)) + + return r[component] if component else r diff --git a/gsxws/content.py b/gsxws/content.py new file mode 100644 index 0000000..b73ca1f --- /dev/null +++ b/gsxws/content.py @@ -0,0 +1,12 @@ +class Content(GsxObject): + def fetch_image(self, url): + """ + The Fetch Image API allows users to get the image file from GSX, + for the content articles, using the image URL. + The image URLs will be obtained from the image html tags + in the data from all content APIs. + """ + dt = self._make_type('ns3:fetchImageRequestType') + dt.imageRequest = {'imageUrl': url} + + return self.submit('FetchImage', dt, 'contentResponse') diff --git a/gsxws/core.py b/gsxws/core.py new file mode 100644 index 0000000..0c55a61 --- /dev/null +++ b/gsxws/core.py @@ -0,0 +1,469 @@ +""" +Copyright (c) 2013, Filipp Lepalaan All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +import re +import json +import base64 +import shelve +import os.path +import hashlib +import logging +import httplib +import tempfile +from urlparse import urlparse +import xml.etree.ElementTree as ET + +from datetime import date, time, datetime, timedelta + +GSX_ENV = "it" +GSX_LANG = "en" +GSX_REGION = "emea" +GSX_SESSION = None +GSX_LOCALE = "en_XXX" + +GSX_TIMEZONES = ( + ('GMT', "UTC (Greenwich Mean Time)"), + ('PDT', "UTC - 7h (Pacific Daylight Time)"), + ('PST', "UTC - 8h (Pacific Standard Time)"), + ('CDT', "UTC - 5h (Central Daylight Time)"), + ('CST', "UTC - 6h (Central Standard Time)"), + ('EDT', "UTC - 4h (Eastern Daylight Time)"), + ('EST', "UTC - 5h (Eastern Standard Time)"), + ('CEST', "UTC + 2h (Central European Summer Time)"), + ('CET', "UTC + 1h (Central European Time)"), + ('JST', "UTC + 9h (Japan Standard Time)"), + ('IST', "UTC + 5.5h (Indian Standard Time)"), + ('CCT', "UTC + 8h (Chinese Coast Time)"), + ('AEST', "UTC + 10h (Australian Eastern Standard Time)"), + ('AEDT', "UTC + 11h (Australian Eastern Daylight Time)"), + ('ACST', "UTC + 9.5h (Austrailian Central Standard Time)"), + ('ACDT', "UTC + 10.5h (Australian Central Daylight Time)"), + ('NZST', "UTC + 12h (New Zealand Standard Time)"), +) + +GSX_REGIONS = ( + ('002', "Asia/Pacific"), + ('003', "Japan"), + ('004', "Europe"), + ('005', "United States"), + ('006', "Canadia"), + ('007', "Latin America"), +) + +REGION_CODES = ('apac', 'am', 'la', 'emea',) + +ENVIRONMENTS = ( + ('pr', "Production"), + ('ut', "Development"), + ('it', "Testing"), +) + + +def validate(value, what=None): + """ + Tries to guess the meaning of value or validate that + value looks like what it's supposed to be. + """ + result = None + + if not isinstance(value, basestring): + raise ValueError('%s is not valid input') + + rex = { + 'partNumber': r'^([A-Z]{1,2})?\d{3}\-?(\d{4}|[A-Z]{2})(/[A-Z])?$', + 'serialNumber': r'^[A-Z0-9]{11,12}$', + 'eeeCode': r'^[A-Z0-9]{3,4}$', + 'returnOrder': r'^7\d{9}$', + 'repairNumber': r'^\d{12}$', + 'dispatchId': r'^G\d{9}$', + 'alternateDeviceId': r'^\d{15}$', + 'diagnosticEventNumber': r'^\d{23}$', + 'productName': r'^i?Mac', + } + + for k, v in rex.items(): + if re.match(v, value): + result = k + + return (result == what) if what else result + + +def get_formats(locale=GSX_LOCALE): + filepath = os.path.join(os.path.dirname(__file__), 'langs.json') + df = open(filepath, 'r') + return json.load(df).get(locale) + + +class GsxError(Exception): + def __init__(self, message=None, xml=None): + + if message is not None: + raise ValueError(message) + + if xml is not None: + el = ET.fromstring(xml) + self.code = el.findtext("*//faultcode") + + if self.code is None: + raise ValueError("An unexpected error occured") + + self.message = el.findtext("*//faultstring") + + def __unicode__(self): + return self.message + + def __str__(self): + return self.message + + +class GsxCache(object): + """ + >>> GsxCache('spam').set('eggs').get() + 'eggs' + """ + shelf = None + tmpdir = tempfile.gettempdir() + expires = timedelta(minutes=20) + filename = os.path.join(tmpdir, "gsxws.tmp") + + def __init__(self, key): + self.key = key + self.shelf = shelve.open(self.filename, protocol=-1) + self.now = datetime.now() + + if not self.shelf.get(key): + # Initialize the key + self.set(None) + + def get(self): + try: + d = self.shelf[self.key] + if d['expires'] > self.now: + return d['value'] + else: + del self.shelf[self.key] + except KeyError: + return None + + def set(self, value): + d = { + 'value': value, + 'expires': self.now + self.expires + } + + self.shelf[self.key] = d + return self + + +class GsxRequest(object): + "Creates and submits the SOAP envelope" + env = None + obj = None # The GsxObject being submitted + data = None # The GsxObject payload in XML format + body = None # The Body part of the SOAP envelope + + _request = "" + _response = "" + + envs = ('pr', 'it', 'ut',) + REGION_CODES = ('apac', 'am', 'la', 'emea',) + + hosts = {'pr': 'ws2', 'it': 'wsit', 'ut': 'wsut'} + url = "https://gsx{env}.apple.com/gsx-ws/services/{region}/asp" + + def __init__(self, **kwargs): + "Construct the SOAP envelope" + self.objects = [] + self.env = ET.Element("soapenv:Envelope") + self.env.set("xmlns:core", "http://gsxws.apple.com/elements/core") + self.env.set("xmlns:glob", "http://gsxws.apple.com/elements/global") + self.env.set("xmlns:asp", "http://gsxws.apple.com/elements/core/asp") + self.env.set("xmlns:soapenv", "http://schemas.xmlsoap.org/soap/envelope/") + + ET.SubElement(self.env, "soapenv:Header") + self.body = ET.SubElement(self.env, "soapenv:Body") + + for k, v in kwargs.items(): + self.obj = v + self._request = k + self.data = v.to_xml(self._request) + self._response = k.replace("Request", "Response") + + def _submit(self, method, response=None): + "Construct and submit the final SOAP message" + global GSX_ENV, GSX_REGION, GSX_SESSION + + root = ET.SubElement(self.body, self.obj._namespace + method) + url = self.url.format(env=self.hosts[GSX_ENV], region=GSX_REGION) + + if method is "Authenticate": + root.append(self.data) + else: + request_name = method + "Request" + request = ET.SubElement(root, request_name) + request.append(GSX_SESSION) + + if self._request == request_name: + "Some requests don't have a top-level container." + self.data = list(self.data)[0] + + request.append(self.data) + + data = ET.tostring(self.env, "UTF-8") + logging.debug(data) + + parsed = urlparse(url) + + ws = httplib.HTTPSConnection(parsed.netloc) + ws.putrequest("POST", parsed.path) + ws.putheader("User-Agent", "py-gsxws 0.9") + ws.putheader("Content-type", 'text/xml; charset="UTF-8"') + ws.putheader("Content-length", "%d" % len(data)) + ws.putheader("SOAPAction", '"%s"' % method) + ws.endheaders() + ws.send(data) + + res = ws.getresponse() + xml = res.read() + + if res.status > 200: + raise GsxError(xml=xml) + + logging.debug("Response: %s %s %s" % (res.status, res.reason, xml)) + response = response or self._response + + for r in ET.fromstring(xml).findall("*//%s" % response): + self.objects.append(GsxObject.from_xml(r)) + + return self.objects + + def __str__(self): + return ET.tostring(self.env) + + +class GsxObject(object): + "XML/SOAP representation of a GSX object" + _data = {} + + def __init__(self, *args, **kwargs): + self._data = {} + self._formats = get_formats() + + for a in args: + k = validate(a) + if k is not None: + kwargs[k] = a + + for k, v in kwargs.items(): + self.__setattr__(k, v) + + def __setattr__(self, name, value): + if name.startswith("_"): + super(GsxObject, self).__setattr__(name, value) + return + + if isinstance(value, int): + value = str(value) + if isinstance(value, date): + value = value.strftime(self._formats['df']) + if isinstance(value, time): + value = value.strftime(self._formats['tf']) + if isinstance(value, bool): + value = 'Y' if value else 'N' + if isinstance(value, date): + value = value.strftime(self._formats['df']) + + self._data[name] = value + + def __getattr__(self, name): + try: + return self._data[name] + except KeyError: + raise AttributeError("Invalid attribute: %s" % name) + + def _submit(self, arg, method, ret=None): + self._req = GsxRequest(**{arg: self}) + result = self._req._submit(method, ret) + return result if len(result) > 1 else result[0] + + def to_xml(self, root): + "Returns this object as an XML element" + root = ET.Element(root) + for k, v in self._data.items(): + el = ET.SubElement(root, k) + el.text = v + + return root + + @classmethod + def from_xml(cls, el): + obj = GsxObject() + + for r in el: + newitem = cls.from_xml(r) + k, v = r.tag, r.text + + if hasattr(obj, k): + + # found duplicate tag %s" % k + attr = obj.__getattr__(k) + + if isinstance(attr, list): + # append to existing list + newattr = attr.append(newitem) + setattr(obj, k, newattr) + else: + # convert to list + setattr(obj, k, [v, newitem]) + else: + # unique tag %s -> set the dictionary" % k + setattr(obj, k, newitem) + + if k in ["partsInfo"]: + # found new list item %s" % k + attr = [] + attr.append(GsxObject.from_xml(r)) + + setattr(obj, k, attr) + + if k in ['packingList', 'proformaFileData', 'returnLabelFileData']: + v = base64.b64decode(v) + + if isinstance(v, basestring): + # convert dates to native Python type + if re.search('^\d{2}/\d{2}/\d{2}$', v): + m, d, y = v.split('/') + v = date(2000+int(y), int(m), int(d)).isoformat() + + # strip currency prefix and munge into float + if re.search('Price$', k): + v = float(re.sub('[A-Z ,]', '', v)) + + # Convert timestamps to native Python type + # 18-Jan-13 14:38:04 + if re.search('TimeStamp$', k): + v = datetime.strptime(v, '%d-%b-%y %H:%M:%S') + + # convert Y and N to corresponding boolean + if re.search('^[YN]$', k): + v = (v == 'Y') + + setattr(obj, k, v) + + return obj + + +class GsxSession(GsxObject): + userId = "" + password = "" + languageCode = "" + userTimeZone = "" + serviceAccountNo = "" + + _cache = None + _cache_key = "" + _session_id = "" + _namespace = "glob:" + + def __init__(self, user_id, password, sold_to, language, timezone): + self.userId = user_id + self.password = password + self.languageCode = language + self.userTimeZone = timezone + self.serviceAccountNo = str(sold_to) + + md5 = hashlib.md5() + md5.update(user_id + self.serviceAccountNo) + + self._cache_key = md5.hexdigest() + self._cache = GsxCache(self._cache_key) + + def get_session(self): + session = ET.Element("userSession") + session_id = ET.Element("userSessionId") + session_id.text = self._session_id + session.append(session_id) + return session + + def login(self): + global GSX_SESSION + + if not self._cache.get() is None: + GSX_SESSION = self._cache.get() + else: + #result = self._submit("AuthenticateRequest", "Authenticated") + self._req = GsxRequest(AuthenticateRequest=self) + result = self._req._submit("Authenticate") + self._session_id = result[0].userSessionId + GSX_SESSION = self.get_session() + self._cache.set(GSX_SESSION) + + return GSX_SESSION + + def logout(self): + return GsxRequest(LogoutRequest=self) + + +def connect(user_id, password, sold_to, + environment='it', + language='en', + timezone='CEST', + region='emea', + locale='en_XXX'): + """ + Establishes connection with GSX Web Services. + Returns the session ID of the new connection. + """ + global GSX_ENV + global GSX_LANG + global GSX_LOCALE + global GSX_REGION + + GSX_LANG = language + GSX_REGION = region + GSX_LOCALE = locale + GSX_ENV = environment + + act = GsxSession(user_id, password, sold_to, language, timezone) + return act.login() + + +if __name__ == '__main__': + import doctest + import argparse + + parser = argparse.ArgumentParser(description='Communicate with GSX Web Services') + + parser.add_argument('user_id') + parser.add_argument('password') + parser.add_argument('sold_to') + parser.add_argument('--language', default='en') + parser.add_argument('--timezone', default='CEST') + parser.add_argument('--environment', default='pr') + parser.add_argument('--region', default='emea') + + args = parser.parse_args() + logging.basicConfig(level=logging.DEBUG) + connect(**vars(args)) + doctest.testmod() diff --git a/gsxws/diagnostics.py b/gsxws/diagnostics.py new file mode 100644 index 0000000..38326da --- /dev/null +++ b/gsxws/diagnostics.py @@ -0,0 +1,29 @@ +from core import GsxObject + + +class Diagnostics(GsxObject): + _namespace = "glob:" + def fetch(self): + """ + The Fetch Repair Diagnostics API allows the service providers/depot/carriers + to fetch MRI/CPU diagnostic details from the Apple Diagnostic Repository OR + diagnostic test details of iOS Devices. + The ticket is generated within GSX system. + + >>> Diagnostics(diagnosticEventNumber='12942008007242012052919').fetch() + """ + if hasattr(self, "alternateDeviceId"): + self._submit("lookupRequestData", "FetchIOSDiagnostic", "diagnosticTestData") + else: + self._submit("lookupRequestData", "FetchRepairDiagnostic", "FetchRepairDiagnosticResponse") + + return self._req.objects[0] + + def events(self): + """ + The Fetch Diagnostic Event Numbers API allows users to retrieve all + diagnostic event numbers associated with provided input + (serial number or alternate device ID). + """ + self._submit("lookupRequestData", "FetchDiagnosticEventNumbers", "diagnosticEventNumbers") + return self._req.objects diff --git a/gsxws/escalations.py b/gsxws/escalations.py new file mode 100644 index 0000000..3cef759 --- /dev/null +++ b/gsxws/escalations.py @@ -0,0 +1,18 @@ +class Escalation(GsxObject): + def create(self): + """ + The Create General Escalation API allows users to create + a general escalation in GSX. The API was earlier known as GSX Help. + """ + dt = self._make_type("ns1:createGenEscRequestType") + dt.escalationRequest = self.data + return self.submit("CreateGeneralEscalation", dt, "escalationConfirmation") + + def update(self): + """ + The Update General Escalation API allows Depot users to + update a general escalation in GSX. + """ + dt = self._make_type("ns1:updateGeneralEscRequestType") + dt.escalationRequest = self.data + return self.submit("UpdateGeneralEscalation", dt, "escalationConfirmation") diff --git a/langs.json b/gsxws/langs.json index 560a595..55dfa2c 100644 --- a/langs.json +++ b/gsxws/langs.json @@ -37,5 +37,11 @@ "ja_XXX": {"df": "YYYY/MM/DD", "tf": "HH:MM"}, "ko_XXX": {"df": "MM/DD/YY", "tf": "HH:MM A"}, "zf_XXX": {"df": "MM/DD/YY", "tf": "HH:MM A"}, - "zh_XXX": {"df": "DD-MM-YY", "tf": "HH:MM A"} + "zh_XXX": {"df": "DD-MM-YY", "tf": "HH:MM A"}, + "pt_BR": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "pt_XXX": {"df": "DD.MM.YYYY", "tf": "HH:MM A"}, + "tr_TR": {"df": "DD.MM.YYYY", "tf": "HH:MM"}, + "tr_XXX": {"df": "DD.MM.YYYY", "tf": "HH:MM"}, + "ru_RU": {"df": "DD.MM.YYYY", "tf": "HH:MM"}, + "ru_XXX": {"df": "DD.MM.YYYY", "tf": "HH:MM"} } diff --git a/gsxws/lookups.py b/gsxws/lookups.py new file mode 100644 index 0000000..0965328 --- /dev/null +++ b/gsxws/lookups.py @@ -0,0 +1,64 @@ +import sys +import base64 +import logging +import tempfile +from datetime import date + +from core import GsxObject + +class Lookup(GsxObject): + def __init__(self, *args, **kwargs): + super(Lookup, self).__init__(*args, **kwargs) + self._namespace = "asp:" + + def lookup(self, method, response="lookupResponseData"): + return self._submit("lookupRequestData", method, response) + + def parts(self): + """ + The Parts Lookup API allows users to access part and part pricing data prior to + creating a repair or order. Parts lookup is also a good way to search for + part numbers by various attributes of a part + (config code, EEE code, serial number, etc.). + """ + self._namespace = "core:" + return self.lookup("PartsLookup", "parts") + + def repairs(self): + """ + The Repair Lookup API mimics the front-end repair search functionality. + It fetches up to 2500 repairs in a given criteria. + Subsequently, the extended Repair Status API can be used + to retrieve more details of the repair. + """ + return self.lookup("RepairLookup") + + def invoices(self): + """ + The Invoice ID Lookup API allows AASP users + to fetch the invoice generated for last 24 hrs + + >>> Lookup(shipTo=677592, invoiceDate=date(2012,2,6)).invoices().invoiceID + '9670348809' + """ + return self.lookup("InvoiceIDLookup") + + def invoice_details(self): + """ + The Invoice Details Lookup API allows AASP users to + download invoice for a given invoice id. + >>> Lookup(invoiceID=9670348809).invoice_details() + """ + result = self.lookup("InvoiceDetailsLookup") + pdf = base64.b64decode(result.invoiceData) + outfile = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) + outfile.write(pdf) + result.invoiceData = outfile.name + return result + +if __name__ == '__main__': + import sys + import doctest + logging.basicConfig(level=logging.DEBUG) + connect(*sys.argv[1:4]) + doctest.testmod() diff --git a/gsxws/orders.py b/gsxws/orders.py new file mode 100644 index 0000000..1687931 --- /dev/null +++ b/gsxws/orders.py @@ -0,0 +1,20 @@ +class Order(GsxObject): + def __init__(self, type='stocking', *args, **kwargs): + super(Order, self).__init__(*args, **kwargs) + self.data['orderLines'] = list() + + def add_part(self, part_number, quantity): + self.data['orderLines'].append({ + 'partNumber': part_number, 'quantity': quantity + }) + + def submit(self): + dt = CLIENT.factory.create('ns1:createStockingOrderRequestType') + dt.userSession = SESSION + dt.orderData = self.data + + try: + result = CLIENT.service.CreateStockingOrder(dt) + return result.orderConfirmation + except suds.WebFault, e: + raise GsxError(fault=e) diff --git a/gsxws/parts.py b/gsxws/parts.py new file mode 100644 index 0000000..8240096 --- /dev/null +++ b/gsxws/parts.py @@ -0,0 +1,25 @@ +import urllib +import tempfile +from core import GsxObject, GsxError + + +class Part(GsxObject): + def lookup(self): + lookup = Lookup(**self.data) + return lookup.parts() + + def fetch_image(self): + """ + Tries the fetch the product image for this service part + """ + if self.partNumber is None: + raise GsxError("Cannot fetch part image without part number") + + image = '%s_350_350.gif' % self.partNumber + url = 'https://km.support.apple.com.edgekey.net/kb/imageService.jsp?image=%s' % image + tmpfile = tempfile.mkstemp(suffix=image) + + try: + return urllib.urlretrieve(url, tmpfile[1])[0] + except Exception, e: + raise GsxError('Failed to fetch part image: %s' % e) diff --git a/gsxws/products.py b/gsxws/products.py new file mode 100644 index 0000000..176f420 --- /dev/null +++ b/gsxws/products.py @@ -0,0 +1,103 @@ +""" +https://gsxwsut.apple.com/apidocs/ut/html/WSAPIChangeLog.html?user=asp +""" + +import sys +import urllib + +import logging +from lookups import Lookup +from diagnostics import Diagnostics +from core import GsxObject, GsxError + + +class Product(GsxObject): + "Something serviceable made by Apple" + _namespace = "glob:" + + def model(self): + """ + Returns the model description of this Product + + >>> Product(serialNumber='DGKFL06JDHJP').model().configDescription + 'iMac (27-inch, Mid 2011)' + """ + result = self._submit("productModelRequest", "FetchProductModel") + + self.configDescription = result.configDescription + self.productLine = result.productLine + self.configCode = result.configCode + return result + + def warranty(self): + """ + The Warranty Status API retrieves the same warranty details + displayed on the GSX Coverage screen. + If part information is provided, the part warranty information is returned. + If you do not provide the optional part information in the + warranty status request, the unit level warranty information is returned. + + >>> Product('DGKFL06JDHJP').warranty().warrantyStatus + 'Out Of Warranty (No Coverage)' + """ + self._submit("unitDetail", "WarrantyStatus", "warrantyDetailInfo") + self.warrantyDetails = self._req.objects[0] + return self.warrantyDetails + + def parts(self): + """ + >>> Product('DGKFL06JDHJP').parts() # doctest: +ELLIPSIS + [<core.GsxObject object at ... + """ + return Lookup(serialNumber=self.serialNumber).parts() + + def repairs(self): + """ + >>> Product(serialNumber='DGKFL06JDHJP').repairs() # doctest: +ELLIPSIS + <core.GsxObject object at ... + """ + return Lookup(serialNumber=self.serialNumber).repairs() + + def diagnostics(self): + """ + >>> Product('DGKFL06JDHJP').diagnostics() + """ + diags = Diagnostics(serialNumber=self.serialNumber) + return diags.fetch() + + def fetch_image(self): + """ + >>> Product('DGKFL06JDHJP').warranty().fetch_image() + """ + if not hasattr(self, "imageURL"): + raise GsxError("No URL to fetch product image") + + try: + result = urllib.urlretrieve(self.imageURL) + return result[0] + except Exception, e: + raise GsxError("Failed to fetch product image: %s" % e) + + def get_activation(self): + """ + The Fetch iOS Activation Details API is used to + fetch activation details of iOS Devices. + + >>> Product('013348005376007').get_activation().unlocked + 'true' + >>> Product('W874939YX92').get_activation().unlocked # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + GsxError: Provided serial number does not belong to an iOS Device... + """ + self._namespace = "glob:" + act = self._submit("FetchIOSActivationDetailsRequest", "FetchIOSActivationDetails") + return act + + +if __name__ == '__main__': + import doctest + from core import connect + logging.basicConfig(level=logging.DEBUG) + connect(*sys.argv[1:4]) + doctest.testmod() diff --git a/gsxws/repairs.py b/gsxws/repairs.py new file mode 100644 index 0000000..27feca8 --- /dev/null +++ b/gsxws/repairs.py @@ -0,0 +1,257 @@ +"gsxws/repairs.py" +import re +import sys +import logging + +from core import GsxObject +from lookups import Lookup + + +class Customer(GsxObject): + """ + Customer address for GSX + + >>> Customer(adressLine1='blaa')._data + {'adressLine1': 'blaa'} + """ + adressLine1 = "" + city = "" + country = "" + firstName = "" + lastName = "" + primaryPhone = "" + region = "" + state = "ZZ" + zipCode = "" + emailAddress = "" + + +class RepairOrderLine(GsxObject): + partNumber = "" + partNumber = "" + comptiaCode = "" + comptiaModifier = "" + + +class Repair(GsxObject): + """ + Base class for the different GSX Repair types + + >>> Repair(repairStatus='Open').lookup() #doctest: +ELLIPSIS + [<core.GsxObject object at ... + >>> Repair('G135773004').details() #doctest: +ELLIPSIS + <core.GsxObject object at ... + >>> Repair('G135773004').status().repairStatus + 'Closed and Completed' + """ + customerAddress = None + symptom = "" + diagnosis = "" + notes = "" + purchaseOrderNumber = "" + referenceNumber = "" + requestReview = False + serialNumber = "" + unitReceivedDate = "" + unitReceivedTime = "" + orderLines = [] + + _namespace = "core:" + + TYPES = ( + ('CA', "Carry-In/Non-Replinished"), + ('NE', "Return Before Replace"), + ('NT', "No Trouble Found"), + ('ON', "Onsite (Indirect/Direct)"), + ('RR', "Repair Or Replace/Whole Unit Mail-In"), + ('WH', "Mail-In"), + ) + + def __init__(self, number=None, **kwargs): + super(Repair, self).__init__(**kwargs) + + if number is not None: + self.dispatchId = number + + def update_sn(self, parts): + """ + Description + The Update Serial Number API allows the service providers to update + the module serial numbers. + + Context: + The API is not applicable for whole unit replacement + serial number entry (see KGB serial update). + """ + self._namespace = "asp:" + + self.partInfo = parts + if hasattr(self, "dispatchId"): + self.repairConfirmationNumber = self.dispatchId + + self._submit("repairData", "UpdateSerialNumber", "repairConfirmation") + return self._req.objects[0] + + def update_kgb_sn(self, sn): + """ + Description: + The KGB Serial Number Update API is always to be used on + whole unit repairs that are in a released state. + This API allows users to provide the KGB serial number for the + whole unit exchange repairs. It also checks for the privilege + to create/ update whole unit exchange repairs + before updating the whole unit exchange repair. + + Context: + The API is to be used on whole unit repairs that are in a released state. + This API can be invoked only after carry-in repair creation API. + """ + self._namespace = "asp:" + + self.serialNumber = sn + self.repairConfirmationNumber = self.dispatchId + + self._submit("UpdateKGBSerialNumberRequest", "UpdateKGBSerialNumber", + "UpdateKGBSerialNumberResponse") + + return self._req.objects[0] + + def lookup(self): + """ + Description: + The Repair Lookup API mimics the front-end repair search functionality. + It fetches up to 2500 repairs in a given criteria. + Subsequently, the extended Repair Status API can be used + to retrieve more details of the repair. + """ + return Lookup(**self._data).repairs() + + def delete(self): + """ + The Delete Repair API allows the service providers to delete + the existing GSX Initiated Carry-In, Return Before Replace & Onsite repairs + which are in Declined-Rejected By TSPS Approver state, + that do not have an active repair id. + """ + pass + + def mark_complete(self, numbers=None): + """ + The Mark Repair Complete API allows a single or an array of + repair confirmation numbers to be submitted to GSX to be marked as complete. + """ + self._namespace = "asp:" + self.repairConfirmationNumbers = numbers or self.dispatchId + self._submit("MarkRepairCompleteRequest", "MarkRepairComplete", + "MarkRepairCompleteResponse") + return self._req.objects[0] + + def status(self, numbers=None): + """ + The Repair Status API retrieves the status + for the submitted repair confirmation number(s). + """ + self._namespace = "asp:" + self.repairConfirmationNumbers = self.dispatchId + status = self._submit("RepairStatusRequest", "RepairStatus", "repairStatus")[0] + self.repairStatus = status.repairStatus + self._status = status + return status + + def details(self): + """ + The Repair Details API includes the shipment information + similar to the Repair Lookup API. + """ + details = self._submit("RepairDetailsRequest", "RepairDetails", "lookupResponseData") + + # fix tracking URL if available + for i, p in enumerate(details.partsInfo): + try: + url = re.sub('<<TRKNO>>', p.deliveryTrackingNumber, p.carrierURL) + details.partsInfo[i].carrierURL = url + except AttributeError: + pass + + self.details = details + return details + + +class CannotDuplicateRepair(Repair): + """ + The Create CND Repair API allows Service Providers to create a repair + whenever the reported issue cannot be duplicated, and the repair + requires no parts replacement. + N01 Unable to Replicate + N02 Software Update/Issue + N03 Cable/Component Reseat + N05 SMC Reset + N06 PRAM Reset + N07 Third Party Part + N99 Other + """ + + +class CarryInRepair(Repair): + """ + GSX validates the information and if all of the validations go through, + it obtains a quote for the repair and creates the carry-in repair + + >>> CarryInRepair(customerAddress=Customer(firstName='Filipp'))._data + """ + shipTo = "" + fileName = "" + fileData = "" + diagnosedByTechId = "" + + def create(self): + """ + GSX validates the information and if all of the validations go through, + it obtains a quote for the repair and creates the carry-in repair. + """ + dt = self._make_type('ns2:carryInRequestType') + dt.repairData = self.data + + return self.submit('CreateCarryInRepair', dt, 'repairConfirmation') + + def update(self, newdata): + """ + Description + The Update Carry-In Repair API allows the service providers + to update the existing open carry-in repairs. + This API assists in addition/deletion of parts and addition of notes + to a repair. On successful update, the repair confirmation number and + quote for any newly added parts would be returned. + In case of any validation error or unsuccessful update, a fault code is issued. + + Carry-In Repair Update Status Codes: + AWTP Awaiting Parts + AWTR Parts Allocated + BEGR In Repair + RFPU Ready for Pickup + """ + dt = self._make_type('ns1:updateCarryInRequestType') + + # Merge old and new data (old data should have Dispatch ID) + dt.repairData = dict(self.data.items() + newdata.items()) + + return self.submit('CarryInRepairUpdate', dt, 'repairConfirmation') + + +class IndirectOnsiteRepair(Repair): + """ + The Create Indirect Onsite Repair API is designed to create the indirect onsite repairs. + When a service provider travels to the customer location to perform repair + on a unit eligible for onsite service, they create an indirect repair. + Once the repair is submitted, it is assigned a confirmation number, + which is a reference number to identify the repair. + """ + pass + + +if __name__ == '__main__': + import doctest + from core import connect + logging.basicConfig(level=logging.DEBUG) + connect(*sys.argv[1:4]) + doctest.testmod() diff --git a/returns.py b/gsxws/returns.py index 4584b3e..4584b3e 100644 --- a/returns.py +++ b/gsxws/returns.py diff --git a/lookups.py b/lookups.py deleted file mode 100644 index c13011e..0000000 --- a/lookups.py +++ /dev/null @@ -1,7 +0,0 @@ -from repairs import GsxObject - -class Lookup(GsxObject): - """docstring for Lookup""" - def __init__(self, arg): - super(Lookup, self).__init__() - self.arg = arg diff --git a/products.py b/products.py deleted file mode 100644 index dc878f8..0000000 --- a/products.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -import suds -from gsxws import connect, GsxError -from repairs import GsxObject -from lookups import Lookup - - -class GsxRequest(object): - def submit(self, method, data, attr=None): - "Submits the SOAP envelope" - from gsxws import CLIENT, SESSION - f = getattr(CLIENT.service, method) - - try: - result = f(data) - return getattr(result, attr) if attr else result - except suds.WebFault, e: - raise GsxError(fault=e) - - -class Product(GsxObject, GsxRequest): - "Something serviceable that Apple makes" - serialNumber = "" - alternateDeviceId = "" - configDescription = "" - - def model(self): - """ - Returns the model description of this Product - - >>> Product(serialNumber='DGKFL06JDHJP').model().configDescription - iMac (27-inch, Mid 2011) - """ - dt = self._make_type("ns3:fetchProductModelRequestType") - dt.productModelRequest = self.data - result = self.submit('FetchProductModel', dt, "productModelResponse")[0] - self.configDescription = result.configDescription - self.productLine = result.productLine - self.configCode = result.configCode - return result - - def warranty(self): - """ - The Warranty Status API retrieves the same warranty details - displayed on the GSX Coverage screen. - If part information is provided, the part warranty information is returned. - If you do not provide the optional part information in the - warranty status request, the unit level warranty information is returned. - - >>> Product(serialNumber='DGKFL06JDHJP').warranty().warrantyStatus - Out Of Warranty (No Coverage) - """ - dt = self._make_type("ns3:warrantyStatusRequestType") - dt.unitDetail = self.data - result = self.submit("WarrantyStatus", dt, "warrantyDetailInfo") - return result - - @property - def parts(self): - pass - -if __name__ == '__main__': - import doctest - connect(*sys.argv[1:4]) - doctest.testmod() diff --git a/repairs.py b/repairs.py deleted file mode 100644 index 2698664..0000000 --- a/repairs.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -gsxws/repairs.py -""" -import sys -from gsxws import connect - - -class GsxObject(object): - - data = {} - - def __init__(self, **kwargs): - self.data = kwargs - - def _make_type(self, new_dt): - """ - Creates the top-level datatype for the API call - """ - from gsxws import CLIENT, SESSION - dt = CLIENT.factory.create(new_dt) - - if SESSION: - dt.userSession = SESSION - - return dt - - -class Customer(GsxObject): - """ - Customer address for GSX - - >>> Customer(adressLine1='blaa').data - {'adressLine1': 'blaa'} - """ - adressLine1 = "" - city = "" - country = "" - firstName = "" - lastName = "" - primaryPhone = "" - region = "" - state = "ZZ" - zipCode = "" - emailAddress = "" - - -class RepairOrderLine(GsxObject): - partNumber = "" - partNumber = "" - comptiaCode = "" - comptiaModifier = "" - - -class Repair(GsxObject): - """ - Abstract base class for the different GSX Repair types - """ - customerAddress = None - symptom = "" - diagnosis = "" - notes = "" - purchaseOrderNumber = "" - referenceNumber = "" - requestReview = False - serialNumber = "" - unitReceivedDate = "" - unitReceivedTime = "" - - orderLines = [] - - TYPES = ( - ('CA', "Carry-In/Non-Replinished"), - ('NE', "Return Before Replace"), - ('NT', "No Trouble Found"), - ('ON', "Onsite (Indirect/Direct)"), - ('RR', "Repair Or Replace/Whole Unit Mail-In"), - ('WH', "Mail-In"), - ) - - def get_data(self): - return {'repairData': self.data} - - def lookup(self): - pass - - -class CarryInRepair(Repair): - """ - GSX validates the information and if all of the validations go through, - it obtains a quote for the repair and creates the carry-in repair - - >>> CarryInRepair(customerAddress=Customer(firstName='Filipp')).get_data() - {} - """ - shipTo = "" - fileName = "" - fileData = "" - diagnosedByTechId = "" - - -class IndirectOnsiteRepair(Repair): - """ - The Create Indirect Onsite Repair API is designed to create the indirect onsite repairs. - When a service provider travels to the customer location to perform repair - on a unit eligible for onsite service, they create an indirect repair. - Once the repair is submitted, it is assigned a confirmation number, - which is a reference number to identify the repair. - """ - pass - - -if __name__ == '__main__': - import doctest - connect(*sys.argv[1:4]) - doctest.testmod() |