diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..638e9ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +timebay.yaml +build/ +dist/ +tkebay.yaml +ebaysdk.egg-info/ +*.swp +*.pyc +.svn diff --git a/Changes b/Changes index 16caf09..bb28d07 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,117 @@ Changes for ebaysdk -- add proxy support +2.1.0 Mon Jul 7 10:03:36 PDT 2014 +- modify install instructions +- remove grequests install dep from setup.py + +2.0.0 +- WARNING!! This release breaks some backward compatibility + Read https://github.com/timotheus/ebaysdk-python/wiki/Migrating-from-v1-to-v2 +- imports: Modified package structure +- execute(): Modified return value +- Switched to lxml.etree +- Added a new response class + response.reply, response.dom(), response.dict(), response.json() +- Added ebaysdk exception classes +- Modified utils.py (dict2xml, xml2dict) + +1.0.3 +- rework unicode fix +- fix logging handler + +1.0.2 Sat Feb 8 20:44:00 PST 2014 +- fix unicode issue is dict to xml routine +- add a unicode test +- fix http back-end + +1.0.0 +- Major refactor + + changes are backward compatible unless your subclassing any of the + SDK classes + + moved from PyCurl to Python Requests + + code organization + + standard python logging + + add exceptions + +0.1.11 +- Add affiliate headers to the Shopping API back-end +- https://github.com/timotheus/ebaysdk-python/issues/40 + fix datatype issue with error codes from the shopping API + +- added py3 support thanks to Nikolay Derkach +- store response error codes in an array when processing response. + The reponse error codes can be accessed by the api.response_codes() + accessor. + + if api.error(): + if 37 in api.response_codes(): + print "Invalid data" + +0.1.10 +- added error checking for html() class +- all class return response_data regardless of the http status code +- added Merchandising API class +- update SOA class default value +- add more trading api samples + +0.1.9 Wed Apr 24 14:38:40 PDT 2013 +- update error handling +- add VerifyAddItem sample call +- update license in setup.py + +0.1.8 Tue Apr 23 15:33:02 PDT 2013 +- push dist to pypi +- fix deprecation warning in utils.py +- pep8 cleanup +- handle utf-8 when parsing with BeautifulStoneSoup +- add retry to standard & parallel calls +- bug fix for SOA class +- add documentation and sample scripts +- clean up YAML file +- YAML values are now overridden by values defined + when building the object +- remove silent depedency on simplejson, at least for Python >= 2.6 +- fix bug in html class that allows for POST +- refactor and style cleanup +- added parallel support using the parallel class +- created new prepare, process, and _process_http_request methods + +0.1.7 +- update tests +- modify response_obj() to return response_dict() + +0.0.6 +- support older version of Element tree +- modify dict2xml to handle nodes with array content + +for example, +{ + 'keywords': 'shoes', + 'paginationInput': { 'pageNumber': 1 }, + 'itemFilter': [ + { 'name': 'MinBids', 'value': 10 }, + { 'name': 'MaxBids', 'value': 14 } + ], +} + + +MinBids10 +MaxBids14 +shoes +1 + + +0.0.5 +- added response_obj() which turns a dict into a Struct + e.g. { a : { 'name' : 'tim' } } can be accessed like a.name +- add support to dict args in the execute call. execute will now check the + data type and convert to xml if necessary +- clean doctests + +0.0.4 +- moved from httplib to pycurl + * pycurl was chosen because of its https/proxy support and + that it's thread-safe. 0.0.3 Wed May 11 11:03:58 PDT 2011 - fix escape username/password diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..48975dd --- /dev/null +++ b/INSTALL @@ -0,0 +1,61 @@ +Running the tests: +~/> export EBAY_YAML='myebay.yaml'; python setup.py test + +Installing ebaysdk on Mac, Linux, Unix: + +1) Install System Dependancies + + Red Hat: + sudo yum install python-lxml + + Ubuntu: + sudo apt-get install python-lxml + +2) Install the SDK with easy_install + + sudo easy_install ebaysdk + + Or install the latest version from github, + + sudo easy_install https://github.com/timotheus/ebaysdk-python/archive/master.zip + +Installing ebaysdk on Windows: + +1) Download and install the latest release of Python 2.7+ + +http://python.org/download/ + +Choose either "Python 3.3.0 Windows x86 MSI Installer" or "Python +3.3.0 Windows X86-64 MSI Installer". To use the latter, you must be +running a 64-bit version of Windows. + +2) Install setuptools + +First, visit http://pypi.python.org/pypi/setuptools + +If you chose the 32-bit version of Python in step 1, you can simply +use latest setuptools package for "MS Windows installer" for Python +2.7 (example setuptools-0.6c11.win32-py2.7.exe). In this case, simply +download the file and run it to install setuptools. + +If you chose the X86-64 version in step 1, you must download +ez_setup.py from the setuptools page to download and install +setuptools, then run it from the command prompt as follows: + + a. Open the Command Prompt + b. Change to the directory in which ez_setup.py was downloaded + c. Install setuptools as follows: c:\Python27\python.exe ez_setup.py + +The last step assumes that Python was installed to its default +location. + +3) Install ebaysdk + +Download and extract the zipball, or clone the ebaysdk-python +repository. Then open the Command Prompt and change to the root +directory of the distribution and execute: + + c:\Python27\python.exe setup.py install + +If there were no errors, ebaysdk should be ready to use! + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b46c53e --- /dev/null +++ b/LICENSE @@ -0,0 +1,318 @@ +COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + 1. Definitions. + 1.1. "Contributor" means each individual or entity that + creates or contributes to the creation of Modifications. + 1.2. "Contributor Version" means the combination of the + Original Software, prior Modifications used by a + Contributor (if any), and the Modifications made by that + particular Contributor. + 1.3. "Covered Software" means (a) the Original Software, or + (b) Modifications, or (c) the combination of files + containing Original Software with files containing + Modifications, in each case including portions thereof. + 1.4. "Executable" means the Covered Software in any form + other than Source Code. + 1.5. "Initial Developer" means the individual or entity + that first makes Original Software available under this + License. + 1.6. "Larger Work" means a work which combines Covered + Software or portions thereof with code not governed by the + terms of this License. + 1.7. "License" means this document. + 1.8. "Licensable" means having the right to grant, to the + maximum extent possible, whether at the time of the initial + grant or subsequently acquired, any and all of the rights + conveyed herein. + 1.9. "Modifications" means the Source Code and Executable + form of any of the following: + A. Any file that results from an addition to, + deletion from or modification of the contents of a + file containing Original Software or previous + Modifications; + B. Any new file that contains any part of the + Original Software or previous Modification; or + C. Any new file that is contributed or otherwise made + available under the terms of this License. + 1.10. "Original Software" means the Source Code and + Executable form of computer software code that is + originally released under this License. + 1.11. "Patent Claims" means any patent claim(s), now owned + or hereafter acquired, including without limitation, + method, process, and apparatus claims, in any patent + Licensable by grantor. + 1.12. "Source Code" means (a) the common form of computer + software code in which modifications are made and (b) + associated documentation included in or with such code. + 1.13. "You" (or "Your") means an individual or a legal + entity exercising rights under, and complying with all of + the terms of, this License. For legal entities, "You" + includes any entity which controls, is controlled by, or is + under common control with You. For purposes of this + definition, "control" means (a) the power, direct or + indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (b) ownership + of more than fifty percent (50%) of the outstanding shares + or beneficial ownership of such entity. + 2. License Grants. + 2.1. The Initial Developer Grant. + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, the + Initial Developer hereby grants You a world-wide, + royalty-free, non-exclusive license: + (a) under intellectual property rights (other than + patent or trademark) Licensable by Initial Developer, + to use, reproduce, modify, display, perform, + sublicense and distribute the Original Software (or + portions thereof), with or without Modifications, + and/or as part of a Larger Work; and + (b) under Patent Claims infringed by the making, + using or selling of Original Software, to make, have + made, use, practice, sell, and offer for sale, and/or + otherwise dispose of the Original Software (or + portions thereof). + (c) The licenses granted in Sections 2.1(a) and (b) + are effective on the date Initial Developer first + distributes or otherwise makes the Original Software + available to a third party under the terms of this + License. + (d) Notwithstanding Section 2.1(b) above, no patent + license is granted: (1) for code that You delete from + the Original Software, or (2) for infringements + caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original + Software with other software or devices. + 2.2. Contributor Grant. + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, each + Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + (a) under intellectual property rights (other than + patent or trademark) Licensable by Contributor to + use, reproduce, modify, display, perform, sublicense + and distribute the Modifications created by such + Contributor (or portions thereof), either on an + unmodified basis, with other Modifications, as + Covered Software and/or as part of a Larger Work; and + (b) under Patent Claims infringed by the making, + using, or selling of Modifications made by that + Contributor either alone and/or in combination with + its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, + have made, and/or otherwise dispose of: (1) + Modifications made by that Contributor (or portions + thereof); and (2) the combination of Modifications + made by that Contributor with its Contributor Version + (or portions of such combination). + (c) The licenses granted in Sections 2.2(a) and + 2.2(b) are effective on the date Contributor first + distributes or otherwise makes the Modifications + available to a third party. + (d) Notwithstanding Section 2.2(b) above, no patent + license is granted: (1) for any code that Contributor + has deleted from the Contributor Version; (2) for + infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the + combination of Modifications made by that Contributor + with other software (except as part of the + Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the + absence of Modifications made by that Contributor. + 3. Distribution Obligations. + 3.1. Availability of Source Code. + Any Covered Software that You distribute or otherwise make + available in Executable form must also be made available in + Source Code form and that Source Code form must be + distributed only under the terms of this License. You must + include a copy of this License with every copy of the + Source Code form of the Covered Software You distribute or + otherwise make available. You must inform recipients of any + such Covered Software in Executable form as to how they can + obtain such Covered Software in Source Code form in a + reasonable manner on or through a medium customarily used + for software exchange. + 3.2. Modifications. + The Modifications that You create or to which You + contribute are governed by the terms of this License. You + represent that You believe Your Modifications are Your + original creation(s) and/or You have sufficient rights to + grant the rights conveyed by this License. + 3.3. Required Notices. + You must include a notice in each of Your Modifications + that identifies You as the Contributor of the Modification. + You may not remove or alter any copyright, patent or + trademark notices contained within the Covered Software, or + any notices of licensing or any descriptive text giving + attribution to any Contributor or the Initial Developer. + 3.4. Application of Additional Terms. + You may not offer or impose any terms on any Covered + Software in Source Code form that alters or restricts the + applicable version of this License or the recipients' + rights hereunder. You may choose to offer, and to charge a + fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Software. + However, you may do so only on Your own behalf, and not on + behalf of the Initial Developer or any Contributor. You + must make it absolutely clear that any such warranty, + support, indemnity or liability obligation is offered by + You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred + by the Initial Developer or such Contributor as a result of + warranty, support, indemnity or liability terms You offer. + 3.5. Distribution of Executable Versions. + You may distribute the Executable form of the Covered + Software under the terms of this License or under the terms + of a license of Your choice, which may contain terms + different from this License, provided that You are in + compliance with the terms of this License and that the + license for the Executable form does not attempt to limit + or alter the recipient's rights in the Source Code form + from the rights set forth in this License. If You + distribute the Covered Software in Executable form under a + different license, You must make it absolutely clear that + any terms which differ from this License are offered by You + alone, not by the Initial Developer or Contributor. You + hereby agree to indemnify the Initial Developer and every + Contributor for any liability incurred by the Initial + Developer or such Contributor as a result of any such terms + You offer. + 3.6. Larger Works. + You may create a Larger Work by combining Covered Software + with other code not governed by the terms of this License + and distribute the Larger Work as a single product. In such + a case, You must make sure the requirements of this License + are fulfilled for the Covered Software. + 4. Versions of the License. + 4.1. New Versions. + Sun Microsystems, Inc. is the initial license steward and + may publish revised and/or new versions of this License + from time to time. Each version will be given a + distinguishing version number. Except as provided in + Section 4.3, no one other than the license steward has the + right to modify this License. + 4.2. Effect of New Versions. + You may always continue to use, distribute or otherwise + make the Covered Software available under the terms of the + version of the License under which You originally received + the Covered Software. If the Initial Developer includes a + notice in the Original Software prohibiting it from being + distributed or otherwise made available under any + subsequent version of the License, You must distribute and + make the Covered Software available under the terms of the + version of the License under which You originally received + the Covered Software. Otherwise, You may also choose to + use, distribute or otherwise make the Covered Software + available under the terms of any subsequent version of the + License published by the license steward. + 4.3. Modified Versions. + When You are an Initial Developer and You want to create a + new license for Your Original Software, You may create and + use a modified version of this License if You: (a) rename + the license and remove any references to the name of the + license steward (except to note that the license differs + from this License); and (b) otherwise make it clear that + the license contains terms which differ from this License. + 5. DISCLAIMER OF WARRANTY. + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" + BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED + SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR + PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND + PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY + COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE + INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF + ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF + WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS + DISCLAIMER. + 6. TERMINATION. + 6.1. This License and the rights granted hereunder will + terminate automatically if You fail to comply with terms + herein and fail to cure such breach within 30 days of + becoming aware of the breach. Provisions which, by their + nature, must remain in effect beyond the termination of + this License shall survive. + 6.2. If You assert a patent infringement claim (excluding + declaratory judgment actions) against Initial Developer or + a Contributor (the Initial Developer or Contributor against + whom You assert such claim is referred to as "Participant") + alleging that the Participant Software (meaning the + Contributor Version where the Participant is a Contributor + or the Original Software where the Participant is the + Initial Developer) directly or indirectly infringes any + patent, then any and all rights granted directly or + indirectly to You by such Participant, the Initial + Developer (if the Initial Developer is not the Participant) + and all Contributors under Sections 2.1 and/or 2.2 of this + License shall, upon 60 days notice from Participant + terminate prospectively and automatically at the expiration + of such 60 day notice period, unless if within such 60 day + period You withdraw Your claim with respect to the + Participant Software against such Participant either + unilaterally or pursuant to a written agreement with + Participant. + 6.3. In the event of termination under Sections 6.1 or 6.2 + above, all end user licenses that have been validly granted + by You or any distributor hereunder prior to termination + (excluding licenses granted to You by any distributor) + shall survive termination. + 7. LIMITATION OF LIABILITY. + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE + INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF + COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE + LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR + CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK + STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL + INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO + NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR + CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT + APPLY TO YOU. + 8. U.S. GOVERNMENT END USERS. + The Covered Software is a "commercial item," as that term is + defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial + computer software" (as that term is defined at 48 C.F.R. ¤ + 252.227-7014(a)(1)) and "commercial computer software + documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. + 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 + through 227.7202-4 (June 1995), all U.S. Government End Users + acquire Covered Software with only those rights set forth herein. + This U.S. Government Rights clause is in lieu of, and supersedes, + any other FAR, DFAR, or other clause or provision that addresses + Government rights in computer software under this License. + 9. MISCELLANEOUS. + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the + extent necessary to make it enforceable. This License shall be + governed by the law of the jurisdiction specified in a notice + contained within the Original Software (except to the extent + applicable law, if any, provides otherwise), excluding such + jurisdiction's conflict-of-law provisions. Any litigation + relating to this License shall be subject to the jurisdiction of + the courts located in the jurisdiction and venue specified in a + notice contained within the Original Software, with the losing + party responsible for costs, including, without limitation, court + costs and reasonable attorneys' fees and expenses. The + application of the United Nations Convention on Contracts for the + International Sale of Goods is expressly excluded. Any law or + regulation which provides that the language of a contract shall + be construed against the drafter shall not apply to this License. + You agree that You alone are responsible for compliance with the + United States export administration regulations (and the export + control laws and regulation of any other countries) when You use, + distribute or otherwise make available any Covered Software. + 10. RESPONSIBILITY FOR CLAIMS. + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or + indirectly, out of its utilization of rights under this License + and You agree to work with Initial Developer and Contributors to + distribute such responsibility on an equitable basis. Nothing + herein is intended or shall be deemed to constitute any admission + of liability. + + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cd79363 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +recursive-include ebaysdk *.py +recursive-include tests *.py +recursive-include samples *.py +include ebay.yaml +include Changes +include INSTALL diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8bcae27 --- /dev/null +++ b/README.rst @@ -0,0 +1,90 @@ +Welcome to the python ebaysdk +============================= + +This SDK is a programmatic interface into the eBay APIs. It simplifies development and cuts development time by standardizing calls, response processing, error handling, and debugging across the Finding, Shopping, Merchandising & Trading APIs. + +Quick Example:: + + import datetime + from lxml.etree import _Element + from ebaysdk.finding import Connection + + try: + api = Connection(appid='YOUR_APPID_HERE') + response = api.execute('findItemsAdvanced', {'keywords': 'legos'}) + + assert(response.reply.ack == 'Success') + assert(type(response.reply.timestamp) == datetime.datetime) + assert(type(response.reply.searchResult.item) == list) + + item = response.reply.searchResult.item[0] + assert(type(item.listingInfo.endTime) == datetime.datetime) + assert(type(response.dict()) == dict) + assert(type(response.dom() == _Element)) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + + +Migrating from v1 to v2 +----------------------- + +For a complete guide on migrating from ebaysdk v1 to v2 and see an overview of the additional features in v2 please read the `v1 to v2 guide`_ + + +Getting Started +--------------- + +1) SDK Classes + +* `Trading API Class`_ - secure, authenticated access to private eBay data. +* `Finding API Class`_ - access eBay's next generation search capabilities. +* `Shopping API Class`_ - performance-optimized, lightweight APIs for accessing public eBay data. +* `Merchandising API Class`_ - find items and products on eBay that provide good value or are otherwise popular with eBay buyers. +* `HTTP Class`_ - generic back-end class the enbles and standardized way to make API calls. +* `Parallel Class`_ - SDK support for concurrent API calls. + +2) SDK Configuration + +* `YAML Configuration`_ +* `Understanding eBay Credentials`_ + +3) Sample code can be found in the `samples directory`_. + +4) Understanding the `Request Dictionary`_. + +Support +------- + +For developer support regarding the SDK code base please use this project's `Github issue tracking`_. + +For developer support regarding the eBay APIs please use the `eBay Developer Forums`_. + +Install +------- + +Installation instructions for *nix and windows can be found in the `INSTALL file`_. + +License +------- + +`COMMON DEVELOPMENT AND DISTRIBUTION LICENSE`_ Version 1.0 (CDDL-1.0) + + +.. _INSTALL file: https://github.com/timotheus/ebaysdk-python/blob/master/INSTALL +.. _COMMON DEVELOPMENT AND DISTRIBUTION LICENSE: http://opensource.org/licenses/CDDL-1.0 +.. _Understanding eBay Credentials: https://github.com/timotheus/ebaysdk-python/wiki/eBay-Credentials +.. _eBay Developer Site: http://developer.ebay.com/ +.. _YAML Configuration: https://github.com/timotheus/ebaysdk-python/wiki/YAML-Configuration +.. _Trading API Class: https://github.com/timotheus/ebaysdk-python/wiki/Trading-API-Class +.. _Finding API Class: https://github.com/timotheus/ebaysdk-python/wiki/Finding-API-Class +.. _Shopping API Class: https://github.com/timotheus/ebaysdk-python/wiki/Shopping-API-Class +.. _Merchandising API Class: https://github.com/timotheus/ebaysdk-python/wiki/Merchandising-API-Class +.. _HTTP Class: https://github.com/timotheus/ebaysdk-python/wiki/HTTP-Class +.. _Parallel Class: https://github.com/timotheus/ebaysdk-python/wiki/Parallel-Class +.. _eBay Developer Forums: https://go.developer.ebay.com/developers/ebay/forums-support/support +.. _Github issue tracking: https://github.com/timotheus/ebaysdk-python/issues +.. _v1 to v2 guide: https://github.com/timotheus/ebaysdk-python/wiki/Migrating-from-v1-to-v2 +.. _samples directory: https://github.com/timotheus/ebaysdk-python/tree/master/samples +.. _Request Dictionary: https://github.com/timotheus/ebaysdk-python/wiki/Request-Dictionary diff --git a/ebay.yaml b/ebay.yaml new file mode 100644 index 0000000..e79658c --- /dev/null +++ b/ebay.yaml @@ -0,0 +1,34 @@ +# eBay SDK Defaults + +name: ebay_api_config + +# Trading API Sandbox - https://www.x.com/developers/ebay/products/trading-api +api.sandbox.ebay.com: + compatability: 719 + appid: ENTER_YOUR_APPID_HERE + certid: ENTER_YOUR_CERTID_HERE + devid: ENTER_YOUR_DEVID_HERE + token: ENTER_YOUR_TOKEN_HERE + +# Trading API - https://www.x.com/developers/ebay/products/trading-api +api.ebay.com: + version: 719 + appid: ENTER_YOUR_APPID_HERE + certid: ENTER_YOUR_CERTID_HERE + devid: ENTER_YOUR_DEVID_HERE + token: ENTER_YOUR_TOKEN_HERE + +# Finding API - https://www.x.com/developers/ebay/products/finding-api +svcs.ebay.com: + appid: ENTER_YOUR_APPID_HERE + version: 1.0.0 + +# Shopping API - https://www.x.com/developers/ebay/products/shopping-api +open.api.ebay.com: + appid: ENTER_YOUR_APPID_HERE + version: 671 + + # Optional affiliate tracking + # http://developer.ebay.com/DevZone/shopping/docs/Concepts/ShoppingAPI_FormatOverview.html#StandardURLParameters + trackingid: ENTER_YOUR_TRACKINGID_HERE + trackingpartnercode: ENTER_YOUR_PARTNERCODE_HERE \ No newline at end of file diff --git a/ebaysdk/.DS_Store b/ebaysdk/.DS_Store new file mode 100644 index 0000000..2818981 Binary files /dev/null and b/ebaysdk/.DS_Store differ diff --git a/ebaysdk/__init__.py b/ebaysdk/__init__.py new file mode 100644 index 0000000..c8f76af --- /dev/null +++ b/ebaysdk/__init__.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import platform +import logging + +__version__ = '2.1.0' +Version = __version__ # for backware compatibility + +try: + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +UserAgent = 'eBaySDK/%s Python/%s %s/%s' % ( + __version__, + platform.python_version(), + platform.system(), + platform.release() +) + +log = logging.getLogger('ebaysdk') + +if not log.handlers: + log.addHandler(NullHandler()) + +def get_version(): + return __version__ + +def set_stream_logger(level=logging.DEBUG, format_string=None): + log.handlers=[] + + if not format_string: + format_string = "%(asctime)s %(name)s [%(levelname)s]:%(message)s" + + log.setLevel(level) + fh = logging.StreamHandler() + fh.setLevel(level) + formatter = logging.Formatter(format_string) + fh.setFormatter(formatter) + log.addHandler(fh) + +def trading(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import trading', + 'from ebaysdk.trading import Connection as trading', + ) + ) + +def shopping(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import shopping', + 'from ebaysdk.shopping import Connection as shopping', + ) + ) + +def finding(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import finding', + 'from ebaysdk.finding import Connection as finding', + ) + ) + +def merchandising(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import merchandising', + 'from ebaysdk.merchandising import Connection as merchandising', + ) + ) + +def html(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import html', + 'from ebaysdk.http import Connection as html', + ) + ) + +def parallel(*args, **kwargs): + raise ImportError( + 'SDK import must be changed as follows:\n\n- %s\n+ %s\n\n' % ( + 'from ebaysdk import parallel', + 'from ebaysdk.parallel import Parallel as parallel', + ) + ) diff --git a/ebaysdk/config.py b/ebaysdk/config.py new file mode 100644 index 0000000..abf922c --- /dev/null +++ b/ebaysdk/config.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import yaml + +from ebaysdk import log +from ebaysdk.exception import ConnectionConfigError + +class Config(object): + """Config Class for all APIs connections + + >>> c = Config(domain='api.ebay.com') + >>> print(c.file()) + ebay.yaml + >>> c.set('fname', 'tim') + >>> c.get('fname') + 'tim' + >>> c.get('missingkey', 'defaultvalue') + 'defaultvalue' + >>> c.set('number', 22) + >>> c.get('number') + 22 + """ + + def __init__(self, domain, connection_kwargs=dict(), config_file='ebay.yaml'): + self.config_file=config_file + self.domain=domain + self.values=dict() + self.config_file_used=[] + self.connection_kwargs=connection_kwargs + + # populate defaults + self._populate_yaml_defaults() + + def _populate_yaml_defaults(self): + "Returns a dictionary of YAML defaults." + + # check for absolute path + if self.config_file and os.path.exists(self.config_file): + self.config_file_used=self.config_file + fhandle = open(self.config_file, "r") + dataobj = yaml.load(fhandle.read()) + + for k, val in dataobj.get(self.domain, {}).items(): + self.set(k, val) + + return self + + # check other directories + dirs = ['.', os.path.expanduser('~'), '/etc'] + for mydir in dirs: + myfile = "%s/%s" % (mydir, self.config_file) + + if os.path.exists(myfile): + self.config_file_used=myfile + + fhandle = open(myfile, "r") + dataobj = yaml.load(fhandle.read()) + + for k, val in dataobj.get(self.domain, {}).items(): + self.set(k, val) + + return self + + if self.config_file: + raise ConnectionConfigError('config file %s not found' % self.config_file) + + def file(self): + return self.config_file_used + + def get(self, cKey, defaultValue=None): + #log.debug('get: %s=%s' % (cKey, self.values.get(cKey, defaultValue))) + return self.values.get(cKey, defaultValue) + + def set(self, cKey, defaultValue, force=False): + + if force: + #log.debug('set (force): %s=%s' % (cKey, defaultValue)) + self.values.update({cKey: defaultValue}) + + elif cKey in self.connection_kwargs and self.connection_kwargs[cKey] is not None: + #log.debug('set: %s=%s' % (cKey, self.connection_kwargs[cKey])) + self.values.update({cKey: self.connection_kwargs[cKey]}) + + # otherwise, use yaml default and then fall back to + # the default set in the __init__() + else: + if not cKey in self.values: + #log.debug('set: %s=%s' % (cKey, defaultValue)) + self.values.update({cKey: defaultValue}) + else: + pass diff --git a/ebaysdk/connection.py b/ebaysdk/connection.py new file mode 100644 index 0000000..d99cdb2 --- /dev/null +++ b/ebaysdk/connection.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +from ebaysdk import log + +import re +import time +import uuid +import webbrowser + +from requests import Request, Session +from requests.adapters import HTTPAdapter + +from xml.dom.minidom import parseString +from xml.parsers.expat import ExpatError + +from ebaysdk import set_stream_logger, UserAgent +from ebaysdk.utils import getNodeText as getNodeTextUtils +from ebaysdk.utils import getValue +from ebaysdk.response import Response +from ebaysdk.exception import ConnectionError, ConnectionResponseError + +HTTP_SSL = { + False: 'http', + True: 'https', +} + +class BaseConnection(object): + """Base Connection Class.""" + + def __init__(self, debug=False, method='GET', + proxy_host=None, timeout=20, proxy_port=80, + parallel=None, **kwargs): + + if debug: + set_stream_logger() + + self.response = None + self.request = None + self.verb = None + self.debug = debug + self.method = method + self.timeout = timeout + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.datetime_nodes = [] + self._list_nodes = [] + + self.proxies = dict() + if self.proxy_host: + proxy = 'http://%s:%s' % (self.proxy_host, self.proxy_port) + self.proxies = { + 'http': proxy, + 'https': proxy + } + + self.session = Session() + self.session.mount('http://', HTTPAdapter(max_retries=3)) + self.session.mount('https://', HTTPAdapter(max_retries=3)) + + self.parallel = parallel + + self.base_list_nodes = [] + self.datetime_nodes = [] + + self._reset() + + def debug_callback(self, debug_type, debug_message): + log.debug('type: ' + str(debug_type) + ' message' + str(debug_message)) + + def v(self, *args, **kwargs): + return getValue(self.response.dict(), *args, **kwargs) + + def getNodeText(self, nodelist): + return getNodeTextUtils(nodelist) + + def _reset(self): + self.response = None + self.request = None + self.verb = None + self._list_nodes = [] + self._request_id = None + self._request_dict = {} + self._time = time.time() + self._response_content = None + self._response_dom = None + self._response_obj = None + self._response_soup = None + self._response_dict = None + self._response_error = None + self._resp_body_errors = [] + self._resp_body_warnings = [] + self._resp_codes = [] + + def _add_prefix(self, nodes, verb): + if verb: + for i, v in enumerate(nodes): + if not nodes[i].startswith(verb.lower()): + nodes[i] = "%sresponse.%s" % (verb.lower(), nodes[i].lower()) + + def execute(self, verb, data=None, list_nodes=[], verb_attrs=None): + "Executes the HTTP request." + log.debug('execute: verb=%s data=%s' % (verb, data)) + + self._reset() + + self._list_nodes += list_nodes + self._add_prefix(self._list_nodes, verb) + + if hasattr(self, 'base_list_nodes'): + self._list_nodes += self.base_list_nodes + + self.build_request(verb, data, verb_attrs) + self.execute_request() + + if hasattr(self.response, 'content'): + self.process_response() + self.error_check() + + log.debug('total time=%s' % (time.time() - self._time)) + + return self.response + + def build_request(self, verb, data, verb_attrs): + + self.verb = verb + self._request_dict = data + self._request_id = uuid.uuid4() + + url = "%s://%s%s" % ( + HTTP_SSL[self.config.get('https', False)], + self.config.get('domain'), + self.config.get('uri') + ) + + + headers = self.build_request_headers(verb) + #print headers + headers.update({'User-Agent': UserAgent, + 'X-EBAY-SDK-REQUEST-ID': str(self._request_id)}) + + request = Request(self.method, + url, + data=self.build_request_data(verb, data, verb_attrs).encode('utf-8', 'ignore'), + headers=headers, + ) + + self.request = request.prepare() + + def execute_request(self): + + log.debug("REQUEST (%s): %s %s" \ + % (self._request_id, self.request.method, self.request.url)) + log.debug('headers=%s' % self.request.headers) + log.debug('body=%s' % self.request.body) + + if self.parallel: + self.parallel._add_request(self) + return None + + self.response = self.session.send(self.request, + verify=False, + proxies=self.proxies, + timeout=self.timeout, + allow_redirects=True + ) + + log.debug('RESPONSE (%s):' % self._request_id) + log.debug('elapsed time=%s' % self.response.elapsed) + log.debug('status code=%s' % self.response.status_code) + log.debug('headers=%s' % self.response.headers) + log.debug('content=%s' % self.response.text) + + def process_response(self, parse_response=True): + """Post processing of the response""" + + self.response = Response(self.response, + verb=self.verb, + list_nodes=self._list_nodes, + datetime_nodes=self.datetime_nodes, + parse_response=parse_response) + + # set for backward compatibility + self._response_content = self.response.content + + if self.response.status_code != 200: + self._response_error = self.response.reason + + def error_check(self): + estr = self.error() + + if estr and self.config.get('errors', True): + log.error(estr) + raise ConnectionError(estr, self.response) + + def response_codes(self): + return self._resp_codes + + def response_status(self): + "Retuns the HTTP response status string." + + return self.response.reason + + def response_code(self): + "Returns the HTTP response status code." + + return self.response.status_code + + def response_content(self): + return self.response.content + + def response_soup(self): + "Returns a BeautifulSoup object of the response." + + if not self._response_soup: + try: + from bs4 import BeautifulStoneSoup + except ImportError: + from BeautifulSoup import BeautifulStoneSoup + log.warn('DeprecationWarning: BeautifulSoup 3 or earlier is deprecated; install bs4 instead\n') + + self._response_soup = BeautifulStoneSoup( + self.response_content.decode('utf-8') + ) + + return self._response_soup + + def response_obj(self): + log.warn('response_obj() DEPRECATED, use response.reply instead') + return self.response.reply + + def response_dom(self): + """ Deprecated: use self.response.dom() instead + Returns the response DOM (xml.dom.minidom). + """ + log.warn('response_dom() DEPRECATED, use response.dom instead') + + if not self._response_dom: + dom = None + content = None + + try: + if self.response.content: + regex = re.compile(b'xmlns="[^"]+"') + content = regex.sub(b'', self.response.content) + else: + content = "<%sResponse>" % (self.verb, self.verb) + + dom = parseString(content) + self._response_dom = dom.getElementsByTagName( + self.verb + 'Response')[0] + + except ExpatError as e: + raise ConnectionResponseError("Invalid Verb: %s (%s)" % (self.verb, e), self.response) + except IndexError: + self._response_dom = dom + + return self._response_dom + + def response_dict(self): + "Returns the response dictionary." + log.warn('response_dict() DEPRECATED, use response.dict() or response.reply instead') + + return self.response.reply + + def response_json(self): + "Returns the response JSON." + log.warn('response_json() DEPRECATED, use response.json() instead') + + return self.response.json() + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + return [] + + def error(self): + "Builds and returns the api error message." + + error_array = [] + if self._response_error: + error_array.append(self._response_error) + + error_array.extend(self._get_resp_body_errors()) + + if len(error_array) > 0: + error_string = u"%s: %s" % (self.verb, u", ".join(error_array)) + + return error_string + + return None + + def opendoc(self): + webbrowser.open(self.config.get('doc_url')) + diff --git a/ebaysdk/exception.py b/ebaysdk/exception.py new file mode 100644 index 0000000..eb3cc36 --- /dev/null +++ b/ebaysdk/exception.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +class ConnectionError(Exception): + def __init__(self, msg, response): + super(ConnectionError, self).__init__(u'%s' % msg) + self.message = u'%s' % msg + self.response = response + + def __str__(self): + return repr(self.message) + +class ConnectionConfigError(Exception): + def __init__(self, msg): + super(ConnectionConfigError, self).__init__(u'%s' % msg) + self.message = u'%s' % msg + + def __str__(self): + return repr(self.message) + +class ConnectionResponseError(Exception): + def __init__(self, msg, response): + super(ConnectionResponseError, self).__init__(u'%s' % msg) + self.message = u'%s' % msg + self.response = response + + def __str__(self): + return repr(self.message) + +class RequestPaginationError(Exception): + def __init__(self, msg, response): + super(RequestPaginationError, self).__init__(u'%s' % msg) + self.message = u'%s' % msg + self.response = response + + def __str__(self): + return repr(self.message) + +class PaginationLimit(Exception): + def __init__(self, msg, response): + super(PaginationLimit, self).__init__(u'%s' % msg) + self.message = u'%s' % msg + self.response = response + + def __str__(self): + return repr(self.message) diff --git a/ebaysdk/finding/__init__.py b/ebaysdk/finding/__init__.py new file mode 100644 index 0000000..6f5b3f5 --- /dev/null +++ b/ebaysdk/finding/__init__.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk import log +from ebaysdk.connection import BaseConnection +from ebaysdk.exception import RequestPaginationError, PaginationLimit +from ebaysdk.config import Config +from ebaysdk.utils import dict2xml + +class Connection(BaseConnection): + """Connection class for the Finding service + + API documentation: + https://www.x.com/developers/ebay/products/finding-api + + Supported calls: + findItemsAdvanced + findItemsByCategory + (all others, see API docs) + + Doctests: + >>> f = Connection(config_file=os.environ.get('EBAY_YAML'), debug=False) + >>> retval = f.execute('findItemsAdvanced', {'keywords': u'niño'}) + >>> error = f.error() + >>> print(error) + None + >>> if not f.error(): + ... print(f.response.reply.itemSearchURL != '') + ... items = f.response.reply.searchResult.item + ... print(len(items) > 2) + ... print(f.response.reply.ack) + True + True + Success + + """ + + def __init__(self, **kwargs): + """Finding class constructor. + + Keyword arguments: + domain -- API endpoint (default: svcs.ebay.com) + config_file -- YAML defaults (default: ebay.yaml) + debug -- debugging enabled (default: False) + warnings -- warnings enabled (default: False) + uri -- API endpoint uri (default: /services/search/FindingService/v1) + appid -- eBay application id + siteid -- eBay country site id (default: EBAY-US) + version -- version number (default: 1.0.0) + https -- execute of https (default: False) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + response_encoding -- API encoding (default: XML) + request_encoding -- API encoding (default: XML) + """ + + super(Connection, self).__init__(method='POST', **kwargs) + + self.config=Config(domain=kwargs.get('domain', 'svcs.ebay.com'), + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + # override yaml defaults with args sent to the constructor + self.config.set('domain', kwargs.get('domain', 'svcs.ebay.com')) + self.config.set('uri', '/services/search/FindingService/v1') + self.config.set('https', False) + self.config.set('warnings', True) + self.config.set('errors', True) + self.config.set('siteid', 'EBAY-US') + self.config.set('response_encoding', 'XML') + self.config.set('request_encoding', 'XML') + self.config.set('proxy_host', None) + self.config.set('proxy_port', None) + self.config.set('token', None) + self.config.set('iaf_token', None) + self.config.set('appid', None) + self.config.set('version', '1.12.0') + self.config.set('service', 'FindingService') + self.config.set('doc_url', 'http://developer.ebay.com/DevZone/finding/CallRef/index.html') + + self.datetime_nodes = ['starttimefrom', 'timestamp', 'starttime', + 'endtime'] + self.base_list_nodes = [ + 'findcompleteditemsresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsadvancedresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsbycategoryresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsbyimageresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsbykeywordsresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsbyproductresponse.categoryhistogramcontainer.categoryhistogram', + 'finditemsinebaystoresresponse.categoryhistogramcontainer.categoryhistogram', + 'findcompleteditemsresponse.aspecthistogramcontainer.aspect', + 'finditemsadvancedresponse.aspecthistogramcontainer.aspect', + 'finditemsbycategoryresponse.aspecthistogramcontainer.aspect', + 'finditemsbyimageresponse.aspecthistogramcontainer.aspect', + 'finditemsbykeywordsresponse.aspecthistogramcontainer.aspect', + 'finditemsbyproductresponse.aspecthistogramcontainer.aspect', + 'finditemsinebaystoresresponse.aspecthistogramcontainer.aspect', + 'findcompleteditemsresponse.aspect.valuehistogram', + 'finditemsadvancedresponse.aspect.valuehistogram', + 'finditemsbycategoryresponse.aspect.valuehistogram', + 'finditemsbyimageresponse.aspect.valuehistogram', + 'finditemsbykeywordsresponse.aspect.valuehistogram', + 'finditemsbyproductresponse.aspect.valuehistogram', + 'finditemsinebaystoresresponse.aspect.valuehistogram', + 'findcompleteditemsresponse.aspectfilter.aspectvaluename', + 'finditemsadvancedresponse.aspectfilter.aspectvaluename', + 'finditemsbycategoryresponse.aspectfilter.aspectvaluename', + 'finditemsbyimageresponse.aspectfilter.aspectvaluename', + 'finditemsbykeywordsresponse.aspectfilter.aspectvaluename', + 'finditemsbyproductresponse.aspectfilter.aspectvaluename', + 'finditemsinebaystoresresponse.aspectfilter.aspectvaluename', + 'findcompleteditemsresponse.searchresult.item', + 'finditemsadvancedresponse.searchresult.item', + 'finditemsbycategoryresponse.searchresult.item', + 'finditemsbyimageresponse.searchresult.item', + 'finditemsbykeywordsresponse.searchresult.item', + 'finditemsbyproductresponse.searchresult.item', + 'finditemsinebaystoresresponse.searchresult.item', + 'findcompleteditemsresponse.domainfilter.domainname', + 'finditemsadvancedresponse.domainfilter.domainname', + 'finditemsbycategoryresponse.domainfilter.domainname', + 'finditemsbyimageresponse.domainfilter.domainname', + 'finditemsbykeywordsresponse.domainfilter.domainname', + 'finditemsinebaystoresresponse.domainfilter.domainname', + 'findcompleteditemsresponse.itemfilter.value', + 'finditemsadvancedresponse.itemfilter.value', + 'finditemsbycategoryresponse.itemfilter.value', + 'finditemsbyimageresponse.itemfilter.value', + 'finditemsbykeywordsresponse.itemfilter.value', + 'finditemsbyproductresponse.itemfilter.value', + 'finditemsinebaystoresresponse.itemfilter.value', + 'findcompleteditemsresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsadvancedresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsbycategoryresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsbyimageresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsbykeywordsresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsinebaystoresresponse.conditionhistogramcontainer.conditionhistogram', + 'finditemsbyproductresponse.conditionhistogramcontainer.conditionhistogram', + 'findcompleteditemsresponse.searchitem.paymentmethod', + 'finditemsadvancedresponse.searchitem.paymentmethod', + 'finditemsbycategoryresponse.searchitem.paymentmethod', + 'finditemsbyimageresponse.searchitem.paymentmethod', + 'finditemsbykeywordsresponse.searchitem.paymentmethod', + 'finditemsbyproductresponse.searchitem.paymentmethod', + 'finditemsinebaystoresresponse.searchitem.paymentmethod', + 'findcompleteditemsresponse.searchitem.gallerypluspictureurl', + 'finditemsadvancedresponse.searchitem.gallerypluspictureurl', + 'finditemsbycategoryresponse.searchitem.gallerypluspictureurl', + 'finditemsbyimageresponse.searchitem.gallerypluspictureurl', + 'finditemsbykeywordsresponse.searchitem.gallerypluspictureurl', + 'finditemsbyproductresponse.searchitem.gallerypluspictureurl', + 'finditemsinebaystoresresponse.searchitem.gallerypluspictureurl', + 'finditemsbycategoryresponse.searchitem.attribute', + 'finditemsadvancedresponse.searchitem.attribute', + 'finditemsbykeywordsresponse.searchitem.attribute', + 'finditemsinebaystoresresponse.searchitem.attribute', + 'finditemsbyproductresponse.searchitem.attribute', + 'findcompleteditemsresponse.searchitem.attribute', + 'findcompleteditemsresponse.shippinginfo.shiptolocations', + 'finditemsadvancedresponse.shippinginfo.shiptolocations', + 'finditemsbycategoryresponse.shippinginfo.shiptolocations', + 'finditemsbyimageresponse.shippinginfo.shiptolocations', + 'finditemsbykeywordsresponse.shippinginfo.shiptolocations', + 'finditemsbyproductresponse.shippinginfo.shiptolocations', + 'finditemsinebaystoresresponse.shippinginfo.shiptolocations', + ] + + def build_request_headers(self, verb): + return { + "X-EBAY-SOA-SERVICE-NAME": self.config.get('service', ''), + "X-EBAY-SOA-SERVICE-VERSION": self.config.get('version', ''), + "X-EBAY-SOA-SECURITY-APPNAME": self.config.get('appid', ''), + "X-EBAY-SOA-GLOBAL-ID": self.config.get('siteid', ''), + "X-EBAY-SOA-OPERATION-NAME": verb, + "X-EBAY-SOA-REQUEST-DATA-FORMAT": self.config.get('request_encoding', ''), + "X-EBAY-SOA-RESPONSE-DATA-FORMAT": self.config.get('response_encoding', ''), + "Content-Type": "text/xml" + } + + def build_request_data(self, verb, data, verb_attrs): + xml = "" + xml += "<" + verb + "Request xmlns=\"http://www.ebay.com/marketplace/search/v1/services\">" + xml += dict2xml(data) + xml += "" + + return xml + + def warnings(self): + warning_string = "" + + if len(self._resp_body_warnings) > 0: + warning_string = "%s: %s" \ + % (self.verb, ", ".join(self._resp_body_warnings)) + + return warning_string + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + warnings = [] + resp_codes = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + for e in dom.findall("error"): + eSeverity = None + eDomain = None + eMsg = None + eId = None + + try: + eSeverity = e.findall('severity')[0].text + except IndexError: + pass + + try: + eDomain = e.findall('domain')[0].text + except IndexError: + pass + + try: + eId = e.findall('errorId')[0].text + if int(eId) not in resp_codes: + resp_codes.append(int(eId)) + except IndexError: + pass + + try: + eMsg = e.findall('message')[0].text + except IndexError: + pass + + msg = "Domain: %s, Severity: %s, errorId: %s, %s" \ + % (eDomain, eSeverity, eId, eMsg) + + if eSeverity == 'Warning': + warnings.append(msg) + else: + errors.append(msg) + + self._resp_body_warnings = warnings + self._resp_body_errors = errors + self._resp_codes = resp_codes + + if self.config.get('warnings') and len(warnings) > 0: + log.warn("%s: %s\n\n" % (self.verb, "\n".join(warnings))) + + try: + if self.response.reply.ack == 'Success' and len(errors) > 0 and self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + + elif len(errors) > 0: + if self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + + return errors + except AttributeError as e: + return errors + + return [] + + def next_page(self): + if type(self._request_dict) is not dict: + raise RequestPaginationError("request data is not of type dict", self.response) + + epp = self._request_dict.get('paginationInput', {}).get('enteriesPerPage', None) + num = int(self.response.reply.paginationOutput.pageNumber) + + if num >= int(self.response.reply.paginationOutput.totalPages): + raise PaginationLimit("no more pages to process", self.response) + return None + + self._request_dict['paginationInput'] = {} + + if epp: + self._request_dict['paginationInput']['enteriesPerPage'] = epp + + self._request_dict['paginationInput']['pageNumber'] = int(num) + 1 + + return self.execute(self.verb, self._request_dict) diff --git a/ebaysdk/http/__init__.py b/ebaysdk/http/__init__.py new file mode 100644 index 0000000..093167d --- /dev/null +++ b/ebaysdk/http/__init__.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import uuid +import time + +from xml.parsers.expat import ExpatError +from xml.dom.minidom import parseString +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + +from requests import Request + +from ebaysdk import log, UserAgent +from ebaysdk.connection import BaseConnection +from ebaysdk.exception import ConnectionResponseError +from ebaysdk.config import Config +from ebaysdk.utils import getNodeText +from ebaysdk.response import Response + +class Connection(BaseConnection): + """HTML class for traditional calls. + + Doctests: + >>> h = Connection() + >>> retval = h.execute('http://feeds.feedburner.com/slashdot/audio?format=xml') + >>> print(h.response.reply.rss.channel.ttl) + 2 + >>> title = h.response.dom().xpath('//title')[0] + >>> print(title.text) + Slashdot + >>> print(h.error()) + None + >>> h = Connection(method='POST', debug=False) + >>> retval = h.execute('http://www.ebay.com/') + >>> print(h.response.content != '') + True + >>> print(h.response_code()) + 200 + >>> h.response.reply + {} + """ + + def __init__(self, method='GET', **kwargs): + """HTML class constructor. + + Keyword arguments: + debug -- debugging enabled (default: False) + method -- GET/POST/PUT (default: GET) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + """ + + super(Connection, self).__init__(method=method, **kwargs) + + self.config=Config(domain=None, + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + + def response_dom(self): + "Returns the HTTP response dom." + + try: + if not self._response_dom: + self._response_dom = parseString(self.response.content) + + return self._response_dom + except ExpatError: + raise ConnectionResponseError('response is not well-formed', self.response) + + def response_dict(self): + "Return the HTTP response dictionary." + + try: + + return self.response.dict() + + except ExpatError: + raise ConnectionResponseError('response is not well-formed', self.response) + + def execute(self, url, data=None, headers=dict(), method=None, parse_response=True): + "Executes the HTTP request." + log.debug('execute: url=%s data=%s' % (url, data)) + + if method: + self.method=method + + self._reset() + self.build_request(url, data, headers) + self.execute_request() + + if self.parallel: + self.parallel._add_request(self) + return None + + self.process_response(parse_response=parse_response) + self.error_check() + + log.debug('total time=%s' % (time.time() - self._time)) + + return self.response + + def build_request(self, url, data, headers): + + self._request_id = uuid.uuid4() + + headers.update({'User-Agent': UserAgent, + 'X-EBAY-SDK-REQUEST-ID': str(self._request_id)}) + + kw = dict() + if self.method == 'POST': + kw['data'] = data + else: + kw['params'] = data + + request = Request(self.method, + url, + headers=headers, + **kw + ) + + self.request = request.prepare() + + def warnings(self): + return '' + + diff --git a/ebaysdk/merchandising/__init__.py b/ebaysdk/merchandising/__init__.py new file mode 100644 index 0000000..a858967 --- /dev/null +++ b/ebaysdk/merchandising/__init__.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk.finding import Connection as FindingConnection +from ebaysdk.utils import dict2xml + +class Connection(FindingConnection): + """Connection class for the Merchandising service + + API documentation: + http://developer.ebay.com/products/merchandising/ + + Supported calls: + getMostWatchedItems + getSimilarItems + getTopSellingProducts + (all others, see API docs) + + Doctests: + >>> s = Connection(config_file=os.environ.get('EBAY_YAML')) + >>> retval = s.execute('getMostWatchedItems', {'maxResults': 3}) + >>> print(s.response.reply.ack) + Success + >>> print(s.error()) + None + """ + + def __init__(self, **kwargs): + """Merchandising class constructor. + + Keyword arguments: + domain -- API endpoint (default: open.api.ebay.com) + config_file -- YAML defaults (default: ebay.yaml) + debug -- debugging enabled (default: False) + warnings -- warnings enabled (default: False) + uri -- API endpoint uri (default: /MerchandisingService) + appid -- eBay application id + siteid -- eBay country site id (default: 0 (US)) + version -- version number (default: 799) + https -- execute of https (default: True) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + response_encoding -- API encoding (default: XML) + request_encoding -- API encoding (default: XML) + """ + + super(Connection, self).__init__(**kwargs) + + self.config.set('uri', '/MerchandisingService', force=True) + self.config.set('service', 'MerchandisingService', force=True) + self.config.set('doc_url', 'http://developer.ebay.com/Devzone/merchandising/docs/CallRef/index.html') + + self.datetime_nodes = ['endtimeto', 'endtimefrom', 'timestamp'] + self.base_list_nodes = [ + 'getdealsresponse.itemrecommendations.item', + 'getmostwatcheditemsresponse.itemrecommendations.item', + 'getrelatedcategoryitemsresponse.itemrecommendations.item', + 'getsimilaritemsresponse.itemrecommendations.item', + 'gettopsellingproductsresponse.productrecommendations.product', + 'getrelatedcategoryitemsresponse.itemfilter.value', + 'getsimilaritemsresponse.itemfilter.value', + ] + + def build_request_headers(self, verb): + return { + "X-EBAY-API-VERSION": self.config.get('version', ''), + "EBAY-SOA-CONSUMER-ID": self.config.get('appid', ''), + "X-EBAY-API-SITEID": self.config.get('siteid', ''), + "X-EBAY-SOA-OPERATION-NAME": verb, + "X-EBAY-API-REQUEST-ENCODING": "XML", + "X-EBAY-SOA-SERVICE-NAME": self.config.get('service', ''), + "Content-Type": "text/xml" + } + + + def build_request_data(self, verb, data, verb_attrs): + xml = "" + xml += "<" + verb + "Request xmlns=\"http://www.ebay.com/marketplace/services\">" + xml += dict2xml(data) + xml += "" + + return xml diff --git a/ebaysdk/parallel.py b/ebaysdk/parallel.py new file mode 100644 index 0000000..c318b79 --- /dev/null +++ b/ebaysdk/parallel.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' +import sys +if sys.version_info[0] >= 3: + raise ImportError('grequests does not work with python3+') + +import grequests +from ebaysdk.exception import ConnectionError + +class Parallel(object): + """ + >>> from ebaysdk.finding import Connection as finding + >>> from ebaysdk.shopping import Connection as shopping + >>> from ebaysdk.http import Connection as html + >>> import os + >>> p = Parallel() + >>> r1 = html(parallel=p) + >>> retval = r1.execute('http://feeds.feedburner.com/slashdot/audio?format=xml') + >>> r2 = finding(parallel=p, config_file=os.environ.get('EBAY_YAML')) + >>> retval = r2.execute('findItemsAdvanced', {'keywords': 'shoes'}) + >>> r3 = shopping(parallel=p, config_file=os.environ.get('EBAY_YAML')) + >>> retval = r3.execute('FindItemsAdvanced', {'CharityID': 3897}) + >>> p.wait() + >>> print(p.error()) + None + >>> print(r1.response.reply.rss.channel.ttl) + 2 + >>> print(r2.response.dict()['ack']) + Success + >>> print(r3.response.reply.Ack) + Success + """ + + def __init__(self): + self._grequests = [] + self._requests = [] + self._errors = [] + + def _add_request(self, request): + self._requests.append(request) + + def wait(self, timeout=20): + "wait for all of the api requests to complete" + + self._errors = [] + self._grequests = [] + + try: + for r in self._requests: + req = grequests.request(r.request.method, + r.request.url, + data=r.request.body, + headers=r.request.headers, + verify=False, + proxies=r.proxies, + timeout=r.timeout, + allow_redirects=True) + + self._grequests.append(req) + + gresponses = grequests.map(self._grequests) + + for idx, r in enumerate(self._requests): + r.response = gresponses[idx] + r.process_response() + r.error_check() + + if r.error(): + self._errors.append(r.error()) + + except ConnectionError as e: + self._errors.append("%s" % e) + + self._requests = [] + + def error(self): + "builds and returns the api error message" + + if len(self._errors) > 0: + return "parallel error:\n%s\n" % ("\n".join(self._errors)) + + return None + + +if __name__ == '__main__': + + import doctest + import sys + + failure_count, test_count = doctest.testmod() + sys.exit(failure_count) \ No newline at end of file diff --git a/ebaysdk/policies/__init__.py b/ebaysdk/policies/__init__.py new file mode 100644 index 0000000..41a7692 --- /dev/null +++ b/ebaysdk/policies/__init__.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk import log +from ebaysdk.connection import BaseConnection +from ebaysdk.exception import RequestPaginationError, PaginationLimit +from ebaysdk.config import Config +from ebaysdk.utils import dict2xml + +class Connection(BaseConnection): + """Connection class for the Business Policies service + + API documentation: + http://developer.ebay.com/Devzone/business-policies + + Supported calls: + addSellerProfile + getSellerProfiles + (all others, see API docs) + + """ + + def __init__(self, **kwargs): + """Finding class constructor. + + Keyword arguments: + domain -- API endpoint (default: svcs.ebay.com) + config_file -- YAML defaults (default: ebay.yaml) + debug -- debugging enabled (default: False) + warnings -- warnings enabled (default: False) + uri -- API endpoint uri (default: /services/selling/v1/SellerProfilesManagementService) + appid -- eBay application id + siteid -- eBay country site id (default: EBAY-US) + version -- version number (default: 1.0.0) + https -- execute of https (default: False) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + response_encoding -- API encoding (default: XML) + request_encoding -- API encoding (default: XML) + """ + + super(Connection, self).__init__(method='POST', **kwargs) + + self.config=Config(domain=kwargs.get('domain', 'svcs.ebay.com'), + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + # override yaml defaults with args sent to the constructor + self.config.set('domain', kwargs.get('domain', 'svcs.ebay.com')) + self.config.set('uri', '/services/selling/v1/SellerProfilesManagementService') + self.config.set('https', True) + self.config.set('warnings', True) + self.config.set('errors', True) + #self.config.set('siteid', 'EBAY-US') + self.config.set('siteid', kwargs.get('siteid', 'EBAY-US')) + + self.config.set('response_encoding', 'XML') + self.config.set('request_encoding', 'XML') + self.config.set('proxy_host', None) + self.config.set('proxy_port', None) + self.config.set('token', None) + self.config.set('iaf_token', None) + self.config.set('appid', None) + self.config.set('version', '1.0.0') + self.config.set('service', 'SellerProfilesManagementService') + self.config.set('doc_url', 'http://developer.ebay.com/Devzone/business-policies/CallRef/index.html') + + self.datetime_nodes = ['deleteddate', 'timestamp', 'maxdeliverydate', + 'mindeliverydate'] + self.base_list_nodes = [ + 'setsellerprofileresponse.paymentprofile.categorygroups.categorygroup', + 'addsellerprofileresponse.paymentprofile.categorygroups.categorygroup', + 'getsellerprofilesresponse.paymentprofilelist.paymentprofile.categorygroups.categorygroup', + 'addsellerprofileresponse.returnpolicyprofile.categorygroups.categorygroup', + 'setsellerprofileresponse.returnpolicyprofile.categorygroups.categorygroup', + 'getsellerprofilesresponse.returnpolicyprofilelist.returnpolicyprofile.categorygroups.categorygroup', + 'addsellerprofileresponse.shippingpolicyprofile.categorygroups.categorygroup', + 'setsellerprofileresponse.shippingpolicyprofile.categorygroups.categorygroup', + 'getsellerprofilesresponse.shippingpolicyprofilelist.shippingpolicyprofile.categorygroups.categorygroup', + 'consolidateshippingprofilesresponse.consolidationjob', + 'getconsolidationjobstatusresponse.consolidationjob', + 'addsellerprofileresponse.paymentprofile.paymentinfo.depositdetails', + 'setsellerprofileresponse.paymentprofile.paymentinfo.depositdetails', + 'getsellerprofilesresponse.paymentprofilelist.paymentprofile.paymentinfo.depositdetails', + 'addsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.freightshipping', + 'setsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.freightshipping', + 'getsellerprofilesresponse.shippingpolicyprofilelist.shippingpolicyprofile.shippingpolicyinfo.freightshipping', + 'addsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.insurance', + 'setsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.insurance', + 'getsellerprofilesresponse.shippingpolicyprofilelist.shippingpolicyprofile.shippingpolicyinfo.insurance', + 'addsellerprofileresponse.paymentprofile.paymentinfo', + 'setsellerprofileresponse.paymentprofile.paymentinfo', + 'getsellerprofilesresponse.paymentprofilelist.paymentprofile.paymentinfo', + 'addsellerprofileresponse.returnpolicyprofile.returnpolicyinfo', + 'setsellerprofileresponse.returnpolicyprofile.returnpolicyinfo', + 'getsellerprofilesresponse.returnpolicyprofilelist.returnpolicyprofile.returnpolicyinfo', + 'addsellerprofileresponse.sellerprofile', + 'setsellerprofileresponse.sellerprofile', + 'getsellerprofilesresponse.paymentprofilelist.sellerprofile', + 'getsellerprofilesresponse.returnpolicyprofilelist.sellerprofile', + 'getsellerprofilesresponse.shippingpolicyprofilelist.sellerprofile', + 'addsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.shippingpolicyinfoservice', + 'setsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.shippingpolicyinfoservice', + 'getsellerprofilesresponse.shippingpolicyprofilelist.shippingpolicyprofile.shippingpolicyinfo.shippingpolicyinfoservice', + 'addsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.shippingprofilediscountinfo', + 'setsellerprofileresponse.shippingpolicyprofile.shippingpolicyinfo.shippingprofilediscountinfo', + 'getsellerprofilesresponse.shippingpolicyprofilelist.shippingpolicyprofile.shippingpolicyinfo.shippingprofilediscountinfo' + ] + + def build_request_headers(self, verb): + return { + "X-EBAY-SOA-SERVICE-NAME": self.config.get('service', ''), + "X-EBAY-SOA-SERVICE-VERSION": self.config.get('version', ''), + "X-EBAY-SOA-SECURITY-TOKEN": self.config.get('token', ''), + "X-EBAY-SOA-GLOBAL-ID": self.config.get('siteid', ''), + "X-EBAY-SOA-OPERATION-NAME": verb, + "X-EBAY-SOA-REQUEST-DATA-FORMAT": self.config.get('request_encoding', ''), + "X-EBAY-SOA-RESPONSE-DATA-FORMAT": self.config.get('response_encoding', ''), + "Content-Type": "text/xml" + } + + def build_request_data(self, verb, data, verb_attrs): + xml = "" + xml += "<" + verb + "Request xmlns=\"http://www.ebay.com/marketplace/search/v1/services\">" + xml += dict2xml(data) + xml += "" + + return xml + + def warnings(self): + warning_string = "" + + if len(self._resp_body_warnings) > 0: + warning_string = "%s: %s" \ + % (self.verb, ", ".join(self._resp_body_warnings)) + + return warning_string + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + warnings = [] + resp_codes = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + for e in dom.findall("error"): + eSeverity = None + eDomain = None + eMsg = None + eId = None + + try: + eSeverity = e.findall('severity')[0].text + except IndexError: + pass + + try: + eDomain = e.findall('domain')[0].text + except IndexError: + pass + + try: + eId = e.findall('errorId')[0].text + if int(eId) not in resp_codes: + resp_codes.append(int(eId)) + except IndexError: + pass + + try: + eMsg = e.findall('message')[0].text + except IndexError: + pass + + msg = "Domain: %s, Severity: %s, errorId: %s, %s" \ + % (eDomain, eSeverity, eId, eMsg) + + if eSeverity == 'Warning': + warnings.append(msg) + else: + errors.append(msg) + + self._resp_body_warnings = warnings + self._resp_body_errors = errors + self._resp_codes = resp_codes + + if self.config.get('warnings') and len(warnings) > 0: + log.warn("%s: %s\n\n" % (self.verb, "\n".join(warnings))) + + try: + if self.response.reply.ack == 'Success' and len(errors) > 0 and self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + + elif len(errors) > 0: + if self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + + return errors + except AttributeError as e: + return errors + + return [] diff --git a/ebaysdk/response.py b/ebaysdk/response.py new file mode 100644 index 0000000..bb74c66 --- /dev/null +++ b/ebaysdk/response.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' +import sys +import lxml +import copy +import datetime + +from collections import defaultdict +import json + +from ebaysdk.utils import get_dom_tree, python_2_unicode_compatible +from ebaysdk import log + +@python_2_unicode_compatible +class ResponseDataObject(object): + + def __init__(self, mydict, datetime_nodes=[]): + self._load_dict(mydict, list(datetime_nodes)) + + def __repr__(self): + return str(self) + + def __str__(self): + return "%s" % self.__dict__ + + def has_key(self, name): + try: + getattr(self, name) + return True + except AttributeError: + return False + + def get(self, name, default=None): + try: + return getattr(self, name) + except AttributeError: + return default + + def _setattr(self, name, value, datetime_nodes): + if name.lower() in datetime_nodes: + try: + ts = "%s %s" % (value.partition('T')[0], value.partition('T')[2].partition('.')[0]) + value = datetime.datetime.strptime(ts, '%Y-%m-%d %H:%M:%S') + except ValueError: + pass + + setattr(self, name, value) + + def _load_dict(self, mydict, datetime_nodes): + + for a in mydict.items(): + + if isinstance(a[1], dict): + o = ResponseDataObject(a[1], datetime_nodes) + setattr(self, a[0], o) + + elif isinstance(a[1], list): + objs = [] + for i in a[1]: + if i is None or isinstance(i, str) or isinstance(i, unicode): + objs.append(i) + else: + objs.append(ResponseDataObject(i, datetime_nodes)) + + setattr(self, a[0], objs) + else: + self._setattr(a[0], a[1], datetime_nodes) + +class Response(object): + ''' + + + Success + 1.12.0 + 2014-02-07T23:31:13.941Z + + + + + + 1 + 2 + 90 + 179 + + http://www.ebay.com/ctg/53039031?_ddo=1&_ipg=2&_pgn=1 + + + Doctests: + >>> xml = b'Success1.12.02014-02-07T23:31:13.941ZItem Two1190179http://www.ebay.com/ctg/53039031?_ddo=1&_ipg=2&_pgn=1' + >>> o = ResponseDataObject({'content': xml}, []) + >>> r = Response(o, verb='findItemsByProduct', list_nodes=['finditemsbyproductresponse.searchresult.item', 'finditemsbyproductresponse.paginationoutput.pagenumber']) + >>> len(r.dom().getchildren()) > 2 + True + >>> r.reply.searchResult._count == '1' + True + >>> type(r.reply.searchResult.item)==list + True + >>> len(r.reply.paginationOutput.pageNumber) == 1 + True + >>> xml = b'Success1.12.02014-02-07T23:31:13.941ZItem TwoUSMXItem One1290179http://www.ebay.com/ctg/53039031?_ddo=1&_ipg=2&_pgn=1' + >>> o = ResponseDataObject({'content': xml}, []) + >>> r = Response(o, verb='findItemsByProduct', list_nodes=['searchResult.item']) + >>> len(r.dom().getchildren()) > 2 + True + >>> import json + >>> j = json.loads(r.json(), 'utf8') + >>> json.dumps(j, sort_keys=True) + '{"ack": "Success", "itemSearchURL": "http://www.ebay.com/ctg/53039031?_ddo=1&_ipg=2&_pgn=1", "paginationOutput": {"entriesPerPage": "2", "pageNumber": "1", "totalEntries": "179", "totalPages": "90"}, "searchResult": {"_count": "2", "item": [{"name": "Item Two", "shipping": {"c": ["US", "MX"]}}, {"name": "Item One"}]}, "timestamp": "2014-02-07T23:31:13.941Z", "version": "1.12.0"}' + >>> sorted(r.dict().keys()) + ['ack', 'itemSearchURL', 'paginationOutput', 'searchResult', 'timestamp', 'version'] + >>> len(r.reply.searchResult.item) == 2 + True + >>> r.reply.searchResult._count == '2' + True + >>> item = r.reply.searchResult.item[0] + >>> item.name == 'Item Two' + True + >>> len(item.shipping.c) == 2 + True + ''' + + def __init__(self, obj, verb=None, list_nodes=[], datetime_nodes=[], parse_response=True): + self._list_nodes=copy.copy(list_nodes) + self._obj = obj + + if parse_response: + try: + self._dom = self._parse_xml(obj.content) + self._dict = self._etree_to_dict(self._dom) + + if verb and 'Envelope' in self._dict.keys(): + elem = self._dom.find('Body').find('%sResponse' % verb) + if elem is not None: + self._dom = elem + + self._dict = self._dict['Envelope']['Body'].get('%sResponse' % verb, self._dict) + elif verb: + elem = self._dom.find('%sResponse' % verb) + if elem is not None: + self._dom = elem + + self._dict = self._dict.get('%sResponse' % verb, self._dict) + + self.reply = ResponseDataObject(self._dict, + datetime_nodes=copy.copy(datetime_nodes)) + except lxml.etree.XMLSyntaxError as e: + log.debug('response parse failed: %s' % e) + self.reply = ResponseDataObject({}, []) + else: + self.reply = ResponseDataObject({}, []) + + def _get_node_path(self, t): + i = t + path = [] + path.insert(0, i.tag) + while 1: + try: + path.insert(0, i.getparent().tag) + i = i.getparent() + except AttributeError: + break + + return '.'.join(path) + + @staticmethod + def _pullval(v): + if len(v) == 1: + return v[0] + else: + return v + + def _etree_to_dict(self, t): + if type(t) == lxml.etree._Comment: + return {} + + # remove xmlns from nodes, I find them meaningless + t.tag = self._get_node_tag(t) + + d = {t.tag: {} if t.attrib else None} + children = list(t) + if children: + dd = defaultdict(list) + for dc in map(self._etree_to_dict, children): + for k, v in dc.items(): + dd[k].append(v) + + d = {t.tag: dict((k, self._pullval(v)) for k, v in dd.items())} + #d = {t.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} + + # TODO: Optimizations? Forces a node to type list + parent_path = self._get_node_path(t) + for k in d[t.tag].keys(): + path = "%s.%s" % (parent_path, k) + if path.lower() in self._list_nodes: + if not isinstance(d[t.tag][k], list): + d[t.tag][k] = [ d[t.tag][k] ] + + if t.attrib: + d[t.tag].update(('_' + k, v) for k, v in t.attrib.items()) + if t.text: + text = t.text.strip() + if children or t.attrib: + if text: + d[t.tag]['value'] = text + else: + d[t.tag] = text + return d + + def __getattr__(self, name): + return getattr(self._obj, name) + + def _parse_xml(self, xml): + return get_dom_tree(xml) + + def _get_node_tag(self, node): + return node.tag.replace('{' + node.nsmap.get(node.prefix, '') + '}', '') + + def dom(self, lxml=True): + if not lxml: + # create and return a cElementTree DOM + pass + + return self._dom + + def dict(self): + return self._dict + + def json(self): + return json.dumps(self.dict()) + + +if __name__ == '__main__': + + import os + import sys + + sys.path.insert(0, '%s/' % os.path.dirname(__file__)) + + import doctest + failure_count, test_count = doctest.testmod() + sys.exit(failure_count) diff --git a/ebaysdk/shopping/__init__.py b/ebaysdk/shopping/__init__.py new file mode 100644 index 0000000..11a4cc8 --- /dev/null +++ b/ebaysdk/shopping/__init__.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk import log +from ebaysdk.connection import BaseConnection +from ebaysdk.config import Config +from ebaysdk.utils import getNodeText, dict2xml + +class Connection(BaseConnection): + """Shopping API class + + API documentation: + http://developer.ebay.com/products/shopping/ + + Supported calls: + getSingleItem + getMultipleItems + (all others, see API docs) + + Doctests: + >>> s = Connection(config_file=os.environ.get('EBAY_YAML')) + >>> retval = s.execute('FindPopularItems', {'QueryKeywords': 'Python'}) + >>> print(s.response_obj().Ack) + Success + >>> print(s.error()) + None + """ + + def __init__(self, **kwargs): + """Shopping class constructor. + + Keyword arguments: + domain -- API endpoint (default: open.api.ebay.com) + config_file -- YAML defaults (default: ebay.yaml) + debug -- debugging enabled (default: False) + warnings -- warnings enabled (default: True) + errors -- errors enabled (default: True) + uri -- API endpoint uri (default: /shopping) + appid -- eBay application id + siteid -- eBay country site id (default: 0 (US)) + compatibility -- version number (default: 799) + https -- execute of https (default: True) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + trackingid -- ID to identify you to your tracking partner + trackingpartnercode -- third party who is your tracking partner + response_encoding -- API encoding (default: XML) + request_encoding -- API encoding (default: XML) + + More affiliate tracking info: + http://developer.ebay.com/DevZone/shopping/docs/Concepts/ShoppingAPI_FormatOverview.html#StandardURLParameters + + """ + super(Connection, self).__init__(method='POST', **kwargs) + + self.config=Config(domain=kwargs.get('domain', 'open.api.ebay.com'), + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + # override yaml defaults with args sent to the constructor + self.config.set('domain', kwargs.get('domain', 'open.api.ebay.com')) + self.config.set('uri', '/shopping') + self.config.set('warnings', True) + self.config.set('errors', True) + self.config.set('https', False) + self.config.set('siteid', 0) + self.config.set('response_encoding', 'XML') + self.config.set('request_encoding', 'XML') + self.config.set('proxy_host', None) + self.config.set('proxy_port', None) + self.config.set('appid', None) + self.config.set('version', '799') + self.config.set('trackingid', None) + self.config.set('trackingpartnercode', None) + self.config.set('doc_url', 'http://developer.ebay.com/DevZone/Shopping/docs/CallRef/index.html') + + if self.config.get('https') and self.debug: + print("HTTPS is not supported on the Shopping API.") + + self.datetime_nodes = ['timestamp', 'registrationdate', 'creationtime', + 'commenttime', 'updatetime', 'estimateddeliverymintime', + 'estimateddeliverymaxtime', 'creationtime', 'estimateddeliverymintime', + 'estimateddeliverymaxtime', 'endtime', 'starttime'] + + self.base_list_nodes=[ + 'findhalfproductsresponse.halfcatalogproducttype.productid', + 'findhalfproductsresponse.halfproductstype.product', + 'getshippingcostsresponse.internationalshippingserviceoptiontype.shipsto', + 'getsingleitemresponse.itemcompatibility.compatibility', + 'getsingleitemresponse.itemcompatibility.namevaluelist', + 'getsingleitemresponse.variationspecifics.namevaluelist', + 'getsingleitemresponse.namevaluelist.value', + 'getsingleitemresponse.pictures.variationspecificpictureset', + 'getmultipleitemsresponse.pictures.variationspecificpictureset', + 'findreviewsandguidesresponse.reviewdetailstype.review', + 'getshippingcostsresponse.shippingdetails.internationalshippingserviceoption', + 'getshippingcostsresponse.shippingdetails.shippingserviceoption', + 'getshippingcostsresponse.shippingdetails.excludeshiptolocation', + 'getshippingcostsresponse.shippingserviceoption.shipsto', + 'findpopularitemsresponse.itemarray.item', + 'findproductsresponse.itemarray.item', + 'getsingleitemresponse.item.paymentmethods', + 'getmultipleitemsresponse.item.pictureurl', + 'getsingleitemresponse.item.pictureurl', + 'findproductsresponse.item.shiptolocations', + 'getmultipleitemsresponse.item.shiptolocations', + 'getsingleitemresponse.item.shiptolocations', + 'getmultipleitemsresponse.item.paymentallowedsite', + 'getsingleitemresponse.item.paymentallowedsite', + 'getsingleitemresponse.item.excludeshiptolocation', + 'getshippingcostsresponse.taxtable.taxjurisdiction', + 'getsingleitemresponse.variationspecificpictureset.pictureurl', + 'getmultipleitemsresponse.variationspecificpictureset.pictureurl', + 'getsingleitemresponse.variations.variation', + 'getmultipleitemsresponse.variations.variation', + 'getsingleitemresponse.variations.pictures', + 'getmultipleitemsresponse.variations.pictures', + ] + + def build_request_headers(self, verb): + headers = { + "X-EBAY-API-VERSION": self.config.get('version', ''), + "X-EBAY-API-APP-ID": self.config.get('appid', ''), + "X-EBAY-API-SITE-ID": self.config.get('siteid', ''), + "X-EBAY-API-CALL-NAME": verb, + "X-EBAY-API-REQUEST-ENCODING": "XML", + "Content-Type": "text/xml" + } + + if self.config.get('trackingid'): + headers.update({ + "X-EBAY-API-TRACKING-ID": self.config.get('trackingid') + }) + + if self.config.get('trackingpartnercode'): + headers.update({ + "X-EBAY-API-TRACKING-PARTNER-CODE": self.config.get('trackingpartnercode') + }) + + return headers + + def build_request_data(self, verb, data, verb_attrs): + + xml = "" + xml += "<" + verb + "Request xmlns=\"urn:ebay:apis:eBLBaseComponents\">" + xml += dict2xml(data) + xml += "" + + return xml + + def warnings(self): + warning_string = "" + + if len(self._resp_body_warnings) > 0: + warning_string = "%s: %s" \ + % (self.verb, ", ".join(self._resp_body_warnings)) + + return warning_string + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + warnings = [] + resp_codes = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + for e in dom.findall('Errors'): + eSeverity = None + eClass = None + eShortMsg = None + eLongMsg = None + eCode = None + + try: + eSeverity = e.findall('SeverityCode')[0].text + except IndexError: + pass + + try: + eClass = e.findall('ErrorClassification')[0].text + except IndexError: + pass + + try: + eCode = e.findall('ErrorCode')[0].text + except IndexError: + pass + + try: + eShortMsg = e.findall('ShortMessage')[0].text + except IndexError: + pass + + try: + eLongMsg = e.findall('LongMessage')[0].text + except IndexError: + pass + + try: + eCode = float(e.findall('ErrorCode')[0].text) + if eCode.is_integer(): + eCode = int(eCode) + + if eCode not in resp_codes: + resp_codes.append(eCode) + except IndexError: + pass + + msg = "Class: %s, Severity: %s, Code: %s, %s%s" \ + % (eClass, eSeverity, eCode, eShortMsg, eLongMsg) + + if eSeverity == 'Warning': + warnings.append(msg) + else: + errors.append(msg) + + self._resp_body_warnings = warnings + self._resp_body_errors = errors + self._resp_codes = resp_codes + + if self.config.get('warnings') and len(warnings) > 0: + log.warn("%s: %s\n\n" % (self.verb, "\n".join(warnings))) + + if self.response.reply.Ack == 'Failure': + if self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + return errors + + return [] diff --git a/ebaysdk/soa/__init__.py b/ebaysdk/soa/__init__.py new file mode 100644 index 0000000..ef01121 --- /dev/null +++ b/ebaysdk/soa/__init__.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +from ebaysdk import log +from ebaysdk.connection import BaseConnection +from ebaysdk.config import Config +from ebaysdk.utils import getNodeText, dict2xml + +class Connection(BaseConnection): + """Connection class for a base SOA service""" + + def __init__(self, app_config=None, site_id='EBAY-US', debug=False, **kwargs): + """SOA Connection class constructor""" + + super(Connection, self).__init__(method='POST', debug=debug, **kwargs) + + self.config=Config(domain=kwargs.get('domain', ''), + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + self.config.set('https', False) + self.config.set('site_id', site_id) + self.config.set('content_type', 'text/xml;charset=UTF-8') + self.config.set('request_encoding', 'XML') + self.config.set('response_encoding', 'XML') + self.config.set('message_protocol', 'SOAP12') + self.config.set('soap_env_str', '') ## http://www.ebay.com/marketplace/fundraising/v1/services', + + ph = None + pp = 80 + if app_config: + self.load_from_app_config(app_config) + ph = self.config.get('proxy_host', ph) + pp = self.config.get('proxy_port', pp) + + + # override this method, to provide setup through a config object, which + # should provide a get() method for extracting constants we care about + # this method should then set the .api_config[] dict (e.g. the comment below) + def load_from_app_config(self, app_config): + #self.api_config['domain'] = app_config.get('API_SERVICE_DOMAIN') + #self.api_config['uri'] = app_config.get('API_SERVICE_URI') + pass + + # Note: this method will always return at least an empty object_dict! + # It used to return None in some cases. If you get an empty dict, + # you can use the .error() method to look for the cause. + def response_dict(self): + return self.response.dict() + + ''' + if self._response_dict: + return self._response_dict + + if self._response_content: + + mydict = self.response.dict() + + try: + verb = self.verb + 'Response' + self._response_dict = mydict['Envelope']['Body'][verb] + + except KeyError: + self._response_dict = mydict.get(self.verb + 'Response', mydict) + + return self._response_dict + ''' + + def build_request_headers(self, verb): + return { + 'Content-Type': self.config.get('content_type'), + 'X-EBAY-SOA-SERVICE-NAME': self.config.get('service'), + 'X-EBAY-SOA-OPERATION-NAME': verb, + 'X-EBAY-SOA-GLOBAL-ID': self.config.get('site_id'), + 'X-EBAY-SOA-REQUEST-DATA-FORMAT': self.config.get('request_encoding'), + 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': self.config.get('response_encoding'), + 'X-EBAY-SOA-MESSAGE-PROTOCOL': self.config.get('message_protocol'), + } + + def build_request_data(self, verb, data, verb_attrs): + xml = '' + xml += ' 0: + warning_string = "%s: %s" \ + % (self.verb, ", ".join(self._resp_body_warnings)) + + return warning_string + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + + 5014CoreRuntimeErrorSystem + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + warnings = [] + resp_codes = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + for e in dom.findall('error'): + + eSeverity = None + eDomain = None + eMsg = None + eId = None + + try: + eSeverity = e.findall('severity')[0].text + except IndexError: + pass + + try: + eDomain = e.findall('domain')[0].text + except IndexError: + pass + + try: + eId = e.findall('errorId')[0].text + if int(eId) not in resp_codes: + resp_codes.append(int(eId)) + except IndexError: + pass + + try: + eMsg = e.findall('message')[0].text + except IndexError: + pass + + msg = "Domain: %s, Severity: %s, errorId: %s, %s" \ + % (eDomain, eSeverity, eId, eMsg) + + if eSeverity == 'Warning': + warnings.append(msg) + else: + errors.append(msg) + + self._resp_body_warnings = warnings + self._resp_body_errors = errors + self._resp_codes = resp_codes + + if self.config.get('warnings') and len(warnings) > 0: + log.warn("%s: %s\n\n" % (self.verb, "\n".join(warnings))) + + try: + if self.response.reply.ack == 'Success' and len(errors) > 0 and self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + elif len(errors) > 0: + if self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + return errors + except AttributeError: + pass + + return [] diff --git a/ebaysdk/soa/finditem.py b/ebaysdk/soa/finditem.py new file mode 100644 index 0000000..b99aead --- /dev/null +++ b/ebaysdk/soa/finditem.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk.soa import Connection as BaseConnection +from ebaysdk.utils import dict2xml, getNodeText + +class Connection(BaseConnection): + """ + Not to be confused with Finding service + + Implements FindItemServiceNextGen + + https://wiki.vip.corp.ebay.com/display/apdoc/FindItemServiceNextGen + + This class is a bit hackish, it subclasses SOAService, but removes + SOAP support. FindItemServiceNextGen works fine with standard XML + and lets avoid all of the ugliness associated with SOAP. + + >>> from ebaysdk.shopping import Connection as Shopping + >>> s = Shopping(config_file=os.environ.get('EBAY_YAML')) + >>> retval = s.execute('FindPopularItems', {'QueryKeywords': 'Python'}) + >>> nodes = s.response_dom().getElementsByTagName('ItemID') + >>> itemIds = [getNodeText(n) for n in nodes] + >>> len(itemIds) > 0 + True + >>> f = Connection(debug=False, config_file=os.environ.get('EBAY_YAML')) + >>> records = f.find_items_by_ids(itemIds) + >>> len(records) > 0 + True + """ + + def __init__(self, site_id='EBAY-US', debug=False, consumer_id=None, + domain='apifindingcore.vip.ebay.com', **kwargs): + + super(Connection, self).__init__(consumer_id=consumer_id, + domain=domain, + app_config=None, + site_id=site_id, + debug=debug, **kwargs) + + self.config.set('domain', 'apifindingcore.vip.ebay.com') + self.config.set('service', 'FindItemServiceNextGen', force=True) + self.config.set('https', False) + self.config.set('uri', "/services/search/FindItemServiceNextGen/v1", force=True) + self.config.set('consumer_id', consumer_id) + + self.read_set = None + + self.datetime_nodes += ['lastupdatetime', 'timestamp'] + self.base_list_nodes += ['finditemsbyidsresponse.record'] + + def build_request_headers(self, verb): + return { + "X-EBAY-SOA-SERVICE-NAME": self.config.get('service', ''), + "X-EBAY-SOA-SERVICE-VERSION": self.config.get('version', ''), + "X-EBAY-SOA-GLOBAL-ID": self.config.get('siteid', ''), + "X-EBAY-SOA-OPERATION-NAME": verb, + "X-EBAY-SOA-CONSUMER-ID": self.config.get('consumer_id', ''), + "Content-Type": "text/xml" + } + + def findItemsByIds(self, ebay_item_ids, + read_set=['ITEM_ID', 'TITLE', 'SELLER_NAME', 'ALL_CATS', 'ITEM_CONDITION_NEW']): + + self.read_set = read_set + read_set_node = [] + + for rtype in self.read_set: + read_set_node.append({ + 'member': { + 'namespace': 'ItemDictionary', + 'name': rtype + } + }) + + args = {'id': ebay_item_ids, 'readSet': read_set_node} + self.execute('findItemsByIds', args) + return self.mappedResponse() + + def mappedResponse(self): + records = [] + + for r in self.response.dict().get('record', []): + mydict = dict() + i = 0 + + for values_dict in r.get('value', {}): + + if values_dict is None: + continue + + for key, value in values_dict.items(): + value_data = None + if type(value) == list: + value_data = [x for x in value] + else: + value_data = value + + mydict.update({self.read_set[i]: value_data}) + + i = i+1 + + records.append(mydict) + + return records + + def find_items_by_ids(self, *args, **kwargs): + return self.findItemsByIds(*args, **kwargs) + + def build_request_data(self, verb, data, verb_attrs): + xml = "" + xml += "<" + verb + "Request" + xml += ' xmlns="http://www.ebay.com/marketplace/search/v1/services"' + xml += '>' + xml += dict2xml(data) + xml += "" + + return xml + diff --git a/ebaysdk/trading/__init__.py b/ebaysdk/trading/__init__.py new file mode 100644 index 0000000..244f9ba --- /dev/null +++ b/ebaysdk/trading/__init__.py @@ -0,0 +1,795 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os + +from ebaysdk import log +from ebaysdk.connection import BaseConnection +from ebaysdk.config import Config +from ebaysdk.utils import getNodeText, dict2xml + +class Connection(BaseConnection): + """Trading API class + + API documentation: + https://www.x.com/developers/ebay/products/trading-api + + Supported calls: + AddItem + ReviseItem + GetUser + (all others, see API docs) + + Doctests: + >>> import datetime + >>> t = Connection(config_file=os.environ.get('EBAY_YAML')) + >>> response = t.execute('GetCharities', {'CharityID': 3897}) + >>> charity_name = '' + >>> if len( t.response.dom().xpath('//Name') ) > 0: + ... charity_name = t.response.dom().xpath('//Name')[0].text + >>> print(charity_name) + Sunshine Kids Foundation + >>> isinstance(response.reply.Timestamp, datetime.datetime) + True + >>> print(t.error()) + None + >>> t2 = Connection(errors=False, debug=False, config_file=os.environ.get('EBAY_YAML')) + >>> response = t2.execute('VerifyAddItem', {}) + >>> print(t2.response_codes()) + [10009] + """ + + def __init__(self, **kwargs): + """Trading class constructor. + + Keyword arguments: + domain -- API endpoint (default: api.ebay.com) + config_file -- YAML defaults (default: ebay.yaml) + debug -- debugging enabled (default: False) + warnings -- warnings enabled (default: False) + uri -- API endpoint uri (default: /ws/api.dll) + appid -- eBay application id + devid -- eBay developer id + certid -- eBay cert id + token -- eBay application/user token + siteid -- eBay country site id (default: 0 (US)) + compatibility -- version number (default: 648) + https -- execute of https (default: True) + proxy_host -- proxy hostname + proxy_port -- proxy port number + timeout -- HTTP request timeout (default: 20) + parallel -- ebaysdk parallel object + response_encoding -- API encoding (default: XML) + request_encoding -- API encoding (default: XML) + """ + + super(Connection, self).__init__(method='POST', **kwargs) + + self.config=Config(domain=kwargs.get('domain', 'api.ebay.com'), + connection_kwargs=kwargs, + config_file=kwargs.get('config_file', 'ebay.yaml')) + + + # override yaml defaults with args sent to the constructor + self.config.set('domain', kwargs.get('domain', 'api.ebay.com')) + self.config.set('uri', '/ws/api.dll') + self.config.set('warnings', True) + self.config.set('errors', True) + self.config.set('https', True) + self.config.set('siteid', kwargs.get('siteid', 0)) + self.config.set('response_encoding', 'XML') + self.config.set('request_encoding', 'XML') + self.config.set('proxy_host', None) + self.config.set('proxy_port', None) + self.config.set('token', None) + self.config.set('iaf_token', None) + self.config.set('appid', None) + self.config.set('devid', None) + self.config.set('certid', None) + self.config.set('compatibility', '837') + self.config.set('doc_url', 'http://developer.ebay.com/devzone/xml/docs/reference/ebay/index.html') + + self.datetime_nodes = [ + 'shippingtime', + 'starttime', + 'endtime', + 'scheduletime', + 'createdtime', + 'hardexpirationtime', + 'invoicedate', + 'begindate', + 'enddate', + 'startcreationtime', + 'endcreationtime', + 'endtimefrom', + 'endtimeto', + 'updatetime', + 'lastupdatetime', + 'lastmodifiedtime', + 'modtimefrom', + 'modtimeto', + 'createtimefrom', + 'createtimeto', + 'starttimefrom', + 'starttimeto', + 'timeto', + 'paymenttimefrom', + 'paymenttimeto', + 'inventorycountlastcalculateddate', + 'registrationdate', + 'timefrom', + 'timestamp', + 'messagecreationtime', + 'resolutiontime', + 'date', + 'bankmodifydate', + 'creditcardexpiration', + 'creditcardmodifydate', + 'lastpaymentdate', + 'submittedtime', + 'announcementstarttime', + 'eventtime', + 'periodicstartdate', + 'modtime', + 'expirationtime', + 'creationtime', + 'lastusedtime', + 'disputecreatedtime', + 'disputemodifiedtime', + 'externaltransactiontime', + 'commenttime', + 'lastbidtime', + 'time', + 'creationdate', + 'lastmodifieddate', + 'receivedate', + 'expirationdate', + 'resolutiondate', + 'lastreaddate', + 'userforwarddate', + 'itemendtime', + 'userresponsedate', + 'nextretrytime', + 'deliverytime', + 'timebid', + 'paidtime', + 'shippedtime', + 'expectedreleasedate', + 'paymenttime', + 'promotionalsalestarttime', + 'promotionalsaleendtime', + 'refundtime', + 'refundrequestedtime', + 'refundcompletiontime', + 'estimatedrefundcompletiontime', + 'lastemailsenttime', + 'sellerinvoicetime', + 'estimateddeliverydate', + 'printedtime', + 'deliverydate', + 'refundgrantedtime', + 'scheduleddeliverytimemin', + 'scheduleddeliverytimemax', + 'actualdeliverytime', + 'usebydate', + 'lastopenedtime', + 'returndate', + 'revocationtime', + 'lasttimemodified', + 'createddate', + 'invoicesenttime', + 'acceptedtime', + 'sellerebaypaymentprocessenabletime', + 'useridlastchanged', + 'actionrequiredby', + ] + + self.base_list_nodes = [ + 'getmymessagesresponse.abstractrequesttype.detaillevel', + 'getaccountresponse.abstractrequesttype.outputselector', + 'getadformatleadsresponse.abstractrequesttype.outputselector', + 'getallbiddersresponse.abstractrequesttype.outputselector', + 'getbestoffersresponse.abstractrequesttype.outputselector', + 'getbidderlistresponse.abstractrequesttype.outputselector', + 'getcategoriesresponse.abstractrequesttype.outputselector', + 'getcategoryfeaturesresponse.abstractrequesttype.outputselector', + 'getcategorylistingsresponse.abstractrequesttype.outputselector', + 'getcrosspromotionsresponse.abstractrequesttype.outputselector', + 'getfeedbackresponse.abstractrequesttype.outputselector', + 'gethighbiddersresponse.abstractrequesttype.outputselector', + 'getitemresponse.abstractrequesttype.outputselector', + 'getitemsawaitingfeedbackresponse.abstractrequesttype.outputselector', + 'getitemshippingresponse.abstractrequesttype.outputselector', + 'getitemtransactionsresponse.abstractrequesttype.outputselector', + 'getmembermessagesresponse.abstractrequesttype.outputselector', + 'getmyebaybuyingresponse.abstractrequesttype.outputselector', + 'getmyebaysellingresponse.abstractrequesttype.outputselector', + 'getmymessagesresponse.abstractrequesttype.outputselector', + 'getnotificationpreferencesresponse.abstractrequesttype.outputselector', + 'getordersresponse.abstractrequesttype.outputselector', + 'getordertransactionsresponse.abstractrequesttype.outputselector', + 'getproductsresponse.abstractrequesttype.outputselector', + 'getsearchresultsresponse.abstractrequesttype.outputselector', + 'getsellereventsresponse.abstractrequesttype.outputselector', + 'getsellerlistresponse.abstractrequesttype.outputselector', + 'getsellerpaymentsresponse.abstractrequesttype.outputselector', + 'getsellertransactionsresponse.abstractrequesttype.outputselector', + 'getmessagepreferencesresponse.asqpreferencestype.subject', + 'getaccountresponse.accountentriestype.accountentry', + 'getaccountresponse.accountsummarytype.additionalaccount', + 'additemresponse.additemresponsecontainertype.discountreason', + 'additemsresponse.additemresponsecontainertype.discountreason', + 'setnotificationpreferencesresponse.applicationdeliverypreferencestype.deliveryurldetails', + 'additemresponse.attributearraytype.attribute', + 'additemsresponse.attributearraytype.attribute', + 'verifyadditemresponse.attributearraytype.attribute', + 'additemresponse.attributetype.value', + 'additemsresponse.attributetype.value', + 'addsellingmanagertemplateresponse.attributetype.value', + 'addliveauctionitemresponse.attributetype.value', + 'getitemrecommendationsresponse.attributetype.value', + 'verifyadditemresponse.attributetype.value', + 'addfixedpriceitemresponse.attributetype.value', + 'relistfixedpriceitemresponse.attributetype.value', + 'revisefixedpriceitemresponse.attributetype.value', + 'getfeedbackresponse.averageratingdetailarraytype.averageratingdetails', + 'getfeedbackresponse.averageratingsummarytype.averageratingdetails', + 'respondtobestofferresponse.bestofferarraytype.bestoffer', + 'getliveauctionbiddersresponse.bidderdetailarraytype.bidderdetail', + 'getallbiddersresponse.biddingsummarytype.itembiddetails', + 'getsellerdashboardresponse.buyersatisfactiondashboardtype.alert', + 'getshippingdiscountprofilesresponse.calculatedshippingdiscounttype.discountprofile', + 'getcategoriesresponse.categoryarraytype.category', + 'getcategoryfeaturesresponse.categoryfeaturetype.listingduration', + 'getcategoryfeaturesresponse.categoryfeaturetype.paymentmethod', + 'getcategoriesresponse.categorytype.categoryparentid', + 'getsuggestedcategoriesresponse.categorytype.categoryparentname', + 'getcategory2csresponse.categorytype.productfinderids', + 'getcategory2csresponse.categorytype.characteristicssets', + 'getproductfamilymembersresponse.characteristicssettype.characteristics', + 'getproductsearchpageresponse.characteristicssettype.characteristics', + 'getproductsearchresultsresponse.characteristicssettype.characteristics', + 'getuserresponse.charityaffiliationdetailstype.charityaffiliationdetail', + 'getbidderlistresponse.charityaffiliationstype.charityid', + 'setcharitiesresponse.charityinfotype.nonprofitaddress', + 'setcharitiesresponse.charityinfotype.nonprofitsocialaddress', + 'getcategoryfeaturesresponse.conditionvaluestype.condition', + 'getbidderlistresponse.crosspromotionstype.promoteditem', + 'getuserdisputesresponse.disputearraytype.dispute', + 'getuserdisputesresponse.disputetype.disputeresolution', + 'getdisputeresponse.disputetype.disputemessage', + 'setsellingmanagerfeedbackoptionsresponse.feedbackcommentarraytype.storedcommenttext', + 'getfeedbackresponse.feedbackdetailarraytype.feedbackdetail', + 'getfeedbackresponse.feedbackperiodarraytype.feedbackperiod', + 'addfixedpriceitemresponse.feestype.fee', + 'additemresponse.feestype.fee', + 'additemsresponse.feestype.fee', + 'addliveauctionitemresponse.feestype.fee', + 'relistfixedpriceitemresponse.feestype.fee', + 'relistitemresponse.feestype.fee', + 'revisefixedpriceitemresponse.feestype.fee', + 'reviseitemresponse.feestype.fee', + 'reviseliveauctionitemresponse.feestype.fee', + 'verifyaddfixedpriceitemresponse.feestype.fee', + 'verifyadditemresponse.feestype.fee', + 'reviseinventorystatusresponse.feestype.fee', + 'verifyrelistitemresponse.feestype.fee', + 'getshippingdiscountprofilesresponse.flatshippingdiscounttype.discountprofile', + 'getitemrecommendationsresponse.getrecommendationsrequestcontainertype.recommendationengine', + 'getitemrecommendationsresponse.getrecommendationsrequestcontainertype.deletedfield', + 'getuserresponse.integratedmerchantcreditcardinfotype.supportedsite', + 'sendinvoiceresponse.internationalshippingserviceoptionstype.shiptolocation', + 'reviseinventorystatusresponse.inventoryfeestype.fee', + 'getbidderlistresponse.itemarraytype.item', + 'getbestoffersresponse.itembestoffersarraytype.itembestoffers', + 'addfixedpriceitemresponse.itemcompatibilitylisttype.compatibility', + 'additemresponse.itemcompatibilitylisttype.compatibility', + 'additemfromsellingmanagertemplateresponse.itemcompatibilitylisttype.compatibility', + 'additemsresponse.itemcompatibilitylisttype.compatibility', + 'addsellingmanagertemplateresponse.itemcompatibilitylisttype.compatibility', + 'relistfixedpriceitemresponse.itemcompatibilitylisttype.compatibility', + 'relistitemresponse.itemcompatibilitylisttype.compatibility', + 'revisefixedpriceitemresponse.itemcompatibilitylisttype.compatibility', + 'reviseitemresponse.itemcompatibilitylisttype.compatibility', + 'revisesellingmanagertemplateresponse.itemcompatibilitylisttype.compatibility', + 'verifyaddfixedpriceitemresponse.itemcompatibilitylisttype.compatibility', + 'verifyadditemresponse.itemcompatibilitylisttype.compatibility', + 'verifyrelistitemresponse.itemcompatibilitylisttype.compatibility', + 'addfixedpriceitemresponse.itemcompatibilitytype.namevaluelist', + 'additemresponse.itemcompatibilitytype.namevaluelist', + 'additemfromsellingmanagertemplateresponse.itemcompatibilitytype.namevaluelist', + 'additemsresponse.itemcompatibilitytype.namevaluelist', + 'addsellingmanagertemplateresponse.itemcompatibilitytype.namevaluelist', + 'relistfixedpriceitemresponse.itemcompatibilitytype.namevaluelist', + 'relistitemresponse.itemcompatibilitytype.namevaluelist', + 'revisefixedpriceitemresponse.itemcompatibilitytype.namevaluelist', + 'reviseitemresponse.itemcompatibilitytype.namevaluelist', + 'revisesellingmanagertemplateresponse.itemcompatibilitytype.namevaluelist', + 'verifyadditemresponse.itemcompatibilitytype.namevaluelist', + 'verifyrelistitemresponse.itemcompatibilitytype.namevaluelist', + 'getpromotionalsaledetailsresponse.itemidarraytype.itemid', + 'leavefeedbackresponse.itemratingdetailarraytype.itemratingdetails', + 'getordertransactionsresponse.itemtransactionidarraytype.itemtransactionid', + 'addfixedpriceitemresponse.itemtype.giftservices', + 'additemresponse.itemtype.giftservices', + 'additemsresponse.itemtype.giftservices', + 'addsellingmanagertemplateresponse.itemtype.giftservices', + 'getitemrecommendationsresponse.itemtype.giftservices', + 'relistfixedpriceitemresponse.itemtype.giftservices', + 'relistitemresponse.itemtype.giftservices', + 'revisefixedpriceitemresponse.itemtype.giftservices', + 'reviseitemresponse.itemtype.giftservices', + 'revisesellingmanagertemplateresponse.itemtype.giftservices', + 'verifyadditemresponse.itemtype.giftservices', + 'verifyrelistitemresponse.itemtype.giftservices', + 'addfixedpriceitemresponse.itemtype.listingenhancement', + 'additemresponse.itemtype.listingenhancement', + 'additemsresponse.itemtype.listingenhancement', + 'addsellingmanagertemplateresponse.itemtype.listingenhancement', + 'getitemrecommendationsresponse.itemtype.listingenhancement', + 'relistfixedpriceitemresponse.itemtype.listingenhancement', + 'relistitemresponse.itemtype.listingenhancement', + 'revisefixedpriceitemresponse.itemtype.listingenhancement', + 'reviseitemresponse.itemtype.listingenhancement', + 'revisesellingmanagertemplateresponse.itemtype.listingenhancement', + 'verifyadditemresponse.itemtype.listingenhancement', + 'verifyrelistitemresponse.itemtype.listingenhancement', + 'addfixedpriceitemresponse.itemtype.paymentmethods', + 'additemresponse.itemtype.paymentmethods', + 'additemfromsellingmanagertemplateresponse.itemtype.paymentmethods', + 'additemsresponse.itemtype.paymentmethods', + 'addsellingmanagertemplateresponse.itemtype.paymentmethods', + 'relistfixedpriceitemresponse.itemtype.paymentmethods', + 'relistitemresponse.itemtype.paymentmethods', + 'revisefixedpriceitemresponse.itemtype.paymentmethods', + 'reviseitemresponse.itemtype.paymentmethods', + 'verifyadditemresponse.itemtype.paymentmethods', + 'verifyrelistitemresponse.itemtype.paymentmethods', + 'addfixedpriceitemresponse.itemtype.shiptolocations', + 'additemresponse.itemtype.shiptolocations', + 'additemsresponse.itemtype.shiptolocations', + 'addsellingmanagertemplateresponse.itemtype.shiptolocations', + 'getitemrecommendationsresponse.itemtype.shiptolocations', + 'relistfixedpriceitemresponse.itemtype.shiptolocations', + 'relistitemresponse.itemtype.shiptolocations', + 'revisefixedpriceitemresponse.itemtype.shiptolocations', + 'reviseitemresponse.itemtype.shiptolocations', + 'revisesellingmanagertemplateresponse.itemtype.shiptolocations', + 'verifyadditemresponse.itemtype.shiptolocations', + 'verifyrelistitemresponse.itemtype.shiptolocations', + 'addfixedpriceitemresponse.itemtype.skypecontactoption', + 'additemresponse.itemtype.skypecontactoption', + 'additemsresponse.itemtype.skypecontactoption', + 'addsellingmanagertemplateresponse.itemtype.skypecontactoption', + 'relistfixedpriceitemresponse.itemtype.skypecontactoption', + 'relistitemresponse.itemtype.skypecontactoption', + 'revisefixedpriceitemresponse.itemtype.skypecontactoption', + 'reviseitemresponse.itemtype.skypecontactoption', + 'revisesellingmanagertemplateresponse.itemtype.skypecontactoption', + 'verifyadditemresponse.itemtype.skypecontactoption', + 'verifyrelistitemresponse.itemtype.skypecontactoption', + 'addfixedpriceitemresponse.itemtype.crossbordertrade', + 'additemresponse.itemtype.crossbordertrade', + 'additemsresponse.itemtype.crossbordertrade', + 'addsellingmanagertemplateresponse.itemtype.crossbordertrade', + 'relistfixedpriceitemresponse.itemtype.crossbordertrade', + 'relistitemresponse.itemtype.crossbordertrade', + 'revisefixedpriceitemresponse.itemtype.crossbordertrade', + 'reviseitemresponse.itemtype.crossbordertrade', + 'revisesellingmanagertemplateresponse.itemtype.crossbordertrade', + 'verifyadditemresponse.itemtype.crossbordertrade', + 'verifyrelistitemresponse.itemtype.crossbordertrade', + 'getitemresponse.itemtype.paymentallowedsite', + 'getsellingmanagertemplatesresponse.itemtype.paymentallowedsite', + 'getcategoryfeaturesresponse.listingdurationdefinitiontype.duration', + 'getcategoryfeaturesresponse.listingdurationdefinitionstype.listingduration', + 'getcategoryfeaturesresponse.listingenhancementdurationreferencetype.duration', + 'addfixedpriceitemresponse.listingrecommendationtype.value', + 'additemresponse.listingrecommendationtype.value', + 'additemsresponse.listingrecommendationtype.value', + 'relistfixedpriceitemresponse.listingrecommendationtype.value', + 'relistitemresponse.listingrecommendationtype.value', + 'revisefixedpriceitemresponse.listingrecommendationtype.value', + 'reviseitemresponse.listingrecommendationtype.value', + 'verifyadditemresponse.listingrecommendationtype.value', + 'verifyaddfixedpriceitemresponse.listingrecommendationtype.value', + 'verifyrelistitemresponse.listingrecommendationtype.value', + 'addfixedpriceitemresponse.listingrecommendationstype.recommendation', + 'additemresponse.listingrecommendationstype.recommendation', + 'additemsresponse.listingrecommendationstype.recommendation', + 'relistfixedpriceitemresponse.listingrecommendationstype.recommendation', + 'relistitemresponse.listingrecommendationstype.recommendation', + 'revisefixedpriceitemresponse.listingrecommendationstype.recommendation', + 'reviseitemresponse.listingrecommendationstype.recommendation', + 'verifyadditemresponse.listingrecommendationstype.recommendation', + 'verifyaddfixedpriceitemresponse.listingrecommendationstype.recommendation', + 'verifyrelistitemresponse.listingrecommendationstype.recommendation', + 'getitemrecommendationsresponse.listingtiparraytype.listingtip', + 'getnotificationsusageresponse.markupmarkdownhistorytype.markupmarkdownevent', + 'getebaydetailsresponse.maximumbuyerpolicyviolationsdetailstype.policyviolationduration', + 'getebaydetailsresponse.maximumitemrequirementsdetailstype.maximumitemcount', + 'getebaydetailsresponse.maximumitemrequirementsdetailstype.minimumfeedbackscore', + 'getebaydetailsresponse.maximumunpaiditemstrikescountdetailstype.count', + 'getebaydetailsresponse.maximumunpaiditemstrikesinfodetailstype.maximumunpaiditemstrikesduration', + 'getadformatleadsresponse.membermessageexchangearraytype.membermessageexchange', + 'getadformatleadsresponse.membermessageexchangetype.response', + 'getmembermessagesresponse.membermessageexchangetype.messagemedia', + 'addmembermessageaaqtopartnerresponse.membermessagetype.recipientid', + 'addmembermessagertqresponse.membermessagetype.recipientid', + 'addmembermessagesaaqtobidderresponse.membermessagetype.recipientid', + 'addmembermessageaaqtopartnerresponse.membermessagetype.messagemedia', + 'addmembermessagertqresponse.membermessagetype.messagemedia', + 'addmembermessagecemresponse.membermessagetype.messagemedia', + 'addmembermessageaaqtosellerresponse.membermessagetype.messagemedia', + 'getebaydetailsresponse.minimumfeedbackscoredetailstype.feedbackscore', + 'relistfixedpriceitemresponse.modifynamearraytype.modifyname', + 'revisefixedpriceitemresponse.modifynamearraytype.modifyname', + 'getmymessagesresponse.mymessagesexternalmessageidarraytype.externalmessageid', + 'getmymessagesresponse.mymessagesmessagearraytype.message', + 'deletemymessagesresponse.mymessagesmessageidarraytype.messageid', + 'getmymessagesresponse.mymessagesmessagetype.messagemedia', + 'getmymessagesresponse.mymessagessummarytype.foldersummary', + 'getmyebaybuyingresponse.myebayfavoritesearchlisttype.favoritesearch', + 'getmyebaybuyingresponse.myebayfavoritesearchtype.searchflag', + 'getmyebaybuyingresponse.myebayfavoritesearchtype.sellerid', + 'getmyebaybuyingresponse.myebayfavoritesearchtype.selleridexclude', + 'getmyebaybuyingresponse.myebayfavoritesellerlisttype.favoriteseller', + 'getcategoryspecificsresponse.namerecommendationtype.valuerecommendation', + 'getitemrecommendationsresponse.namerecommendationtype.valuerecommendation', + 'addfixedpriceitemresponse.namevaluelistarraytype.namevaluelist', + 'additemresponse.namevaluelistarraytype.namevaluelist', + 'additemsresponse.namevaluelistarraytype.namevaluelist', + 'addsellingmanagertemplateresponse.namevaluelistarraytype.namevaluelist', + 'addliveauctionitemresponse.namevaluelistarraytype.namevaluelist', + 'relistfixedpriceitemresponse.namevaluelistarraytype.namevaluelist', + 'relistitemresponse.namevaluelistarraytype.namevaluelist', + 'revisefixedpriceitemresponse.namevaluelistarraytype.namevaluelist', + 'reviseitemresponse.namevaluelistarraytype.namevaluelist', + 'revisesellingmanagertemplateresponse.namevaluelistarraytype.namevaluelist', + 'reviseliveauctionitemresponse.namevaluelistarraytype.namevaluelist', + 'verifyaddfixedpriceitemresponse.namevaluelistarraytype.namevaluelist', + 'verifyadditemresponse.namevaluelistarraytype.namevaluelist', + 'verifyrelistitemresponse.namevaluelistarraytype.namevaluelist', + 'additemresponse.namevaluelisttype.value', + 'additemfromsellingmanagertemplateresponse.namevaluelisttype.value', + 'additemsresponse.namevaluelisttype.value', + 'addsellingmanagertemplateresponse.namevaluelisttype.value', + 'addliveauctionitemresponse.namevaluelisttype.value', + 'relistitemresponse.namevaluelisttype.value', + 'reviseitemresponse.namevaluelisttype.value', + 'revisesellingmanagertemplateresponse.namevaluelisttype.value', + 'reviseliveauctionitemresponse.namevaluelisttype.value', + 'verifyadditemresponse.namevaluelisttype.value', + 'verifyrelistitemresponse.namevaluelisttype.value', + 'getnotificationsusageresponse.notificationdetailsarraytype.notificationdetails', + 'setnotificationpreferencesresponse.notificationenablearraytype.notificationenable', + 'setnotificationpreferencesresponse.notificationuserdatatype.summaryschedule', + 'getebaydetailsresponse.numberofpolicyviolationsdetailstype.count', + 'getallbiddersresponse.offerarraytype.offer', + 'gethighbiddersresponse.offerarraytype.offer', + 'getordersresponse.orderarraytype.order', + 'getordersresponse.orderidarraytype.orderid', + 'getmyebaybuyingresponse.ordertransactionarraytype.ordertransaction', + 'addorderresponse.ordertype.paymentmethods', + 'getordertransactionsresponse.ordertype.externaltransaction', + 'getordersresponse.ordertype.externaltransaction', + 'getordersresponse.paymentinformationcodetype.payment', + 'getordersresponse.paymentinformationtype.payment', + 'getordersresponse.paymenttransactioncodetype.paymentreferenceid', + 'getordersresponse.paymenttransactiontype.paymentreferenceid', + 'getsellerdashboardresponse.performancedashboardtype.site', + 'getordersresponse.pickupdetailstype.pickupoptions', + 'additemresponse.picturedetailstype.pictureurl', + 'additemsresponse.picturedetailstype.pictureurl', + 'addsellingmanagertemplateresponse.picturedetailstype.pictureurl', + 'getitemrecommendationsresponse.picturedetailstype.pictureurl', + 'relistitemresponse.picturedetailstype.pictureurl', + 'reviseitemresponse.picturedetailstype.pictureurl', + 'revisesellingmanagertemplateresponse.picturedetailstype.pictureurl', + 'verifyadditemresponse.picturedetailstype.pictureurl', + 'verifyrelistitemresponse.picturedetailstype.pictureurl', + 'getitemresponse.picturedetailstype.externalpictureurl', + 'addfixedpriceitemresponse.picturestype.variationspecificpictureset', + 'verifyaddfixedpriceitemresponse.picturestype.variationspecificpictureset', + 'relistfixedpriceitemresponse.picturestype.variationspecificpictureset', + 'revisefixedpriceitemresponse.picturestype.variationspecificpictureset', + 'getsellerdashboardresponse.powersellerdashboardtype.alert', + 'getbidderlistresponse.productlistingdetailstype.copyright', + 'getitemrecommendationsresponse.productrecommendationstype.product', + 'addfixedpriceitemresponse.productsuggestionstype.productsuggestion', + 'additemresponse.productsuggestionstype.productsuggestion', + 'relistfixedpriceitemresponse.productsuggestionstype.productsuggestion', + 'relistitemresponse.productsuggestionstype.productsuggestion', + 'revisefixedpriceitemresponse.productsuggestionstype.productsuggestion', + 'reviseitemresponse.productsuggestionstype.productsuggestion', + 'verifyadditemresponse.productsuggestionstype.productsuggestion', + 'verifyrelistitemresponse.productsuggestionstype.productsuggestion', + 'getpromotionalsaledetailsresponse.promotionalsalearraytype.promotionalsale', + 'addfixedpriceitemresponse.recommendationtype.recommendedvalue', + 'additemresponse.recommendationtype.recommendedvalue', + 'additemsresponse.recommendationtype.recommendedvalue', + 'relistfixedpriceitemresponse.recommendationtype.recommendedvalue', + 'relistitemresponse.recommendationtype.recommendedvalue', + 'revisefixedpriceitemresponse.recommendationtype.recommendedvalue', + 'reviseitemresponse.recommendationtype.recommendedvalue', + 'verifyadditemresponse.recommendationtype.recommendedvalue', + 'verifyaddfixedpriceitemresponse.recommendationtype.recommendedvalue', + 'verifyrelistitemresponse.recommendationtype.recommendedvalue', + 'getcategoryspecificsresponse.recommendationvalidationrulestype.relationship', + 'getitemrecommendationsresponse.recommendationvalidationrulestype.relationship', + 'getcategoryspecificsresponse.recommendationstype.namerecommendation', + 'getitemrecommendationsresponse.recommendationstype.namerecommendation', + 'getuserresponse.recoupmentpolicyconsenttype.site', + 'getordersresponse.refundarraytype.refund', + 'getordersresponse.refundfundingsourcearraytype.refundfundingsource', + 'getitemtransactionsresponse.refundfundingsourcearraytype.refundfundingsource', + 'getordertransactionsresponse.refundfundingsourcearraytype.refundfundingsource', + 'getsellertransactionsresponse.refundfundingsourcearraytype.refundfundingsource', + 'getordersresponse.refundinformationtype.refund', + 'getordersresponse.refundlinearraytype.refundline', + 'getitemtransactionsresponse.refundlinearraytype.refundline', + 'getordertransactionsresponse.refundlinearraytype.refundline', + 'getsellertransactionsresponse.refundlinearraytype.refundline', + 'getordersresponse.refundtransactionarraytype.refundtransaction', + 'getitemtransactionsresponse.refundtransactionarraytype.refundtransaction', + 'getordertransactionsresponse.refundtransactionarraytype.refundtransaction', + 'getsellertransactionsresponse.refundtransactionarraytype.refundtransaction', + 'getordersresponse.requiredselleractionarraytype.requiredselleraction', + 'getebaydetailsresponse.returnpolicydetailstype.refund', + 'getebaydetailsresponse.returnpolicydetailstype.returnswithin', + 'getebaydetailsresponse.returnpolicydetailstype.returnsaccepted', + 'getebaydetailsresponse.returnpolicydetailstype.warrantyoffered', + 'getebaydetailsresponse.returnpolicydetailstype.warrantytype', + 'getebaydetailsresponse.returnpolicydetailstype.warrantyduration', + 'getebaydetailsresponse.returnpolicydetailstype.shippingcostpaidby', + 'getebaydetailsresponse.returnpolicydetailstype.restockingfeevalue', + 'getsellertransactionsresponse.skuarraytype.sku', + 'getsellerlistresponse.skuarraytype.sku', + 'getsellerdashboardresponse.selleraccountdashboardtype.alert', + 'getitemtransactionsresponse.sellerdiscountstype.sellerdiscount', + 'getordersresponse.sellerdiscountstype.sellerdiscount', + 'getordertransactionsresponse.sellerdiscountstype.sellerdiscount', + 'getsellertransactionsresponse.sellerdiscountstype.sellerdiscount', + 'getuserpreferencesresponse.sellerexcludeshiptolocationpreferencestype.excludeshiptolocation', + 'getuserpreferencesresponse.sellerfavoriteitempreferencestype.favoriteitemid', + 'getfeedbackresponse.sellerratingsummaryarraytype.averageratingsummary', + 'getbidderlistresponse.sellerebaypaymentprocessconsentcodetype.useragreementinfo', + 'getsellingmanagertemplateautomationruleresponse.sellingmanagerautolistaccordingtoscheduletype.dayofweek', + 'getsellingmanagerinventoryfolderresponse.sellingmanagerfolderdetailstype.childfolder', + 'revisesellingmanagerinventoryfolderresponse.sellingmanagerfolderdetailstype.childfolder', + 'getsellingmanagersalerecordresponse.sellingmanagersoldordertype.sellingmanagersoldtransaction', + 'getsellingmanagersoldlistingsresponse.sellingmanagersoldordertype.sellingmanagersoldtransaction', + 'getsellingmanagersalerecordresponse.sellingmanagersoldordertype.vatrate', + 'getsellingmanagersoldlistingsresponse.sellingmanagersoldtransactiontype.listedon', + 'getsellingmanagertemplatesresponse.sellingmanagertemplatedetailsarraytype.sellingmanagertemplatedetails', + 'completesaleresponse.shipmentlineitemtype.lineitem', + 'addshipmentresponse.shipmentlineitemtype.lineitem', + 'reviseshipmentresponse.shipmentlineitemtype.lineitem', + 'revisesellingmanagersalerecordresponse.shipmentlineitemtype.lineitem', + 'setshipmenttrackinginforesponse.shipmentlineitemtype.lineitem', + 'completesaleresponse.shipmenttype.shipmenttrackingdetails', + 'getitemresponse.shippingdetailstype.shippingserviceoptions', + 'getsellingmanagertemplatesresponse.shippingdetailstype.shippingserviceoptions', + 'addfixedpriceitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'additemresponse.shippingdetailstype.internationalshippingserviceoption', + 'additemsresponse.shippingdetailstype.internationalshippingserviceoption', + 'addsellingmanagertemplateresponse.shippingdetailstype.internationalshippingserviceoption', + 'addorderresponse.shippingdetailstype.internationalshippingserviceoption', + 'getitemrecommendationsresponse.shippingdetailstype.internationalshippingserviceoption', + 'relistfixedpriceitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'relistitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'revisefixedpriceitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'reviseitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'revisesellingmanagertemplateresponse.shippingdetailstype.internationalshippingserviceoption', + 'verifyadditemresponse.shippingdetailstype.internationalshippingserviceoption', + 'verifyrelistitemresponse.shippingdetailstype.internationalshippingserviceoption', + 'getsellerlistresponse.shippingdetailstype.excludeshiptolocation', + 'getitemtransactionsresponse.shippingdetailstype.shipmenttrackingdetails', + 'getsellertransactionsresponse.shippingdetailstype.shipmenttrackingdetails', + 'getshippingdiscountprofilesresponse.shippinginsurancetype.flatrateinsurancerangecost', + 'addfixedpriceitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'additemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'additemsresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'verifyadditemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'verifyaddfixedpriceitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'verifyrelistitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'relistfixedpriceitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'relistitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'revisefixedpriceitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'reviseitemresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'addsellingmanagertemplateresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'revisesellingmanagertemplateresponse.shippingservicecostoverridelisttype.shippingservicecostoverride', + 'getebaydetailsresponse.shippingservicedetailstype.servicetype', + 'getebaydetailsresponse.shippingservicedetailstype.shippingpackage', + 'getebaydetailsresponse.shippingservicedetailstype.shippingcarrier', + 'getebaydetailsresponse.shippingservicedetailstype.deprecationdetails', + 'getebaydetailsresponse.shippingservicedetailstype.shippingservicepackagedetails', + 'getordersresponse.shippingserviceoptionstype.shippingpackageinfo', + 'getcategoryfeaturesresponse.sitedefaultstype.listingduration', + 'getcategoryfeaturesresponse.sitedefaultstype.paymentmethod', + 'uploadsitehostedpicturesresponse.sitehostedpicturedetailstype.picturesetmember', + 'getcategory2csresponse.sitewidecharacteristicstype.excludecategoryid', + 'getstoreoptionsresponse.storecolorschemearraytype.colorscheme', + 'getstoreresponse.storecustomcategoryarraytype.customcategory', + 'getstoreresponse.storecustomcategorytype.childcategory', + 'setstorecategoriesresponse.storecustomcategorytype.childcategory', + 'setstoreresponse.storecustomlistingheadertype.linktoinclude', + 'getstorecustompageresponse.storecustompagearraytype.custompage', + 'getstoreoptionsresponse.storelogoarraytype.logo', + 'getcategoryfeaturesresponse.storeownerextendedlistingdurationstype.duration', + 'getstoreoptionsresponse.storesubscriptionarraytype.subscription', + 'getstoreoptionsresponse.storethemearraytype.theme', + 'getsuggestedcategoriesresponse.suggestedcategoryarraytype.suggestedcategory', + 'getuserpreferencesresponse.supportedsellerprofilestype.supportedsellerprofile', + 'settaxtableresponse.taxtabletype.taxjurisdiction', + 'getitemtransactionsresponse.taxestype.taxdetails', + 'getordersresponse.taxestype.taxdetails', + 'getordertransactionsresponse.taxestype.taxdetails', + 'getsellertransactionsresponse.taxestype.taxdetails', + 'getdescriptiontemplatesresponse.themegrouptype.themeid', + 'getuserresponse.topratedsellerdetailstype.topratedprogram', + 'getordersresponse.transactionarraytype.transaction', + 'getitemtransactionsresponse.transactiontype.externaltransaction', + 'getsellertransactionsresponse.transactiontype.externaltransaction', + 'getebaydetailsresponse.unitofmeasurementdetailstype.unitofmeasurement', + 'getebaydetailsresponse.unitofmeasurementtype.alternatetext', + 'getuserpreferencesresponse.unpaiditemassistancepreferencestype.excludeduser', + 'getsellerlistresponse.useridarraytype.userid', + 'getuserresponse.usertype.usersubscription', + 'getuserresponse.usertype.skypeid', + 'addfixedpriceitemresponse.variationspecificpicturesettype.pictureurl', + 'revisefixedpriceitemresponse.variationspecificpicturesettype.pictureurl', + 'relistfixedpriceitemresponse.variationspecificpicturesettype.pictureurl', + 'verifyaddfixedpriceitemresponse.variationspecificpicturesettype.pictureurl', + 'getitemresponse.variationspecificpicturesettype.externalpictureurl', + 'getitemsresponse.variationspecificpicturesettype.externalpictureurl', + 'addfixedpriceitemresponse.variationstype.variation', + 'revisefixedpriceitemresponse.variationstype.variation', + 'relistfixedpriceitemresponse.variationstype.variation', + 'verifyaddfixedpriceitemresponse.variationstype.variation', + 'addfixedpriceitemresponse.variationstype.pictures', + 'revisefixedpriceitemresponse.variationstype.pictures', + 'relistfixedpriceitemresponse.variationstype.pictures', + 'verifyaddfixedpriceitemresponse.variationstype.pictures', + 'getveroreasoncodedetailsresponse.veroreasoncodedetailstype.verositedetail', + 'veroreportitemsresponse.veroreportitemtype.region', + 'veroreportitemsresponse.veroreportitemtype.country', + 'veroreportitemsresponse.veroreportitemstype.reportitem', + 'getveroreportstatusresponse.veroreporteditemdetailstype.reporteditem', + 'getveroreasoncodedetailsresponse.verositedetailtype.reasoncodedetail', + 'getebaydetailsresponse.verifieduserrequirementsdetailstype.feedbackscore', + 'getwantitnowsearchresultsresponse.wantitnowpostarraytype.wantitnowpost', + ] + + def build_request_headers(self, verb): + headers = { + "X-EBAY-API-COMPATIBILITY-LEVEL": self.config.get('compatibility', ''), + "X-EBAY-API-DEV-NAME": self.config.get('devid', ''), + "X-EBAY-API-APP-NAME": self.config.get('appid', ''), + "X-EBAY-API-CERT-NAME": self.config.get('certid', ''), + "X-EBAY-API-SITEID": self.config.get('siteid', ''), + "X-EBAY-API-CALL-NAME": self.verb, + "Content-Type": "text/xml" + } + if self.config.get('iaf_token', None): + headers["X-EBAY-API-IAF-TOKEN"] = self.config.get('iaf_token') + + return headers + + def build_request_data(self, verb, data, verb_attrs): + xml = u"" + xml += u"<" + self.verb + u"Request xmlns=\"urn:ebay:apis:eBLBaseComponents\">" + if not self.config.get('iaf_token', None): + xml += u"" + if self.config.get('token', None): + xml += u"%s" % self.config.get('token') + elif self.config.get('username', None): + xml += u"%s" % self.config.get('username', '') + if self.config.get('password', None): + xml += u"%s" % self.config.get('password', '') + xml += u"" + + xml += dict2xml(data) + #print dict2xml(data) + + xml += u"" + return xml + + def warnings(self): + warning_string = "" + + if len(self._resp_body_warnings) > 0: + warning_string = "%s: %s" \ + % (self.verb, ", ".join(self._resp_body_warnings)) + + return warning_string + + def _get_resp_body_errors(self): + """Parses the response content to pull errors. + + Child classes should override this method based on what the errors in the + XML response body look like. They can choose to look at the 'ack', + 'Errors', 'errorMessage' or whatever other fields the service returns. + the implementation below is the original code that was part of error() + """ + + if self._resp_body_errors and len(self._resp_body_errors) > 0: + return self._resp_body_errors + + errors = [] + warnings = [] + resp_codes = [] + + if self.verb is None: + return errors + + dom = self.response.dom() + if dom is None: + return errors + + for e in dom.findall('Errors'): + eSeverity = None + eClass = None + eShortMsg = None + eLongMsg = None + eCode = None + + try: + eSeverity = e.findall('SeverityCode')[0].text + except IndexError: + pass + + try: + eClass = e.findall('ErrorClassification')[0].text + except IndexError: + pass + + try: + eCode = e.findall('ErrorCode')[0].text + except IndexError: + pass + + try: + eShortMsg = e.findall('ShortMessage')[0].text + except IndexError: + pass + + try: + eLongMsg = e.findall('LongMessage')[0].text + except IndexError: + pass + + try: + eCode = e.findall('ErrorCode')[0].text + if int(eCode) not in resp_codes: + resp_codes.append(int(eCode)) + except IndexError: + pass + + msg = "Class: %s, Severity: %s, Code: %s, %s%s" \ + % (eClass, eSeverity, eCode, eShortMsg, eLongMsg) + + if eSeverity == 'Warning': + warnings.append(msg) + else: + errors.append(msg) + + self._resp_body_warnings = warnings + self._resp_body_errors = errors + self._resp_codes = resp_codes + + if self.config.get('warnings') and len(warnings) > 0: + log.warn("%s: %s\n\n" % (self.verb, "\n".join(warnings))) + + if self.response.reply.Ack == 'Failure': + if self.config.get('errors'): + log.error("%s: %s\n\n" % (self.verb, "\n".join(errors))) + + return errors + + return [] diff --git a/ebaysdk/utils.py b/ebaysdk/utils.py new file mode 100644 index 0000000..3152e12 --- /dev/null +++ b/ebaysdk/utils.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' +import sys + +from lxml import etree as ET + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if sys.version_info[0] < 3: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8', 'ignore') + return klass + +def get_dom_tree(xml): + tree = ET.fromstring(xml) + return tree.getroottree().getroot() + +def attribute_check(root): + attrs = [] + value = None + + if isinstance(root, dict): + if '#text' in root: + value = root['#text'] + if '@attrs' in root: + for ak, av in sorted(root.pop('@attrs').items()): + attrs.append('%s="%s"' % (ak, av)) + + return attrs, value + +def smart_encode(value): + try: + if sys.version_info[0] < 3: + return unicode(value) #.encode('utf-8', 'ignore') + else: + return value + #return str(value) + + except UnicodeDecodeError: + return value + +def to_xml(root): + return dict2xml(root) + +def dict2xml(root): + ''' + Doctests: + >>> dict1 = {'Items': {'ItemId': ['1234', '2222']}} + >>> dict2xml(dict1) + '12342222' + >>> dict2 = { + ... 'searchFilter': {'categoryId': {'#text': 222, '@attrs': {'site': 'US'} }}, + ... 'paginationInput': { + ... 'pageNumber': '1', + ... 'pageSize': '25' + ... }, + ... 'sortOrder': 'StartTimeNewest' + ... } + >>> dict2xml(dict2) + '125222StartTimeNewest' + >>> dict3 = { + ... 'parent': {'child': {'#text': 222, '@attrs': {'site': 'US', 'id': 1234}}} + ... } + >>> dict2xml(dict3) + '222' + >>> dict5 = { + ... 'parent': {'child': {'@attrs': {'site': 'US', 'id': 1234}, }} + ... } + >>> dict2xml(dict5) + '' + >>> dict4 = { + ... 'searchFilter': {'categoryId': {'#text': 222, '@attrs': {'site': 'US'} }}, + ... 'paginationInput': { + ... 'pageNumber': '1', + ... 'pageSize': '25' + ... }, + ... 'itemFilter': [ + ... {'name': 'Condition', + ... 'value': 'Used'}, + ... {'name': 'LocatedIn', + ... 'value': 'GB'}, + ... ], + ... 'sortOrder': 'StartTimeNewest' + ... } + >>> dict2xml(dict4) + 'ConditionUsedLocatedInGB125222StartTimeNewest' + >>> dict2xml({}) + '' + >>> dict2xml('b') + 'b' + >>> dict2xml(None) + '' + >>> common_attrs = {'xmlns:xs': 'http://www.w3.org/2001/XMLSchema', 'xsi:type': 'xs:string'} + >>> attrdict = { 'attributeAssertion': [ + ... {'@attrs': {'Name': 'DevId', 'NameFormat': 'String', 'FriendlyName': 'DeveloperID'}, + ... 'urn:AttributeValue': { + ... '@attrs': common_attrs, + ... '#text': 'mydevid' + ... }, + ... }, + ... {'@attrs': {'Name': 'AppId', 'NameFormat': 'String', 'FriendlyName': 'ApplicationID'}, + ... 'urn:AttributeValue': { + ... '@attrs': common_attrs, + ... '#text': 'myappid', + ... }, + ... }, + ... {'@attrs': {'Name': 'CertId', 'NameFormat': 'String', 'FriendlyName': 'Certificate'}, + ... 'urn:AttributeValue': { + ... '@attrs': common_attrs, + ... '#text': 'mycertid', + ... }, + ... }, + ... ], + ... } + >>> print(dict2xml(attrdict)) + mydevidmyappidmycertid + ''' + + xml = u'' + if root is None: + return xml + + if isinstance(root, dict): + for key in sorted(root.keys()): + + if isinstance(root[key], dict): + attrs, value = attribute_check(root[key]) + + if not value: + value = dict2xml(root[key]) + elif isinstance(value, dict): + value = dict2xml(value) + + attrs_sp = u'' + if len(attrs) > 0: + attrs_sp = u' ' + + xml = u'%(xml)s<%(tag)s%(attrs_sp)s%(attrs)s>%(value)s' % \ + {'tag': key, 'xml': xml, 'attrs': u' '.join(attrs), + 'value': smart_encode(value), 'attrs_sp': attrs_sp} + + elif isinstance(root[key], list): + + for item in root[key]: + attrs, value = attribute_check(item) + + if not value: + value = dict2xml(item) + elif isinstance(value, dict): + value = dict2xml(value) + + attrs_sp = '' + if len(attrs) > 0: + attrs_sp = ' ' + + xml = u'%(xml)s<%(tag)s%(attrs_sp)s%(attrs)s>%(value)s' % \ + {'xml': xml, 'tag': key, 'attrs': ' '.join(attrs), + 'value': smart_encode(value), 'attrs_sp': attrs_sp} + + else: + value = root[key] + xml = u'%(xml)s<%(tag)s>%(value)s' % \ + {'xml': xml, 'tag': key, 'value': smart_encode(value)} + + elif isinstance(root, str) or isinstance(root, int) \ + or isinstance(root, unicode) or isinstance(root, long) \ + or isinstance(root, float): + xml = u'%s%s' % (xml, root) + else: + raise Exception('Unable to serialize node of type %s (%s)' % \ + (type(root), root)) + + return xml + +def getValue(response_dict, *args, **kwargs): + args_a = [w for w in args] + first = args_a[0] + args_a.remove(first) + + h = kwargs.get('mydict', {}) + if h: + h = h.get(first, {}) + else: + h = response_dict.get(first, {}) + + if len(args) == 1: + try: + return h.get('value', None) + except: + return h + + last = args_a.pop() + + for a in args_a: + h = h.get(a, {}) + + h = h.get(last, {}) + + try: + return h.get('value', None) + except: + return h + +def getNodeText(node): + "Returns the node's text string." + + rc = [] + + if hasattr(node, 'childNodes'): + for cn in node.childNodes: + if cn.nodeType == cn.TEXT_NODE: + rc.append(cn.data) + elif cn.nodeType == cn.CDATA_SECTION_NODE: + rc.append(cn.data) + + return ''.join(rc) + +def perftest_dict2xml(): + sample_dict = { + 'searchFilter': {'categoryId': {'#text': 222, '@attrs': {'site': 'US'} }}, + 'paginationInput': { + 'pageNumber': '1', + 'pageSize': '25' + }, + 'itemFilter': [ + {'name': 'Condition', + 'value': 'Used'}, + {'name': 'LocatedIn', + 'value': 'GB'}, + ], + 'sortOrder': 'StartTimeNewest' + } + + xml = dict2xml(sample_dict) + +if __name__ == '__main__': + + import timeit + print("perftest_dict2xml() %s" % \ + timeit.timeit("perftest_dict2xml()", number=50000, + setup="from __main__ import perftest_dict2xml")) + + import doctest + failure_count, test_count = doctest.testmod() + sys.exit(failure_count) + diff --git a/samples/common.py b/samples/common.py new file mode 100644 index 0000000..83f00e4 --- /dev/null +++ b/samples/common.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +def dump(api, full=False): + + print("\n") + + if api.warnings(): + print("Warnings" + api.warnings()) + + if api.response.content: + print("Call Success: %s in length" % len(api.response.content)) + + print("Response code: %s" % api.response_code()) + print("Response DOM1: %s" % api.response_dom()) # deprecated + print("Response ETREE: %s" % api.response.dom()) + + if full: + print(api.response.content) + print(api.response.json()) + print("Response Reply: %s" % api.response.reply) + else: + dictstr = "%s" % api.response.dict() + print("Response dictionary: %s..." % dictstr[:150]) + replystr = "%s" % api.response.reply + print("Response Reply: %s" % replystr[:150]) \ No newline at end of file diff --git a/samples/finding.py b/samples/finding.py new file mode 100644 index 0000000..6572b0f --- /dev/null +++ b/samples/finding.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.finding import Connection as finding +from ebaysdk.exception import ConnectionError + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + + (opts, args) = parser.parse_args() + return opts, args + + +def run(opts): + + try: + api = finding(debug=opts.debug, appid=opts.appid, + config_file=opts.yaml, warnings=True) + + api_request = { + #'keywords': u'niño', + 'keywords': u'GRAMMY Foundation®', + 'itemFilter': [ + {'name': 'Condition', + 'value': 'Used'}, + {'name': 'LocatedIn', + 'value': 'GB'}, + ], + 'affiliate': {'trackingId': 1}, + 'sortOrder': 'CountryDescending', + } + + response = api.execute('findItemsAdvanced', api_request) + + dump(api) + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def run2(opts): + try: + api = finding(debug=opts.debug, appid=opts.appid, config_file=opts.yaml) + + response = api.execute('findItemsByProduct', + '530390311') + + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def run_motors(opts): + api = finding(siteid='EBAY-MOTOR', debug=opts.debug, appid=opts.appid, config_file=opts.yaml, + warnings=True) + + api.execute('findItemsAdvanced', { + 'keywords': 'tesla', + }) + + if api.error(): + raise Exception(api.error()) + + if api.response_content(): + print "Call Success: %s in length" % len(api.response_content()) + + print "Response code: %s" % api.response_code() + print "Response DOM: %s" % api.response_dom() + + dictstr = "%s" % api.response_dict() + print "Response dictionary: %s..." % dictstr[:250] + +if __name__ == "__main__": + print("Finding samples for SDK version %s" % ebaysdk.get_version()) + (opts, args) = init_options() + run(opts) + run2(opts) + run_motors(opts) diff --git a/samples/finditem.py b/samples/finditem.py new file mode 100644 index 0000000..9af3400 --- /dev/null +++ b/samples/finditem.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.soa.finditem import Connection as FindItem +from ebaysdk.shopping import Connection as Shopping +from ebaysdk.utils import getNodeText +from ebaysdk.exception import ConnectionError + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + parser.add_option("-c", "--consumer_id", + dest="consumer_id", default=None, + help="Specifies the eBay consumer_id id to use.") + + (opts, args) = parser.parse_args() + return opts, args + +def run(opts): + + try: + + shopping = Shopping(debug=opts.debug, appid=opts.appid, + config_file=opts.yaml, warnings=False) + + response = shopping.execute('FindPopularItems', + {'QueryKeywords': 'Python'}) + + nodes = response.dom().xpath('//ItemID') + itemIds = [n.text for n in nodes] + + api = FindItem(debug=opts.debug, + consumer_id=opts.consumer_id, config_file=opts.yaml) + + records = api.find_items_by_ids([itemIds[0]]) + + for r in records: + print("ID(%s) TITLE(%s)" % (r['ITEM_ID'], r['TITLE'][:35])) + + dump(api) + + records = api.find_items_by_ids(itemIds) + + for r in records: + print("ID(%s) TITLE(%s)" % (r['ITEM_ID'], r['TITLE'][:35])) + + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + + +if __name__ == "__main__": + print("FindItem samples for SDK version %s" % ebaysdk.get_version()) + (opts, args) = init_options() + run(opts) diff --git a/samples/merchandising.py b/samples/merchandising.py new file mode 100644 index 0000000..c5cefa4 --- /dev/null +++ b/samples/merchandising.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.merchandising import Connection as merchandising +from ebaysdk.exception import ConnectionError + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + + (opts, args) = parser.parse_args() + return opts, args + +def run(opts): + try: + api = merchandising(debug=opts.debug, appid=opts.appid, + config_file=opts.yaml, warnings=True) + + response = api.execute('getMostWatchedItems', {'maxResults': 4}) + + dump(api) + except ConnectionError as e: + print(e) + print(e.response.dict()) + + +if __name__ == "__main__": + (opts, args) = init_options() + run(opts) diff --git a/samples/parallel.py b/samples/parallel.py new file mode 100644 index 0000000..788b06b --- /dev/null +++ b/samples/parallel.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump +from ebaysdk.finding import Connection as finding +from ebaysdk.http import Connection as html +from ebaysdk.parallel import Parallel +from ebaysdk.exception import ConnectionError + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + + (opts, args) = parser.parse_args() + return opts, args + + +def run(opts): + + try: + p = Parallel() + apis = [] + + api1 = finding(parallel=p, debug=opts.debug, appid=opts.appid, config_file=opts.yaml) + api1.execute('findItemsAdvanced', {'keywords': 'python'}) + apis.append(api1) + + api4 = html(parallel=p) + api4.execute('http://www.ebay.com/sch/i.html?_nkw=Shirt&_rss=1') + apis.append(api4) + + api2 = finding(parallel=p, debug=opts.debug, appid=opts.appid, config_file=opts.yaml) + api2.execute('findItemsAdvanced', {'keywords': 'perl'}) + apis.append(api2) + + api3 = finding(parallel=p, debug=opts.debug, appid=opts.appid, config_file=opts.yaml) + api3.execute('findItemsAdvanced', {'keywords': 'php'}) + apis.append(api3) + + p.wait() + + if p.error(): + print(p.error()) + + for api in apis: + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +if __name__ == "__main__": + (opts, args) = init_options() + run(opts) diff --git a/samples/request_dictionary.py b/samples/request_dictionary.py new file mode 100644 index 0000000..57fde67 --- /dev/null +++ b/samples/request_dictionary.py @@ -0,0 +1,95 @@ +import os +import sys + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from ebaysdk.utils import dict2xml + +dict1 = {'a': 'b'} + +assert(dict2xml(dict1)=='b') + +''' dict2 XML Output +222 +''' + +dict2 = { + 'tag': { + '#text': 222, + '@attrs': {'site': 'US', 'attr2': 'attr2value'} + } +} + +assert(dict2xml(dict2)=='222') + +''' dict3 XML Output + + Condition + Used + + + LocatedIn + GB + + + More + more + +''' + +dict3 = { + 'itemFilter': [ + {'name': 'Condition', 'value': 'Used'}, + {'name': 'LocatedIn', 'value': 'GB'}, + {'name': 'More', 'value': 'more'}, + ] +} + +assert(dict2xml(dict3)=='ConditionUsedLocatedInGBMoremore') + +''' dict4 XML Output + + tag2 value + +''' + +dict4 = { + 'tag1': { + '#text': {'tag2': 'tag2 value'}, + '@attrs': {'site': 'US', 'attr2': 'attr2value'} + } +} + +assert(dict2xml(dict4)=='tag2 value') + +''' dict5 XML Output + + tag2 value + +''' + +dict5 = { + 'tag1': { + '#text': {'tag2': { + '#text': 'tag2 value', + '@attrs': {'tag2attr': 'myvalue'} + }}, + '@attrs': {'site': 'US', 'tag1attr': 'myvalue'} + } +} + +assert(dict2xml(dict5)=='tag2 value') + +''' dict6 outputSelector +SellerInfo +GalleryInfo +''' + +dict6 = { + 'outputSelector': [ + 'SellerInfo', + 'GalleryInfo' + ] +} + +assert(dict2xml(dict6)=='SellerInfoGalleryInfo') diff --git a/samples/shopping.py b/samples/shopping.py new file mode 100644 index 0000000..0131041 --- /dev/null +++ b/samples/shopping.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +from optparse import OptionParser + +try: + input = raw_input +except NameError: + pass + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.exception import ConnectionError +from ebaysdk.shopping import Connection as Shopping + + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + + (opts, args) = parser.parse_args() + return opts, args + +def run(opts): + api = Shopping(debug=opts.debug, appid=opts.appid, config_file=opts.yaml, + warnings=True) + + print("Shopping samples for SDK version %s" % ebaysdk.get_version()) + + try: + response = api.execute('FindPopularItems', {'QueryKeywords': 'Python'}) + + dump(api) + + print("Matching Titles:") + for item in response.reply.ItemArray.Item: + print(item.Title) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def popularSearches(opts): + + api = Shopping(debug=opts.debug, appid=opts.appid, config_file=opts.yaml, + warnings=True) + + choice = True + + while choice: + + choice = input('Search: ') + + if choice == 'quit': + break + + mySearch = { + "MaxKeywords": 10, + "QueryKeywords": choice, + } + + try: + response = api.execute('FindPopularSearches', mySearch) + + dump(api, full=False) + + print("Related: %s" % response.reply.PopularSearchResult.RelatedSearches) + + for term in response.reply.PopularSearchResult.AlternativeSearches.split(';')[:3]: + api.execute('FindPopularItems', {'QueryKeywords': term, 'MaxEntries': 3}) + + print("Term: %s" % term) + + try: + for item in response.reply.ItemArray.Item: + print(item.Title) + except AttributeError: + pass + + dump(api) + + print("\n") + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def categoryInfo(opts): + + try: + api = Shopping(debug=opts.debug, appid=opts.appid, config_file=opts.yaml, + warnings=True) + + response = api.execute('GetCategoryInfo', {"CategoryID": 3410}) + + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def with_affiliate_info(opts): + try: + api = Shopping(debug=opts.debug, appid=opts.appid, + config_file=opts.yaml, warnings=True, + trackingid=1234, trackingpartnercode=9) + + mySearch = { + "MaxKeywords": 10, + "QueryKeywords": 'shirt', + } + + response = api.execute('FindPopularSearches', mySearch) + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def using_attributes(opts): + + try: + api = Shopping(debug=opts.debug, appid=opts.appid, + config_file=opts.yaml, warnings=True) + + response = api.execute('FindProducts', { + "ProductID": {'@attrs': {'type': 'ISBN'}, + '#text': '0596154488'}}) + + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +if __name__ == "__main__": + (opts, args) = init_options() + run(opts) + popularSearches(opts) + categoryInfo(opts) + with_affiliate_info(opts) + using_attributes(opts) diff --git a/samples/t_http.py b/samples/t_http.py new file mode 100644 index 0000000..698299d --- /dev/null +++ b/samples/t_http.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys + +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.http import Connection as HTTP +from ebaysdk.exception import ConnectionError + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + + (opts, args) = parser.parse_args() + return opts, args + +def run(opts): + + try: + api = HTTP(debug=opts.debug, method='GET') + + api.execute('http://feeds.wired.com/wired/index') + dump(api) + + except ConnectionError as e: + print(e) + +if __name__ == "__main__": + print("HTTP samples for SDK version %s" % ebaysdk.get_version()) + (opts, args) = init_options() + run(opts) diff --git a/samples/trading.py b/samples/trading.py new file mode 100644 index 0000000..1ca7b81 --- /dev/null +++ b/samples/trading.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import os +import sys +import datetime +from optparse import OptionParser + +sys.path.insert(0, '%s/../' % os.path.dirname(__file__)) + +from common import dump + +import ebaysdk +from ebaysdk.utils import getNodeText +from ebaysdk.exception import ConnectionError +from ebaysdk.trading import Connection as Trading + + +def init_options(): + usage = "usage: %prog [options]" + parser = OptionParser(usage=usage) + + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enabled debugging [default: %default]") + parser.add_option("-y", "--yaml", + dest="yaml", default='ebay.yaml', + help="Specifies the name of the YAML defaults file. [default: %default]") + parser.add_option("-a", "--appid", + dest="appid", default=None, + help="Specifies the eBay application id to use.") + parser.add_option("-p", "--devid", + dest="devid", default=None, + help="Specifies the eBay developer id to use.") + parser.add_option("-c", "--certid", + dest="certid", default=None, + help="Specifies the eBay cert id to use.") + + (opts, args) = parser.parse_args() + return opts, args + +def run(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid) + + api.execute('GetCharities', {'CharityID': 3897}) + dump(api) + print(api.response.reply.Charity.Name) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def feedback(opts): + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=False) + + api.execute('GetFeedback', {'UserID': 'tim0th3us'}) + dump(api) + + if int(api.response.reply.FeedbackScore) > 50: + print("Doing good!") + else: + print("Sell more, buy more..") + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def getTokenStatus(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=False) + + api.execute('GetTokenStatus') + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def verifyAddItem(opts): + """http://www.utilities-online.info/xmltojson/#.UXli2it4avc + """ + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=False) + + myitem = { + "Item": { + "Title": "Harry Potter and the Philosopher's Stone", + "Description": "This is the first book in the Harry Potter series. In excellent condition!", + "PrimaryCategory": {"CategoryID": "377"}, + "StartPrice": "1.0", + "CategoryMappingAllowed": "true", + "Country": "US", + "ConditionID": "3000", + "Currency": "USD", + "DispatchTimeMax": "3", + "ListingDuration": "Days_7", + "ListingType": "Chinese", + "PaymentMethods": "PayPal", + "PayPalEmailAddress": "tkeefdddder@gmail.com", + "PictureDetails": {"PictureURL": "http://i1.sandbox.ebayimg.com/03/i/00/30/07/20_1.JPG?set_id=8800005007"}, + "PostalCode": "95125", + "Quantity": "1", + "ReturnPolicy": { + "ReturnsAcceptedOption": "ReturnsAccepted", + "RefundOption": "MoneyBack", + "ReturnsWithinOption": "Days_30", + "Description": "If you are not satisfied, return the book for refund.", + "ShippingCostPaidByOption": "Buyer" + }, + "ShippingDetails": { + "ShippingType": "Flat", + "ShippingServiceOptions": { + "ShippingServicePriority": "1", + "ShippingService": "USPSMedia", + "ShippingServiceCost": "2.50" + } + }, + "Site": "US" + } + } + + api.execute('VerifyAddItem', myitem) + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + + +def verifyAddItemErrorCodes(opts): + """http://www.utilities-online.info/xmltojson/#.UXli2it4avc + """ + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=False) + + myitem = { + "Item": { + "Title": "Harry Potter and the Philosopher's Stone", + "Description": "This is the first book in the Harry Potter series. In excellent condition!", + "PrimaryCategory": {"CategoryID": "377aaaaaa"}, + "StartPrice": "1.0", + "CategoryMappingAllowed": "true", + "Country": "US", + "ConditionID": "3000", + "Currency": "USD", + "DispatchTimeMax": "3", + "ListingDuration": "Days_7", + "ListingType": "Chinese", + "PaymentMethods": "PayPal", + "PayPalEmailAddress": "tkeefdddder@gmail.com", + "PictureDetails": {"PictureURL": "http://i1.sandbox.ebayimg.com/03/i/00/30/07/20_1.JPG?set_id=8800005007"}, + "PostalCode": "95125", + "Quantity": "1", + "ReturnPolicy": { + "ReturnsAcceptedOption": "ReturnsAccepted", + "RefundOption": "MoneyBack", + "ReturnsWithinOption": "Days_30", + "Description": "If you are not satisfied, return the book for refund.", + "ShippingCostPaidByOption": "Buyer" + }, + "ShippingDetails": { + "ShippingType": "Flat", + "ShippingServiceOptions": { + "ShippingServicePriority": "1", + "ShippingService": "USPSMedia", + "ShippingServiceCost": "2.50" + } + }, + "Site": "US" + } + } + + api.execute('VerifyAddItem', myitem) + + except ConnectionError as e: + # traverse the DOM to look for error codes + for node in api.response.dom().findall('ErrorCode'): + print("error code: %s" % node.text) + + # check for invalid data - error code 37 + if 37 in api.response_codes(): + print("Invalid data in request") + + print(e) + print(e.response.dict()) + +def uploadPicture(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=True) + + pictureData = { + "WarningLevel": "High", + "ExternalPictureURL": "http://developer.ebay.com/DevZone/XML/docs/images/hp_book_image.jpg", + "PictureName": "WorldLeaders" + } + + api.execute('UploadSiteHostedPictures', pictureData) + dump(api) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def memberMessages(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=True) + + now = datetime.datetime.now() + + memberData = { + "WarningLevel": "High", + "MailMessageType": "All", + # "MessageStatus": "Unanswered", + "StartCreationTime": now - datetime.timedelta(days=60), + "EndCreationTime": now, + "Pagination": { + "EntriesPerPage": "5", + "PageNumber": "1" + } + } + + api.execute('GetMemberMessages', memberData) + + dump(api) + + if api.response.reply.has_key('MemberMessage'): + messages = api.response.reply.MemberMessage.MemberMessageExchange + + if type(messages) != list: + messages = [ messages ] + + for m in messages: + print("%s: %s" % (m.CreationDate, m.Question.Subject[:50])) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def getUser(opts): + try: + + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=True, timeout=20, siteid=101) + + api.execute('GetUser', {'UserID': 'biddergoat'}) + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def getOrders(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=True, timeout=20) + + api.execute('GetOrders', {'NumberOfDays': 30}) + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +def categories(opts): + + try: + api = Trading(debug=opts.debug, config_file=opts.yaml, appid=opts.appid, + certid=opts.certid, devid=opts.devid, warnings=True, timeout=20, siteid=101) + + callData = { + 'DetailLevel': 'ReturnAll', + 'CategorySiteID': 101, + 'LevelLimit': 4, + } + + api.execute('GetCategories', callData) + dump(api, full=False) + + except ConnectionError as e: + print(e) + print(e.response.dict()) + +''' +api = trading(domain='api.sandbox.ebay.com') +api.execute('GetCategories', { + 'DetailLevel': 'ReturnAll', + 'CategorySiteID': 101, + 'LevelLimit': 4, +}) +''' + +if __name__ == "__main__": + (opts, args) = init_options() + + print("Trading API Samples for version %s" % ebaysdk.get_version()) + + run(opts) + feedback(opts) + verifyAddItem(opts) + getTokenStatus(opts) + verifyAddItemErrorCodes(opts) + uploadPicture(opts) + memberMessages(opts) + categories(opts) + getUser(opts) + getOrders(opts) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..35f0091 --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +#from distutils.core import setup + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, find_packages +import re +import os + +PKG = 'ebaysdk' + +# Get the version +VERSIONFILE = os.path.join(PKG, "__init__.py") +version = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + open(VERSIONFILE, "rt").read(), re.M).group(1) + + +long_desc = """This SDK is a programatic inteface into the eBay +APIs. It simplifies development and cuts development time by standerizing +calls, response processing, error handling, debugging across the Finding, +Shopping, Merchandising, & Trading APIs. """ + +setup( + name=PKG, + version=version, + description="eBay SDK for Python", + author="Tim Keefer", + author_email="tkeefer@gmail.com", + url="https://github.com/timotheus/ebaysdk-python", + license="COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0", + packages=find_packages(), + provides=[PKG], + install_requires=['PyYaml', 'lxml', 'requests'], + test_suite='tests', + long_description=long_desc, + classifiers=[ + 'Topic :: Internet :: WWW/HTTP', + 'Intended Audience :: Developers', + ] +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1fffd6d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +''' +© 2012-2013 eBay Software Foundation +Authored by: Tim Keefer +Licensed under CDDL 1.0 +''' + +import sys +import unittest +import doctest +import ebaysdk.utils +import ebaysdk.config +import ebaysdk.response +import ebaysdk.connection +import ebaysdk.http +import ebaysdk.shopping +import ebaysdk.trading +import ebaysdk.merchandising +import ebaysdk.soa.finditem +import ebaysdk.finding + +# does not pass with python3.3 +try: + import ebaysdk.parallel +except ImportError: + pass + +def getTestSuite(): + suite = unittest.TestSuite() + + suite.addTest(doctest.DocTestSuite(ebaysdk.utils)) + suite.addTest(doctest.DocTestSuite(ebaysdk.config)) + suite.addTest(doctest.DocTestSuite(ebaysdk.response)) + suite.addTest(doctest.DocTestSuite(ebaysdk.connection)) + suite.addTest(doctest.DocTestSuite(ebaysdk.http)) + suite.addTest(doctest.DocTestSuite(ebaysdk.shopping)) + suite.addTest(doctest.DocTestSuite(ebaysdk.trading)) + suite.addTest(doctest.DocTestSuite(ebaysdk.merchandising)) + suite.addTest(doctest.DocTestSuite(ebaysdk.finding)) + + if not sys.version_info[0] >= 3 \ + and sys.modules.has_key('grequests') is True: + + suite.addTest(doctest.DocTestSuite(ebaysdk.parallel)) + + + # inside only + #suite.addTest(doctest.DocTestSuite(ebaysdk.soa.finditem)) + + return suite + +runner = unittest.TextTestRunner() +runner.run(getTestSuite()) +