aboutsummaryrefslogtreecommitdiffstats
path: root/gsxws
diff options
context:
space:
mode:
authorFilipp Lepalaan <f@230.to>2013-05-12 22:34:53 +0300
committerFilipp Lepalaan <f@230.to>2013-05-12 22:34:53 +0300
commitaaacaebb861beaf2ef39b6bc54db2d12262e9b0d (patch)
tree2d373fc7d04ab03f87bfe5e4d13f36d6d7bc81a5 /gsxws
parent452005bbb83059913d4c8b7648d9e368936e53da (diff)
downloadpy-gsxws-aaacaebb861beaf2ef39b6bc54db2d12262e9b0d.tar.gz
py-gsxws-aaacaebb861beaf2ef39b6bc54db2d12262e9b0d.tar.bz2
py-gsxws-aaacaebb861beaf2ef39b6bc54db2d12262e9b0d.zip
More speed, more power, less suds, WIP
Diffstat (limited to 'gsxws')
-rw-r--r--gsxws/__init__.py0
-rw-r--r--gsxws/comms.py13
-rw-r--r--gsxws/comptia.json297
-rw-r--r--gsxws/comptia.py89
-rw-r--r--gsxws/content.py12
-rw-r--r--gsxws/core.py469
-rw-r--r--gsxws/diagnostics.py29
-rw-r--r--gsxws/escalations.py18
-rw-r--r--gsxws/langs.json47
-rw-r--r--gsxws/lookups.py64
-rw-r--r--gsxws/orders.py20
-rw-r--r--gsxws/parts.py25
-rw-r--r--gsxws/products.py103
-rw-r--r--gsxws/repairs.py257
-rw-r--r--gsxws/returns.py156
15 files changed, 1599 insertions, 0 deletions
diff --git a/gsxws/__init__.py b/gsxws/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gsxws/__init__.py
diff --git a/gsxws/comms.py b/gsxws/comms.py
new file mode 100644
index 0000000..e18193f
--- /dev/null
+++ b/gsxws/comms.py
@@ -0,0 +1,13 @@
+class Communication(GsxObject):
+ def get_content():
+ """
+ The Fetch Communication Content API allows the service providers/depot/carriers
+ to fetch the communication content by article ID from the service news channel.
+ """
+
+ def get_articles():
+ """
+ The Fetch Communication Articles API allows the service partners
+ to fetch all the active communication message IDs.
+ """
+ \ No newline at end of file
diff --git a/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
+ [<core.GsxObject object at ...
+ """
+ return Lookup(serialNumber=self.serialNumber).parts()
+
+ def repairs(self):
+ """
+ >>> Product(serialNumber='DGKFL06JDHJP').repairs() # doctest: +ELLIPSIS
+ <core.GsxObject object at ...
+ """
+ return Lookup(serialNumber=self.serialNumber).repairs()
+
+ def diagnostics(self):
+ """
+ >>> Product('DGKFL06JDHJP').diagnostics()
+ """
+ diags = Diagnostics(serialNumber=self.serialNumber)
+ return diags.fetch()
+
+ def fetch_image(self):
+ """
+ >>> Product('DGKFL06JDHJP').warranty().fetch_image()
+ """
+ if not hasattr(self, "imageURL"):
+ raise GsxError("No URL to fetch product image")
+
+ try:
+ result = urllib.urlretrieve(self.imageURL)
+ return result[0]
+ except Exception, e:
+ raise GsxError("Failed to fetch product image: %s" % e)
+
+ def get_activation(self):
+ """
+ The Fetch iOS Activation Details API is used to
+ fetch activation details of iOS Devices.
+
+ >>> Product('013348005376007').get_activation().unlocked
+ 'true'
+ >>> Product('W874939YX92').get_activation().unlocked # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ GsxError: Provided serial number does not belong to an iOS Device...
+ """
+ self._namespace = "glob:"
+ act = self._submit("FetchIOSActivationDetailsRequest", "FetchIOSActivationDetails")
+ return act
+
+
+if __name__ == '__main__':
+ import doctest
+ from core import connect
+ logging.basicConfig(level=logging.DEBUG)
+ connect(*sys.argv[1:4])
+ doctest.testmod()
diff --git a/gsxws/repairs.py b/gsxws/repairs.py
new file mode 100644
index 0000000..27feca8
--- /dev/null
+++ b/gsxws/repairs.py
@@ -0,0 +1,257 @@
+"gsxws/repairs.py"
+import re
+import sys
+import logging
+
+from core import GsxObject
+from lookups import Lookup
+
+
+class Customer(GsxObject):
+ """
+ Customer address for GSX
+
+ >>> Customer(adressLine1='blaa')._data
+ {'adressLine1': 'blaa'}
+ """
+ adressLine1 = ""
+ city = ""
+ country = ""
+ firstName = ""
+ lastName = ""
+ primaryPhone = ""
+ region = ""
+ state = "ZZ"
+ zipCode = ""
+ emailAddress = ""
+
+
+class RepairOrderLine(GsxObject):
+ partNumber = ""
+ partNumber = ""
+ comptiaCode = ""
+ comptiaModifier = ""
+
+
+class Repair(GsxObject):
+ """
+ Base class for the different GSX Repair types
+
+ >>> Repair(repairStatus='Open').lookup() #doctest: +ELLIPSIS
+ [<core.GsxObject object at ...
+ >>> Repair('G135773004').details() #doctest: +ELLIPSIS
+ <core.GsxObject object at ...
+ >>> Repair('G135773004').status().repairStatus
+ 'Closed and Completed'
+ """
+ customerAddress = None
+ symptom = ""
+ diagnosis = ""
+ notes = ""
+ purchaseOrderNumber = ""
+ referenceNumber = ""
+ requestReview = False
+ serialNumber = ""
+ unitReceivedDate = ""
+ unitReceivedTime = ""
+ orderLines = []
+
+ _namespace = "core:"
+
+ TYPES = (
+ ('CA', "Carry-In/Non-Replinished"),
+ ('NE', "Return Before Replace"),
+ ('NT', "No Trouble Found"),
+ ('ON', "Onsite (Indirect/Direct)"),
+ ('RR', "Repair Or Replace/Whole Unit Mail-In"),
+ ('WH', "Mail-In"),
+ )
+
+ def __init__(self, number=None, **kwargs):
+ super(Repair, self).__init__(**kwargs)
+
+ if number is not None:
+ self.dispatchId = number
+
+ def update_sn(self, parts):
+ """
+ Description
+ The Update Serial Number API allows the service providers to update
+ the module serial numbers.
+
+ Context:
+ The API is not applicable for whole unit replacement
+ serial number entry (see KGB serial update).
+ """
+ self._namespace = "asp:"
+
+ self.partInfo = parts
+ if hasattr(self, "dispatchId"):
+ self.repairConfirmationNumber = self.dispatchId
+
+ self._submit("repairData", "UpdateSerialNumber", "repairConfirmation")
+ return self._req.objects[0]
+
+ def update_kgb_sn(self, sn):
+ """
+ Description:
+ The KGB Serial Number Update API is always to be used on
+ whole unit repairs that are in a released state.
+ This API allows users to provide the KGB serial number for the
+ whole unit exchange repairs. It also checks for the privilege
+ to create/ update whole unit exchange repairs
+ before updating the whole unit exchange repair.
+
+ Context:
+ The API is to be used on whole unit repairs that are in a released state.
+ This API can be invoked only after carry-in repair creation API.
+ """
+ self._namespace = "asp:"
+
+ self.serialNumber = sn
+ self.repairConfirmationNumber = self.dispatchId
+
+ self._submit("UpdateKGBSerialNumberRequest", "UpdateKGBSerialNumber",
+ "UpdateKGBSerialNumberResponse")
+
+ return self._req.objects[0]
+
+ def lookup(self):
+ """
+ Description:
+ The Repair Lookup API mimics the front-end repair search functionality.
+ It fetches up to 2500 repairs in a given criteria.
+ Subsequently, the extended Repair Status API can be used
+ to retrieve more details of the repair.
+ """
+ return Lookup(**self._data).repairs()
+
+ def delete(self):
+ """
+ The Delete Repair API allows the service providers to delete
+ the existing GSX Initiated Carry-In, Return Before Replace & Onsite repairs
+ which are in Declined-Rejected By TSPS Approver state,
+ that do not have an active repair id.
+ """
+ pass
+
+ def mark_complete(self, numbers=None):
+ """
+ The Mark Repair Complete API allows a single or an array of
+ repair confirmation numbers to be submitted to GSX to be marked as complete.
+ """
+ self._namespace = "asp:"
+ self.repairConfirmationNumbers = numbers or self.dispatchId
+ self._submit("MarkRepairCompleteRequest", "MarkRepairComplete",
+ "MarkRepairCompleteResponse")
+ return self._req.objects[0]
+
+ def status(self, numbers=None):
+ """
+ The Repair Status API retrieves the status
+ for the submitted repair confirmation number(s).
+ """
+ self._namespace = "asp:"
+ self.repairConfirmationNumbers = self.dispatchId
+ status = self._submit("RepairStatusRequest", "RepairStatus", "repairStatus")[0]
+ self.repairStatus = status.repairStatus
+ self._status = status
+ return status
+
+ def details(self):
+ """
+ The Repair Details API includes the shipment information
+ similar to the Repair Lookup API.
+ """
+ details = self._submit("RepairDetailsRequest", "RepairDetails", "lookupResponseData")
+
+ # fix tracking URL if available
+ for i, p in enumerate(details.partsInfo):
+ try:
+ url = re.sub('<<TRKNO>>', p.deliveryTrackingNumber, p.carrierURL)
+ details.partsInfo[i].carrierURL = url
+ except AttributeError:
+ pass
+
+ self.details = details
+ return details
+
+
+class CannotDuplicateRepair(Repair):
+ """
+ The Create CND Repair API allows Service Providers to create a repair
+ whenever the reported issue cannot be duplicated, and the repair
+ requires no parts replacement.
+ N01 Unable to Replicate
+ N02 Software Update/Issue
+ N03 Cable/Component Reseat
+ N05 SMC Reset
+ N06 PRAM Reset
+ N07 Third Party Part
+ N99 Other
+ """
+
+
+class CarryInRepair(Repair):
+ """
+ GSX validates the information and if all of the validations go through,
+ it obtains a quote for the repair and creates the carry-in repair
+
+ >>> CarryInRepair(customerAddress=Customer(firstName='Filipp'))._data
+ """
+ shipTo = ""
+ fileName = ""
+ fileData = ""
+ diagnosedByTechId = ""
+
+ def create(self):
+ """
+ GSX validates the information and if all of the validations go through,
+ it obtains a quote for the repair and creates the carry-in repair.
+ """
+ dt = self._make_type('ns2:carryInRequestType')
+ dt.repairData = self.data
+
+ return self.submit('CreateCarryInRepair', dt, 'repairConfirmation')
+
+ def update(self, newdata):
+ """
+ Description
+ The Update Carry-In Repair API allows the service providers
+ to update the existing open carry-in repairs.
+ This API assists in addition/deletion of parts and addition of notes
+ to a repair. On successful update, the repair confirmation number and
+ quote for any newly added parts would be returned.
+ In case of any validation error or unsuccessful update, a fault code is issued.
+
+ Carry-In Repair Update Status Codes:
+ AWTP Awaiting Parts
+ AWTR Parts Allocated
+ BEGR In Repair
+ RFPU Ready for Pickup
+ """
+ dt = self._make_type('ns1:updateCarryInRequestType')
+
+ # Merge old and new data (old data should have Dispatch ID)
+ dt.repairData = dict(self.data.items() + newdata.items())
+
+ return self.submit('CarryInRepairUpdate', dt, 'repairConfirmation')
+
+
+class IndirectOnsiteRepair(Repair):
+ """
+ The Create Indirect Onsite Repair API is designed to create the indirect onsite repairs.
+ When a service provider travels to the customer location to perform repair
+ on a unit eligible for onsite service, they create an indirect repair.
+ Once the repair is submitted, it is assigned a confirmation number,
+ which is a reference number to identify the repair.
+ """
+ pass
+
+
+if __name__ == '__main__':
+ import doctest
+ from core import connect
+ logging.basicConfig(level=logging.DEBUG)
+ connect(*sys.argv[1:4])
+ doctest.testmod()
diff --git a/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