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 --- README.md | 3 +- comptia.json | 297 -------------- gsxws.py | 1116 -------------------------------------------------- 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 +++++++ langs.json | 41 -- lookups.py | 7 - products.py | 65 --- repairs.py | 115 ------ returns.py | 156 ------- 23 files changed, 1600 insertions(+), 1799 deletions(-) delete mode 100644 comptia.json delete mode 100755 gsxws.py 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 delete mode 100644 langs.json delete mode 100644 lookups.py delete mode 100644 products.py delete mode 100644 repairs.py delete mode 100644 returns.py diff --git a/README.md b/README.md index 3db385b..2cf2d36 100644 --- a/README.md +++ b/README.md @@ -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/comptia.json b/comptia.json deleted file mode 100644 index 92d6f1c..0000000 --- a/comptia.json +++ /dev/null @@ -1,297 +0,0 @@ -{ - "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.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: 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('<>', 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 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 diff --git a/langs.json b/langs.json deleted file mode 100644 index 560a595..0000000 --- a/langs.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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"} -} 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() diff --git a/returns.py b/returns.py deleted file mode 100644 index 4584b3e..0000000 --- a/returns.py +++ /dev/null @@ -1,156 +0,0 @@ -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