From aaacaebb861beaf2ef39b6bc54db2d12262e9b0d Mon Sep 17 00:00:00 2001 From: Filipp Lepalaan Date: Sun, 12 May 2013 22:34:53 +0300 Subject: More speed, more power, less suds, WIP --- gsxws/__init__.py | 0 gsxws/comms.py | 13 ++ gsxws/comptia.json | 297 ++++++++++++++++++++++++++++++++ gsxws/comptia.py | 89 ++++++++++ gsxws/content.py | 12 ++ gsxws/core.py | 469 +++++++++++++++++++++++++++++++++++++++++++++++++++ gsxws/diagnostics.py | 29 ++++ gsxws/escalations.py | 18 ++ gsxws/langs.json | 47 ++++++ gsxws/lookups.py | 64 +++++++ gsxws/orders.py | 20 +++ gsxws/parts.py | 25 +++ gsxws/products.py | 103 +++++++++++ gsxws/repairs.py | 257 ++++++++++++++++++++++++++++ gsxws/returns.py | 156 +++++++++++++++++ 15 files changed, 1599 insertions(+) create mode 100644 gsxws/__init__.py create mode 100644 gsxws/comms.py create mode 100644 gsxws/comptia.json create mode 100644 gsxws/comptia.py create mode 100644 gsxws/content.py create mode 100644 gsxws/core.py create mode 100644 gsxws/diagnostics.py create mode 100644 gsxws/escalations.py create mode 100644 gsxws/langs.json create mode 100644 gsxws/lookups.py create mode 100644 gsxws/orders.py create mode 100644 gsxws/parts.py create mode 100644 gsxws/products.py create mode 100644 gsxws/repairs.py create mode 100644 gsxws/returns.py (limited to 'gsxws') diff --git a/gsxws/__init__.py b/gsxws/__init__.py new file mode 100644 index 0000000..e69de29 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/gsxws/comptia.json b/gsxws/comptia.json new file mode 100644 index 0000000..92d6f1c --- /dev/null +++ b/gsxws/comptia.json @@ -0,0 +1,297 @@ +{ + "E": { + "E01": "Repairable accidental damage (OOW) - Customer Satisfaction codes", + "E02": "Controls not responding (wheel-button-touch screen)", + "E03": "Any Battery issue", + "E04": "Any Display issue", + "E05": "Input Output issue (audio-video-USB-WiFi)", + "E06": "Alert messages appearing on screen or computer", + "E07": "Will not boot-dead-no power-unusually hot", + "E08": "Fails Service diagnostics", + "E09": "Issue with accessories" + }, + "B": { + "B01": "Liquid or Corrosion Damage", + "B03": "Any Battery issue", + "B06": "Alert message on screen or computer", + "B07": "No Power-will not boot-unusually warm", + "B08": "No Service - dropped calls", + "B0A": "Any Camera issue", + "B0B": "Any audio issue", + "B0C": "Connectivity (Web-GPS-WiFi-BT)", + "B0D": "Display or image issue", + "B0E": "Multi-touch control", + "B0F": "Button or switch issues", + "B0G": "Enclosure issue", + "B0H": "Sensors (Accelerometer, ALS, Proximity)", + "B0J": "Wired Connection (iTunes or accessory)", + "B0K": "SIM Issue", + "B0L": "Unstable device (resets-crashes-frozen)" + }, + "F": { + "F0A": "LCI - Liquid Contamination Indicator On", + "F0B": "Oxidation/Corrosion - No LCI Activated", + "F1A": "Physical Damege - Enclosure", + "F1B": "Physical Damege - LCD/Display", + "F1C": "Engraving Issue", + "F2A": "Controls Not Responding (button-switch)", + "F2B": "Touch/Multi-Touch Control Issues", + "F2C": "Sensor - Accelerometer, ALS, Gyro, HE Issue", + "F3A": "Battery Issue", + "F4A": "Display issue", + "F5A": "Connectivity - USB", + "F5B": "Cellular Data Issue", + "F5D": "Input Issue - Audio/Video", + "F5E": "Output Issue - Audio/Video", + "F5F": "Camera Issue", + "F5G": "Connectivity - Wifi Issue", + "F5H": "Connectivity - Bluetooth Issue", + "F6A": "Alert Messages On Screen or Computer", + "F7A": "Will Not Boot-Dead-No Power", + "F7B": "Unusually Hot", + "F8A": "Fails Functional Test", + "F9A": "Issue With Accessory" + }, + "5": { + "M01": "No Power/No Light", + "M02": "Has Power/Light But Will Not Boot", + "M03": "Caused No Video", + "M04": "Caused Video Distortion", + "M05": "Hang/Freeze Up", + "M06": "Kernel Panic-Restart Required Message", + "M07": "Memory Errors/Not Recognized", + "M08": "Random Shutdown w/Reset During Use", + "M09": "Caused Audio Issue", + "M10": "Ethernet Port/Device Issue", + "M11": "Airport/Bluetooth", + "M12": "Firewire Port/Device Issues", + "M13": "Caused Camera Issue", + "M14": "Modem Issues", + "M15": "USB Port/Device Issues (Not Power)", + "M16": "Caused Keyboard/Trackpad Issue", + "M17": "I/O Expansion Slot", + "M18": "Caused Fan/Thermal Issue", + "M19": "Cannot Detect Hard/Optical Drive", + "M20": "Does Not Detect/Charge Battery", + "M21": "Won't Detect Working Power Adapter", + "M22": "Sleep/Wake Issue", + "M23": "Sensors Test Failed", + "M24": "Connectors - Broken/Damaged", + "M25": "No Backlight/Has Boot Image", + "M26": "No Video to External Video", + "M27": "SD Card Issue", + "M28": "HDMI Issue", + "M29": "Caused Whole Screen Flicker", + "M30": "No Power/No Adapter LED", + "M31": "Video Distortion On External Display", + "M32": "Thunderbolt Display Functionality Issue", + "M33": "Thunderbolt Port Inoperative", + "M34": "Thunderbolt not passing enough power", + "M35": "Airport Not Recognized", + "M36": "Bluetooth Not Recognized", + "M37": "USB Device Not Detected", + "M38": "USB Port Has Insufficient Power", + "M39": "Boots To 3 Beeps-Memory not recog", + "M85": "Unusually Hot - Overheat", + "M90": "Liquid Spill/LSI Tripped", + "M99": "Un-Categorized Symptom", + "Z03": "MB Pro NVIDIA Issue", + "Z04": "MB Pro NVIDIA Issue (Multiple Issues)", + "Z17": "ATI 2600 XT-No Video", + "Z18": "ATI 2600 XT-No Video/multi-issue", + "Z19": "ATI 2600 XT-Distorted Video", + "Z20": "ATI 2600 XT-Distorted Video/multi-issue", + "Z33": "MBPRO 15 Kernel Panic", + "Z34": "MBPRO 15 Kernel Panic (multi-issue)" + }, + "3": { + "J01": "Optical Drive Won't Accept Optical Media", + "J02": "Optical Drive Won't Eject Optical Media", + "J03": "Optical Media Read/Write Data Error", + "J04": "Optical Drive Noisy", + "J05": "Optical Drive Physical Damage", + "J06": "Optical Video Problems", + "J07": "Optical Drive Not Performing To Spec", + "J08": "Firmware Issues", + "J09": "Optical Drive Not Recognized", + "H01": "Hard Drive Not Recognized/Mount", + "H02": "Hard Drive Can't Boot", + "H03": "Hard Drive Read/Write Problem", + "H04": "Drive - Pins/Connector bent/broken", + "H05": "Hard Drive Bad Sector/Defective", + "H06": "Hard Drive Operational But Noisy", + "H07": "Hard Drive Formatting Issues", + "H08": "Hard Drive Firmware Issues", + "H90": "Liquid Spill/LSI Tripped", + "H99": "Un-Categorized Symptom", + "Z29": "iMac (Mid 2011) Hard Drive Program", + "Z30": "iMac (Mid 2011) Hard Drive Program (multiple issues)" + }, + "9": { + "N01": "No Power/Dead Unit", + "N02": "TimeCapsule - Internal HDD Not Mounting/Seen", + "N03": "Overheating/Fan in Full Speed", + "N04": "No/Poor WiFi Signal", + "N05": "Backup Issues", + "N06": "Can't Configure or Upgrade Firmware", + "N07": "Amber LED Flashes", + "N08": "USB Connection Issue/Ext USB Devices", + "N09": "Random Disconnect/Network Connection Issues", + "N10": "Wireless Distribution Setup Issue", + "N11": "Audio Issue", + "N13": "Kernel Panic/Freeze", + "N14": "Performance Issue/Slow Connection", + "N15": "Bluetooth Issue", + "N16": "Modem - Defective", + "N17": "Mechanical Damage/Cosmetic Issues", + "N18": "Airport Card - Not Recognized", + "N19": "Can't Connect", + "N20": "Firmware Update/Restore Issue", + "N21": "Cosmetic Issue", + "N85": "Unusually Hot - Overheat", + "N90": "Liquid Spill/LSI Tripped", + "N99": "Un-Categorized Symptom", + "Z13": "Time Capsule Power Supply", + "Z14": "TC PS Multiple Issues" + }, + "8": { + "T01": "No Power/Dead Unit", + "T02": "AppleTV - No Video Output", + "T03": "AppleTV - Distorted Video", + "T04": "AppleTV - No/Poor Wireless Signal", + "T05": "AppleTV - Won't Sync", + "T06": "AppleTV - No Audio", + "T07": "AppleTV - No Audio in HDMI", + "T08": "Won't Boot Up", + "T09": "System Hang/Freeze Up", + "T10": "Distorted or Cracking Audio", + "T11": "Unusually Hot - Overheat", + "T13": "Mechanical/Cosmetic Issue", + "T14": "Ethernet Connectivity Issue", + "T15": "Wireless (Wi-Fi) Connectivity Issue", + "T16": "Cosmetic Issue", + "T90": "Liquid Spill/LSI Tripped", + "T99": "Un-Categorized Symptom" + }, + "4": { + "K01": "Specific Key(s) Do Not Work", + "K02": "No Mouse/Trackpad Response", + "K03": "Built-In Keyboard Locks Up", + "K04": "Wrong Keyboard Language", + "K05": "Sticky Keys", + "K06": "Defective Mouse Jogball", + "K07": "Wireless Input Device - Can't Pair", + "K08": "Wireless Input Device - Lost Connection", + "K09": "Wireless Keyboard - No Green LED/No Power", + "K10": "Built-In Keyboard - No/Dim Backlight", + "K11": "Built-In Keyboard - Not Recognized", + "K12": "Trackpad Cursor Not Tracking Properly", + "K13": "Trackpad Click Not Recognized", + "K14": "Mouse Clicking Issue", + "K15": "Device Not Recognized", + "K16": "Mechanical/Physical Damage", + "K17": "Key Caps - Wrong/Missing/Fall Off", + "K18": "Touch/Multi-Touch Gesture Issue", + "K19": "Power Button Issue", + "K20": "Power Issue, Not due to Power Button", + "K21": "Cosmetic Issue", + "K22": "Port Functionality Issue", + "K23": "Trackpad Cursor Not Responding", + "K24": "Trackpad Requires High Click Force", + "K25": "Trackpad Click Oversensitive", + "K26": "Mouse Issue", + "K27": "Key(s) Missing/Falling Off", + "K28": "Backlight Uneven Across Keyboard", + "K90": "Liquid Spill/LSI Tripped", + "K99": "Un-Categorized Symptom", + "Z07": "MB Top Case Cracking", + "Z08": "MB Top Case Cracking (Multiple Issues)" + }, + "2": { + "L01": "No Power/Power Light Issue", + "L02": "Incorrect Colors Or Tinting", + "L03": "Has Power/Blank/No Video", + "L04": "Distorted/Blurred/Non-Focus Video", + "L05": "Vertical/Horizontal Lines", + "L06": "Full Screen Flicker/Flash", + "L07": "Can't Control Brightness", + "L09": "No Backlight/Has Video", + "L10": "Can't Change Resolution", + "L11": "Built In Audio Device Problem", + "L14": "Connector/Port/Cable Issue", + "L15": "Sleep Function Not Working", + "L16": "Wireless Function Not Working", + "L17": "Camera Image/Detect Issue", + "L18": "Mechanical/Physical Damages", + "L19": "Cosmetic Defects", + "L20": "Dead Pixels/Foreign Material", + "L21": "Bad Spots (Mura)", + "L22": "Wake Function Issues", + "L23": "Bluetooth Function Not Working", + "L24": "Clamshell Misalignment", + "L25": "Image Sticking/Ghost", + "L26": "Horizontal Lines Or Bands", + "L27": "Vertical Lines Or Bands", + "L90": "Liquid Spill/LSI Tripped", + "L99": "Un-Categorized Symptom", + "Z09": "MBAir Hinge Cracking", + "Z10": "MBAir Hinge Cracking (Multiple Issues)", + "Z35": "LCD Contamination" + }, + "6": { + "P01": "No Power/Dead Unit", + "P02": "PSupply Causes Unexpected Reset/Shutdown", + "P03": "No LED/LED Indicated Errors", + "P04": "Noise/Hum/Vibration", + "P05": "Audio Alarm/Prefailure Notice", + "P06": "Power Supply - Fan Not Working/Noisy", + "P07": "Wrong Voltage Selector", + "P08": "Burnt Smell/Odor", + "P09": "Battery Runtime Too Short, Fails Diag", + "P10": "Battery Won't Charge At All", + "P11": "Battery - Not Recognized", + "P12": "Battery Recognized-Won't Run Unit", + "P13": "Battery - Leakage/Swollen", + "P14": "Adapter - Won't Run on AC alone", + "P15": "Adapter Pins Stuck/Broken/Burnt", + "P16": "Mechanical - Connector/Cable/Duckhead Damaged", + "P17": "Unusually Hot - Overheat", + "P18": "Out-of-Warranty Battery Replacement", + "P19": "Battery Diagnostic Reported Failure", + "P21": "Cosmetic Issue", + "P22": "Battery Runtime Too Short, Passes Diag", + "P23": "Adapter No Power/Not Damaged", + "P90": "Liquid Spill/LSI Tripped", + "P99": "Un-Categorized Symptom", + "Z25": "Battery single part repair", + "Z26": "Battery multi-part repair" + }, + "1": { + "X01": "Memory Caused Kernel Panic", + "X02": "Memory Caused No Boot", + "X03": "Cables - Defective", + "X04": "Remote - Inoperable", + "X05": "Remote - Battery Life Too Short", + "X06": "Memory Module (RAM) - Issues", + "X08": "Internal Speaker - No Audio", + "X09": "Internal Speaker - Distorted Audio/Sound", + "X10": "Thermal Module Defective", + "X12": "Enclosure - Defective Latch/Hinge", + "X13": "Enclosure - Mechanical/Cosmetic Damaged", + "X14": "Enclosure - Reset/Power Button Stuck", + "X15": "Enclosure - Wobble/Uneven", + "X17": "Remote - Specific Button Not Working", + "X19": "Microphone - Defective", + "X20": "Camera - Video/Image Distortion", + "X21": "Camera - No Video", + "X22": "Fan Dead", + "X23": "Fan Sound Abnormal (tick, whine, grind)", + "X24": "Interface Card/Cage Issue", + "X26": "Damaged Smart Cable", + "X27": "Thunderbolt Firmware Update", + "X90": "Liquid Spill/LSI Tripped", + "X99": "Un-Categorized Symptom", + "Z21": "Bottom Case Delamination", + "Z22": "Bottom Case Delamination multiple issues" + } +} 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/gsxws/langs.json b/gsxws/langs.json new file mode 100644 index 0000000..55dfa2c --- /dev/null +++ b/gsxws/langs.json @@ -0,0 +1,47 @@ +{ + "en_CA": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "en_GB": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "en_US": {"df": "MM/DD/YY", "tf": "HH:MM A"}, + "en_IN": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "en_IE": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "en_AU": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "en_NZ": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "en_ZA": {"df": "YY/MM/DD", "tf": "HH:MM A"}, + "en_HK": {"df": "MM/DD/YY", "tf": "HH:MM A"}, + "en_SG": {"df": "DD-MM-YY", "tf": "HH:MM A"}, + "en_TH": {"df": "DD-MM-YY", "tf": "HH:MM A"}, + "en_XXX": {"df": "%m/%d/%y", "tf": "%I:%M %p"}, + "fr_FR": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "fr_LU": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "fr_CH": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "fr_BE": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "fr_CA": {"df": "YY-MM-DD", "tf": "HH:MM"}, + "fr_XXX": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "it_IT": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "it_CH": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "it_XXX": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "de_CH": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "de_AT": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "de_DE": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "de_DK": {"df": "DD-MM-YY", "tf": "HH:MM"}, + "de_LU": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "de_XXX": {"df": "DD.MM.YY", "tf": "HH:MM"}, + "es_MX": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "es_ES": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "es_CR": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "es_AR": {"df": "DD/MM/YY", "tf": "HH:MM"}, + "es_CL": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "es_EC": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "es_XXX": {"df": "DD/MM/YY", "tf": "HH:MM A"}, + "ja_JP": {"df": "YYYY/MM/DD", "tf": "HH:MM"}, + "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"}, + "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 + [>> Product(serialNumber='DGKFL06JDHJP').repairs() # doctest: +ELLIPSIS + >> 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 + [>> Repair('G135773004').details() #doctest: +ELLIPSIS + >> 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('<>', 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/gsxws/returns.py b/gsxws/returns.py new file mode 100644 index 0000000..4584b3e --- /dev/null +++ b/gsxws/returns.py @@ -0,0 +1,156 @@ +from gsxws import GsxObject + +RETURN_TYPES = ( + (1, "Dead On Arrival"), + (2, "Good Part Return"), + (3, "Convert To Stock"), + (4, "Transfer to Out of Warranty"), +) + +CARRIERS = ( + ('XAER', "Aero 2000"), + ('XAIRBEC', "Airborne"), + ('XAIRB', "Airborne"), + ('XARM', "Aramex"), + ('XOZP', "Australia Post"), + ('XBAX', "BAX GLOBAL PTE LTD"), + ('XCPW', "CPW Internal"), + ('XCL', "Citylink"), + ('XDHL', "DHL"), + ('XDHLC', "DHL"), + ('XDZNA', "Danzas-AEI"), + ('XEAS', "EAS"), + ('XEGL', "Eagle ASIA PACIFIC HOLDINGS"), + ('XEXXN', "Exel"), + ('XFEDE', "FedEx"), + ('XFDE', "FedEx Air"), + ('XGLS', "GLS-General Logistics Systems"), + ('XHNF', "H and Friends"), + ('XNGLN', "Nightline"), + ('XPL', "Parceline"), + ('XPRLA', "Purolator"), + ('SDS', "SDS An Post"), + ('XSNO', "Seino Transportation Co. Ltd."), + ('XSTE', "Star Track Express"), + ('XTNT', "TNT"), + ('XUPSN', "UPS"), + ('XUTI', "UTi (Japan) K.K."), + ('XYMT', "YAMATO"), +) + +class Return(GsxObject): + 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) + return result -- cgit v1.2.3