├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── amazonmws ├── __init__.py ├── feeds.py ├── mws.py ├── orders.py ├── products.py ├── reports.py ├── sellers.py └── util.py ├── dev └── twisted.py ├── git-commit.sh ├── github-upload.sh └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled python files. 2 | *~ 3 | *.pyc 4 | *.egg-info 5 | build 6 | dist 7 | MANIFEST -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | global-exclude *~ *.pyc .goutputstream-* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-amazon-mws 2 | 3 | A powerful, easy to use Amazon MWS (Marketplace Web Services) API for Python 2 and Python3. 4 | 5 | ## Documentation 6 | 7 | ### Table of Contents 8 | * [Introduction](#introduction) 9 | * [Common Arguments](#common-arguments) 10 | * [marketplaces](#marketplaces) 11 | * [from_date](#from_date) 12 | * [to_date](#to_date) 13 | * [next_token](#next_token) 14 | * [debug](#debug) 15 | * [Feeds](#feeds) 16 | * [Common Feed Arguments](#common-feed-arguments) 17 | * [submissions](#submissions) 18 | * [feed_types](#feed_types) 19 | * [statuses](#statuses) 20 | * [Feed Calls](#feed-calls) 21 | * [cancel_submissions](#cancel_submissions) 22 | * [count_submissions](#count_submissions) 23 | * [get_report](#get_report) 24 | * [list_submissions](#list_submissions) 25 | * [list_submissions_next](#list_submissions_next) 26 | * [submit_feed](#submit_feed) 27 | * [Fulfillment (not yet implemented)](#fulfillment) 28 | * [Inbound](#inbound) 29 | * [CreateInboundShipmentPlan](#createinboundshipmentplan) 30 | * [CreateInboundShipment](#createinboundshipment) 31 | * [UpdateInboundShipment](#updateinboundshipment) 32 | * [ListInboundShipments](#listinboundshipments) 33 | * [ListInboundShipmentsByNextToken](#listinboundshipmentsbynexttoken) 34 | * [ListInboundShipmentItems](#listinboundshipmentitems) 35 | * [ListInboundShipmentItemsByNextToken](#listinboundshipmentitemsbynexttoken) 36 | * [GetServiceStatus](#getservicestatus) 37 | * [Inventory](#inventory) 38 | * [ListInventorySupply](#listinventorysupply) 39 | * [ListInventorySupplyByNextToken](#listinventorysupplybynexttoken) 40 | * [GetServiceStatus](#getservicestatus-1) 41 | * [Outbound](#outbound) 42 | * [GetFulfillmentPreview](#getfulfillmentpreview) 43 | * [CreateFulfillmentOrder](#createfulfillmentorder) 44 | * [GetFulfillmentOrder](#getfulfillmentorder) 45 | * [ListAllFulfillmentOrders](#listallfulfillmentorders) 46 | * [ListAllFulfillmentOrdersByNextToken](#listallfulfillmentordersbynexttoken) 47 | * [GetPackageTrackingDetails](#getpackagetrackingdetails) 48 | * [CancelFulfillmentOrder](#cancelfulfillmentorder) 49 | * [GetServiceStatus](#getservicestatus-2) 50 | * [Orders](#orders) 51 | * [Order Calls](#order-calls) 52 | * [list_orders](#list_orders) 53 | * [list_orders_next](#list_orders_next) 54 | * [send_request](#send_request) 55 | * [Products](#products) 56 | * [Common Product Arguments](#common-product-arguments) 57 | * [marketplace_id](#marketplace_id) 58 | * [id_type](#id_type) 59 | * [id_list](#id_list) 60 | * [verbose](#verbose) 61 | * [condition](#condition) 62 | * [Product Calls](#product-calls) 63 | * [get_categories](#get_categories) 64 | * [get_competitive_pricing](#get_competitive_pricing) 65 | * [get_lowest_listings](#get_lowest_listings) 66 | * [get_my_price](#get_my_price) 67 | * [get_products](#get_products) 68 | * [list_matching](#list_matching) 69 | * [Recommendations (not yet implemented)](#recommendations) 70 | * [GetLastUpdatedTimeForRecommendations](#getlastupdatedtimeforrecommendations) 71 | * [ListRecommendations / ListRecommendationsByNextToken](#listrecommendations--listrecommendationsbynexttoken) 72 | * [Reports](#reports) 73 | * [Common Report Arguments](#common-report-arguments) 74 | * [requests](#requests) 75 | * [report_types](#report_types) 76 | * [statuses](#statuses-1) 77 | * [acknowledged](#acknowledged) 78 | * [max_count](#max_count) 79 | * [Report Calls](#report-calls) 80 | * [cancel_report_requests](#cancel_report_requests) 81 | * [get_report](#get_report-1) 82 | * [get_report_count](#get_report_count) 83 | * [get_report_list](#get_report_list) 84 | * [get_report_list_next](#get_report_list_next) 85 | * [get_report_request_count](#get_report_request_count) 86 | * [get_report_request_list](#get_report_request_list) 87 | * [get_report_request_list_next](#get_report_request_list_next) 88 | * [get_report_schedule_count (not yet implemented)](#get_report_schedule_count) 89 | * [get_report_schedule_list (not yet implemented)](#get_report_schedule_list) 90 | * [get_report_schedule_list_next (not yet implemented)](#get_report_schedule_list_next) 91 | * [manage_report_schedule (not yet implemented)](#manage_report_schedule) 92 | * [request_report](#request_report) 93 | * [update_report_acknowledgements](#update_report_acknowledgements) 94 | * [Sellers](#sellers) 95 | * [Seller Calls](#seller-calls) 96 | * [get_status](#get_status) 97 | * [list_marketplaces](#list_marketplaces) 98 | * [list_marketplaces_next](#list_marketplaces_next) 99 | 100 | ### Introduction 101 | > 102 | > TODO: Add introduction. 103 | > 104 | 105 | ### Common Arguments 106 | > 107 | > #### marketplaces 108 | > 109 | >> TODO: Add documentation. 110 | > 111 | > #### from_date 112 | > 113 | >> TODO: Add documentation. 114 | > 115 | > #### to_date 116 | > 117 | >> TODO: Add documentation. 118 | > 119 | > #### next_token 120 | > 121 | >> TODO: Add documentation. 122 | > 123 | > #### debug 124 | > 125 | >> TODO: Add documentation. 126 | 127 | ### Feeds 128 | > 129 | > #### Common Feed Arguments 130 | > 131 | >> 132 | >> ##### submissions 133 | >> 134 | >>> TODO: Add documentation. 135 | >> 136 | >> ##### feed_types 137 | >> 138 | >>> TODO: Add documentation. 139 | >> 140 | >> ##### statuses 141 | >> 142 | >>> TODO: Add documentation. 143 | > 144 | > #### Feed Calls 145 | > 146 | >> 147 | >> ##### cancel_submissions 148 | >> 149 | >>> 150 | >>> **Arguments** 151 | >>> * submissions (Feeds: Common Arguments) 152 | >>> * feed_types (Feeds: Common Arguments) 153 | >>> * from_date (Common Arguments) 154 | >>> * to_date (Common Arguments) 155 | >>> * all_submissions (Specific Argument) 156 | >>> * debug (Common Arguments) 157 | >>> 158 | >> 159 | >> ##### count_submissions 160 | >> 161 | >>> 162 | >>> **Arguments** 163 | >>> * feed_types (Feeds: Common Arguments) 164 | >>> * statuses (Feeds: Common Arguments) 165 | >>> * from_date (Common Arguments) 166 | >>> * to_date (Common Arguments) 167 | >>> * debug (Common Arguments) 168 | >>> 169 | >> 170 | >> ##### get_report 171 | >> 172 | >>> 173 | >>> **Arguments** 174 | >>> * submission_id (Specific Argument) 175 | >>> * debug (Common Arguments) 176 | >>> 177 | >> 178 | >> ##### list_submissions 179 | >> 180 | >>> 181 | >>> **Arguments** 182 | >>> * submissions (Feeds: Common Arguments) 183 | >>> * count (Specific Argument) 184 | >>> * feed_types (Feeds: Common Arguments) 185 | >>> * statuses (Feeds: Common Arguments) 186 | >>> * from_date (Common Arguments) 187 | >>> * to_date (Common Arguments) 188 | >>> * debug (Common Arguments) 189 | >>> 190 | >> 191 | >> ##### list_submissions_next 192 | >> 193 | >>> 194 | >>> **Arguments** 195 | >>> * next_token (Common Arguments) 196 | >>> * debug (Common Arguments) 197 | >>> 198 | >> 199 | >> ##### submit_feed 200 | >> 201 | >>> 202 | >>> **Arguments** 203 | >>> * feed_type (Specific Argument) 204 | >>> * data (Specific Argument) 205 | >>> * content_type (Specific Argument) 206 | >>> * marketplaces (Common Arguments) 207 | >>> * debug (Common Arguments) 208 | >>> 209 | 210 | ### Fulfillment 211 | > 212 | > Fulfillment has not yet been implemented into this API. 213 | > 214 | > ### Inbound 215 | > 216 | > Inbound has not yet been implemented into this API. 217 | > 218 | > Inbound Calls: 219 | >> 220 | >> #### CreateInboundShipmentPlan 221 | >> 222 | >> #### CreateInboundShipment 223 | >> 224 | >> #### UpdateInboundShipment 225 | >> 226 | >> #### ListInboundShipments 227 | >> 228 | >> #### ListInboundShipmentsByNextToken 229 | >> 230 | >> #### ListInboundShipmentItems 231 | >> 232 | >> #### ListInboundShipmentItemsByNextToken 233 | >> 234 | >> #### GetServiceStatus 235 | >> 236 | > 237 | > ### Inventory 238 | > 239 | > Inventory has not yet been implemented into this API. 240 | > 241 | > Inventory Calls: 242 | >> #### ListInventorySupply 243 | >> #### ListInventorySupplyByNextToken 244 | >> #### GetServiceStatus 245 | > 246 | > ### Outbound 247 | > 248 | > Outbound has not yet been implemented into this API. 249 | > 250 | > Outbound Calls: 251 | >> 252 | >> #### GetFulfillmentPreview 253 | >> 254 | >> #### CreateFulfillmentOrder 255 | >> 256 | >> #### GetFulfillmentOrder 257 | >> 258 | >> #### ListAllFulfillmentOrders 259 | >> 260 | >> #### ListAllFulfillmentOrdersByNextToken 261 | >> 262 | >> #### GetPackageTrackingDetails 263 | >> 264 | >> #### CancelFulfillmentOrder 265 | >> 266 | >> #### GetServiceStatus 267 | >> 268 | 269 | ### Orders 270 | > #### Order Calls 271 | > 272 | >> 273 | >> ##### list_orders 274 | >> 275 | >>> 276 | >>> **Arguments** 277 | >>> * created_after (Specific Argument) 278 | >>> * updated_after (Specific Argument) 279 | >>> * order_statuses (Specific Argument) 280 | >>> * marketplaces (Common Arguments) 281 | >>> 282 | >> 283 | >> ##### list_orders_next 284 | >> 285 | >>> 286 | >>> **Arguments** 287 | >>> * next_token (Common Arguments) 288 | >>> 289 | >> 290 | >> ##### send_request 291 | >> 292 | >>> 293 | >>> **Arguments** 294 | >>> * action (Specific Argument) 295 | >>> * args_dict (Specific Argument) 296 | >>> 297 | 298 | ### Products 299 | > 300 | > #### Common Product Arguments 301 | > 302 | >> 303 | >> ##### marketplace_id 304 | >> 305 | >>> TODO: Add documentation. 306 | >> 307 | >> ##### id_type 308 | >> 309 | >>> TODO: Add documentation. 310 | >> 311 | >> ##### id_list 312 | >> 313 | >>> TODO: Add documentation. 314 | >> 315 | >> ##### verbose 316 | >> 317 | >>> TODO: Add documentation. 318 | >> 319 | >> ##### condition 320 | >> 321 | >>> TODO: Add documentation. 322 | > 323 | > #### Product Calls 324 | > 325 | >> 326 | >> ##### get_categories 327 | >> 328 | >>> 329 | >>> **Arguments** 330 | >>> * marketplace_id (Products: Common Arguments) 331 | >>> * id_type (Products: Common Arguments) 332 | >>> * id_list (Products: Common Arguments) 333 | >>> * verbose (Products: Common Arguments) 334 | >>> 335 | >> 336 | >> ##### get_competitive_pricing 337 | >> 338 | >>> 339 | >>> **Arguments** 340 | >>> * marketplace_id (Products: Common Arguments) 341 | >>> * id_type (Products: Common Arguments) 342 | >>> * id_list (Products: Common Arguments) 343 | >>> * verbose (Products: Common Arguments) 344 | >>> 345 | >> 346 | >> ##### get_lowest_listings 347 | >> 348 | >>> 349 | >>> **Arguments** 350 | >>> * marketplace_id (Products: Common Arguments) 351 | >>> * id_type (Products: Common Arguments) 352 | >>> * id_list (Products: Common Arguments) 353 | >>> * condition (Products: Common Arguments) 354 | >>> * exclude_me (Specific Argument) 355 | >>> * verbose (Products: Common Arguments) 356 | >>> 357 | >> 358 | >> ##### get_my_price 359 | >> 360 | >>> 361 | >>> **Arguments** 362 | >>> * marketplace_id (Products: Common Arguments) 363 | >>> * id_type (Products: Common Arguments) 364 | >>> * id_list (Products: Common Arguments) 365 | >>> * condition (Products: Common Arguments) 366 | >>> * verbose (Products: Common Arguments) 367 | >>> 368 | >> 369 | >> ##### get_products 370 | >> 371 | >>> 372 | >>> **Arguments** 373 | >>> * marketplace_id (Products: Common Arguments) 374 | >>> * id_type (Products: Common Arguments) 375 | >>> * id_list (Products: Common Arguments) 376 | >>> * verbose (Products: Common Arguments) 377 | >>> 378 | >> 379 | >> ##### list_matching 380 | >> 381 | >>> 382 | >>> **Arguments** 383 | >>> * marketplace_id (Products: Common Arguments) 384 | >>> * query (Specific Argument) 385 | >>> * context (Specific Argument) 386 | >>> * verbose (Products: Common Arguments) 387 | >>> 388 | 389 | ### Recommendations 390 | > 391 | > Recommendations has not yet been implemented into this API. 392 | > 393 | > Recommendations Calls: 394 | >> 395 | >> #### GetLastUpdatedTimeForRecommendations 396 | >> 397 | >> #### ListRecommendations 398 | >> 399 | >> #### ListRecommendationsByNextToken 400 | >> 401 | 402 | ### Reports 403 | 404 | > 405 | > #### Common Report Arguments 406 | > 407 | >> 408 | >> ##### requests 409 | >> 410 | >>> TODO: Add documentation. 411 | >> 412 | >> ##### report_types 413 | >> 414 | >>> TODO: Add documentation. 415 | >> 416 | >> ##### statuses 417 | >> 418 | >>> TODO: Add documentation. 419 | >> 420 | >> ##### acknowledged 421 | >> 422 | >>> TODO: Add documentation. 423 | >> 424 | >> ##### max_count 425 | >> 426 | >>> TODO: Add documentation. 427 | > 428 | > #### Report Calls 429 | > 430 | >> 431 | >> ##### cancel_report_requests 432 | >> 433 | >>> 434 | >>> **Arguments** 435 | >>> * requests (Reports: Common Arguments) 436 | >>> * report_types (Reports: Common Arguments) 437 | >>> * statuses (Reports: Common Arguments) 438 | >>> * from_date (Common Arguments) 439 | >>> * to_date (Common Arguments) 440 | >>> * marketplaces (Common Arguments) 441 | >>> * debug (Common Arguments) 442 | >>> 443 | >> 444 | >> ##### get_report 445 | >> 446 | >>> 447 | >>> **Arguments** 448 | >>> * report_id (Specific Argument) 449 | >>> * marketplaces (Common Arguments) 450 | >>> * debug (Common Arguments) 451 | >>> 452 | >> 453 | >> ##### get_report_count 454 | >> 455 | >>> 456 | >>> **Arguments** 457 | >>> * report_types (Reports: Common Arguments) 458 | >>> * acknowledged (Reports: Common Arguments) 459 | >>> * from_date (Common Arguments) 460 | >>> * to_date (Common Arguments) 461 | >>> * marketplaces (Common Arguments) 462 | >>> * debug (Common Arguments) 463 | >>> 464 | >> 465 | >> ##### get_report_list 466 | >> 467 | >>> 468 | >>> **Arguments** 469 | >>> * requests (Reports: Common Arguments) 470 | >>> * max_count (Reports: Common Arguments) 471 | >>> * report_types (Reports: Common Arguments) 472 | >>> * acknowledged (Reports: Common Arguments) 473 | >>> * from_date (Common Arguments) 474 | >>> * to_date (Common Arguments) 475 | >>> * marketplaces (Common Arguments) 476 | >>> * debug (Common Arguments) 477 | >>> 478 | >> 479 | >> ##### get_report_list_next 480 | >> 481 | >>> 482 | >>> **Arguments** 483 | >>> * next_token (Common Arguments) 484 | >>> * debug (Common Arguments) 485 | >>> 486 | >> 487 | >> ##### get_report_request_count 488 | >> 489 | >>> 490 | >>> **Arguments** 491 | >>> * report_types (Reports: Common Arguments) 492 | >>> * statuses (Reports: Common Arguments) 493 | >>> * from_date (Common Arguments) 494 | >>> * to_date (Common Arguments) 495 | >>> * marketplaces (Common Arguments) 496 | >>> * debug (Common Arguments) 497 | >>> 498 | >> 499 | >> ##### get_report_request_list 500 | >> 501 | >>> 502 | >>> **Arguments** 503 | >>> * requests (Reports: Common Arguments) 504 | >>> * max_count (Reports: Common Arguments) 505 | >>> * report_types (Reports: Common Arguments) 506 | >>> * statuses (Reports: Common Arguments) 507 | >>> * from_date (Common Arguments) 508 | >>> * to_date (Common Arguments) 509 | >>> * marketplaces (Common Arguments) 510 | >>> * debug (Common Arguments) 511 | >>> 512 | >> 513 | >> ##### get_report_request_list_next 514 | >> 515 | >>> 516 | >>> **Arguments** 517 | >>> * next_token (Common Arguments) 518 | >>> * debug (Common Arguments) 519 | >>> 520 | >> 521 | >> ##### get_report_schedule_count 522 | >> 523 | >>> Not yet implemented within this API. 524 | >> 525 | >> ##### get_report_schedule_list 526 | >> 527 | >>> Not yet implemented within this API. 528 | >> 529 | >> ##### get_report_schedule_list_next 530 | >> 531 | >>> Not yet implemented within this API. 532 | >> 533 | >> ##### manage_report_schedule 534 | >> 535 | >>> Not yet implemented within this API. 536 | >> 537 | >> ##### request_report 538 | >> 539 | >>> 540 | >>> **Arguments** 541 | >>> * report_type (Specific Argument) 542 | >>> * start_date (Specific Argument) 543 | >>> * end_date (Specific Argument) 544 | >>> * show_sales_channel (Specific Argument) 545 | >>> * marketplaces (Common Arguments) 546 | >>> * debug (Common Arguments) 547 | >>> 548 | >> 549 | >> ##### update_report_acknowledgements 550 | >> 551 | >>> 552 | >>> **Arguments** 553 | >>> * reports (Specific Argument) 554 | >>> * marketplaces (Common Arguments) 555 | >>> * debug (Common Arguments) 556 | >>> 557 | 558 | ### Sellers 559 | > #### Seller Calls 560 | > 561 | >> 562 | >> ##### get_status 563 | >> 564 | >>> 565 | >>> **Arguments** 566 | >>> * debug (Common Arguments) 567 | >>> 568 | >> 569 | >> ##### list_marketplaces 570 | >> 571 | >>> 572 | >>> **Arguments** 573 | >>> * debug (Common Arguments) 574 | >>> 575 | >> 576 | >> ##### list_marketplaces_next 577 | >> 578 | >>> 579 | >>> **Arguments** 580 | >>> * next_token (Common Arguments) 581 | >>> * debug (Common Arguments) 582 | >>> 583 | -------------------------------------------------------------------------------- /amazonmws/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | *amazonmws* is an implementation of the Amazon Marketplace Web Service (MWS) 4 | API. 5 | """ 6 | 7 | __authors__ = ["Caleb P. Burns", "Wes Hansen"] 8 | __copyright__ = u"Copyright © 2012-2021 Caleb P. Burns, Wes Hansen" 9 | __created__ = "2012-11-30" 10 | __credits__ = ["Joshua D. Burns"] 11 | __license__ = "MPL 2.0" 12 | __modified__ = "2021-05-30" 13 | __project__ = "amazonmws" 14 | __status__ = "Production" 15 | __version__ = "1.0.3" 16 | -------------------------------------------------------------------------------- /amazonmws/feeds.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This package provides an implementation of the Amazon MWS Feeds API. 4 | Subpackages are available that provide classes for creating several 5 | feeds. 6 | """ 7 | 8 | __author__ = "Caleb P. Burns" 9 | __created__ = "2012-11-26" 10 | __modified__ = "2016-04-05" 11 | __modified_by___ = "Joshua D. Burns" 12 | 13 | import six # Python2/Python3 compatibility library. 14 | import datetime 15 | import amazonmws.mws 16 | from amazonmws.util import datetime_to_iso8601, encode_string, is_sequence, marketplace_args 17 | 18 | #: Feed types. 19 | FEED_TYPES = { 20 | # XML Feeds 21 | 'offer': '_POST_OFFER_ONLY_DATA_', # Offer 22 | 'order_acknowledgement': '_POST_ORDER_ACKNOWLEDGEMENT_DATA_', # Order 23 | 'order_cancellation': '_POST_FULFILLMENT_ORDER_CANCELLATION_REQUEST_DATA_', # Order 24 | 'order_fulfillment': '_POST_ORDER_FULFILLMENT_DATA_', # Order 25 | 'product_data': '_POST_PRODUCT_DATA_', # Product 26 | 'product_image': '_POST_PRODUCT_IMAGE_DATA_', # Product 27 | 'product_inventory': '_POST_INVENTORY_AVAILABILITY_DATA_', # Product 28 | 'product_item': '_POST_ITEM_DATA_', # Product 29 | 'product_override': '_POST_PRODUCT_OVERRIDES_DATA_', # Product 30 | 'product_pricing': '_POST_PRODUCT_PRICING_DATA_', # Product 31 | 'product_relationship': '_POST_PRODUCT_RELATIONSHIP_DATA_', # Product 32 | 'shipping_override': '_POST_SHIPPING_OVERRIDE_DATA_', # Shipping 33 | 'webstore_item': '_POST_WEBSTORE_ITEM_DATA_', # Webstore 34 | # Flat-File Feeds 35 | 'flat_book': '_POST_FLAT_FILE_BOOKLOADER_DATA_', # Book 36 | 'flat_book_uiee': '_POST_UIEE_BOOKLOADER_DATA_', # Book: Universal Information Exchange Environment 37 | 'flat_product_converge': '_POST_FLAT_FILE_CONVERGENCE_LISTINGS_DATA_', # Product: Merging 38 | 'flat_product_data': '_POST_FLAT_FILE_LISTINGS_DATA_', # Product 39 | 'flat_product_inventory': '_POST_FLAT_FILE_INVLOADER_DATA_', # Product 40 | 'flat_product_price_inv': '_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_', # Product 41 | } 42 | 43 | #: Feed Methods. Maps FEED_TYPES to data-types 44 | FEED_METHODS = { 45 | 'offer': 'xml', 46 | 'order_acknowledgement': 'xml', 47 | 'order_cancellation': 'xml', 48 | 'order_fulfillment': 'xml', 49 | 'product_data': 'xml', 50 | 'product_image': 'xml', 51 | 'product_inventory': 'xml', 52 | 'product_item': 'xml', 53 | 'product_override': 'xml', 54 | 'product_pricing': 'xml', 55 | 'product_relationship': 'xml', 56 | 'shipping_override': 'xml', 57 | 'webstore_item': 'xml', 58 | 'flat_book': 'flat-file', 59 | 'flat_book_uiee': 'flat-file', 60 | 'flat_product_converge': 'flat-file', 61 | 'flat_product_data': 'flat-file', 62 | 'flat_product_inventory': 'flat-file', 63 | 'flat_product_price_inv': 'flat-file', 64 | } 65 | 66 | #: Content types. Key is ENDPOINT aliased name. Maps FEED_TYPE, FEED_METHOD and ENTPOINT to content-type. 67 | CONTENT_TYPES = { 68 | 'ca': { 69 | 'xml': 'text/xml', 70 | 'flat-file': 'text/tab-separated-values; charset=iso-8859-1', 71 | }, 72 | 'cn': { 73 | 'xml': 'text/xml', 74 | 'flat-file': 'text/tab-separated-values; charset=UTF-8', 75 | # TODO: How should we account for two separate encodings belonging to a single ENDPOINT? 76 | 'flat-file-alt': 'text/tab-separated-values; charset=UTF-16', 77 | }, 78 | 'eu': { 79 | 'xml': 'text/xml', 80 | 'flat-file': 'text/tab-separated-values; charset=iso-8859-1', 81 | }, 82 | 'in': { 83 | 'xml': 'text/xml', 84 | 'flat-file': 'text/tab-separated-values; charset=iso-8859-1', # Guess, need to verify. 85 | }, 86 | 'jp': { 87 | 'xml': 'text/xml', 88 | 'flat-file': 'text/tab-separated-values; charset=Shift_JIS', 89 | }, 90 | 'us': { 91 | 'xml': 'text/xml', 92 | 'flat-file': 'text/tab-separated-values; charset=iso-8859-1', 93 | }, 94 | } 95 | 96 | #: Processing statuses.. 97 | PROCESSING_STATUSES = { 98 | 'cancelled': '_CANCELLED_', 99 | 'done': '_DONE_', 100 | 'in_progress': '_IN_PROGRESS_', 101 | 'submitted': '_SUBMITTED_' 102 | } 103 | 104 | 105 | class Feeds(amazonmws.mws.MWS): 106 | """ 107 | The ``MWSFeeds`` class is used to send requests to the Amazon MWS 108 | Feeds API. The primary purpose of this class is to submit feeds to 109 | MWS and get the results back. 110 | """ 111 | 112 | client_api_version = __modified__ 113 | """ 114 | *client_api_version* (``str``) is the version of this client API. 115 | """ 116 | 117 | feeds_api_version = '2009-01-01' 118 | """ 119 | *feeds_api_version* (``str``) is the version of the MWS Feeds API 120 | implemented. 121 | """ 122 | 123 | def new_args(self): 124 | """ 125 | Returns a new ``dict`` of default arguments. 126 | """ 127 | args = { 128 | 'AWSAccessKeyId': self.access_key, 129 | 'Merchant': self.merchant_id, 130 | 'Timestamp': datetime_to_iso8601(datetime.datetime.utcnow()), 131 | 'Version': self.feeds_api_version 132 | } 133 | return args 134 | 135 | def CancelFeedSubmissions(self, submissions=None, feed_types=None, from_date=None, to_date=None, all_submissions=None, debug=None): 136 | """ 137 | Requests all Feed Submissions that match the specified criteria to 138 | be cancelled. 139 | 140 | .. NOTE:: Only feeds that have been submitted but have not yet been 141 | processed can be cancelled (i.e., where status is "_SUBMITTED_"). 142 | 143 | To delete specific Feed Submissions, set *submissions*: 144 | 145 | *submissions* (**sequence**) contains the ID (``str``) of each Feed 146 | Submission to cancel. 147 | 148 | To delete Feed Submissions based upon a query, use any of the 149 | following: *feed_types*, *from_date*, *to_date*. 150 | 151 | *feed_types* (**sequence**) contains each Feed Type (``str``) to 152 | filter the list of submissions to cancel. This can contain any of 153 | the keys and values from ``FEED_TYPES``. Default is ``None`` for all 154 | Feed Types. 155 | 156 | *from_date* (``datetime.datetime`` or ``float``) is the earliest 157 | date to filter the list of submissions to cancel. Default is 158 | ``None`` for 180 days ago. 159 | 160 | *to_date* (``datetime.datetime`` or ``float``) is the latest date to 161 | filter the list of submissions to cancel. Default is ``None`` for 162 | now. 163 | 164 | To delete all Feed Submissions, set *all_submissions* to ``True``: 165 | 166 | *all_submissions* (``bool``) indicates whether all Feed Submissions 167 | should be deleted (``True``), or not (``False``). This is used to 168 | prevent accidental deletion of all Feed Submissions. 169 | 170 | Returns the response XML (``str``). 171 | """ 172 | cancel_list = 1 if submissions is not None else 0 173 | cancel_query = 1 if (feed_types is not None or from_date is not None or to_date is not None) else 0 174 | cancel_all = 1 if all_submissions else 0 175 | cancel_sum = cancel_list + cancel_query + cancel_all 176 | if cancel_sum != 1: 177 | if cancel_sum: 178 | msg = [] 179 | if cancel_list: 180 | msg.append("submissions:{subs!r}") 181 | if cancel_query: 182 | msg.append("a query (feed_types:{types!r}, from_date:{from_date!r}, and to_date:{to_date!r})") 183 | if cancel_all: 184 | msg.append("all_submissions:{all_subs!r}") 185 | msg = "Only one of {} can be set.".format(", or ".join(msg)) 186 | else: 187 | msg = "Either submissions:{subs!r}, or a query (feed_types:{types!r}, from_date:{from_date!r}, and to_date:{to_date!r}), or all_submissions:{all_subs!r} must be set." 188 | 189 | raise ValueError(msg.format(subs=submissions, types=feed_types, to_date=to_date, from_date=from_date, all_subs=all_submissions)) 190 | 191 | # Build args. 192 | args = self.new_args() 193 | args['Action'] = 'CancelFeedSubmissions' 194 | 195 | if submissions is not None: 196 | args.update(submission_args(submissions, name='submissions')) 197 | 198 | if feed_types is not None: 199 | args.update(feed_type_args(feed_types, name='feed_types')) 200 | 201 | if from_date is not None: 202 | args['SubmittedFromDate'] = datetime_to_iso8601(from_date, name='from_date') 203 | 204 | if to_date is not None: 205 | args['SubmittedToDate'] = datetime_to_iso8601(to_date, name='to_date') 206 | 207 | # Send request. 208 | return self.send_request(args, debug=debug) 209 | 210 | def GetFeedSubmissionCount(self, feed_types=None, statuses=None, from_date=None, to_date=None, debug=None): 211 | """ 212 | Requests a count of all Feed Submissions that match the specified 213 | criteria. 214 | 215 | *feed_types* (**sequence**) contains each Feed Type (``str``) to 216 | filter the list of submissions to count. This can contain any keys 217 | or values from ``FEED_TYPES``. Default is ``None`` for all Feed 218 | Types. 219 | 220 | *statuses* (**sequence**) contains each Processing Status 221 | (``str``) to filter the list of submissions to count. This can 222 | contain keys or values from ``PROCESSING_STATUSES``. Default is 223 | 224 | *from_date* (``datetime.datetime`` or ``float``) is the earliest 225 | date to filter the list of submissions to count. Default is 226 | ``None`` for 90 days ago. 227 | 228 | *to_date* (``datetime.datetime`` or ``float``) is the latest date to 229 | filter the list of submissions to cancel. count is ``None`` for 230 | now. 231 | 232 | Returns the response XML (``str``). 233 | """ 234 | # Build args. 235 | args = self.new_args() 236 | args['Action'] = 'GetFeedSubmissionCount' 237 | 238 | if feed_types is not None: 239 | args.update(feed_type_args(feed_types, name='feed_types')) 240 | 241 | if statuses is not None: 242 | args.update(status_args(statuses, name='statuses')) 243 | 244 | if from_date is not None: 245 | args['SubmittedFromDate'] = datetime_to_iso8601(from_date, name='from_date') 246 | 247 | if to_date is not None: 248 | args['SubmittedToDate'] = datetime_to_iso8601(to_date, name='to_date') 249 | 250 | # Send request. 251 | return self.send_request(args, debug=debug) 252 | 253 | def GetFeedSubmissionResult(self, submission_id, debug=None): 254 | """ 255 | Requests the Feed Processing Report. 256 | 257 | *submission_id* (``str``) is the ID of the Feed Submission. 258 | 259 | Returns the response XML (``str``). 260 | """ 261 | if not isinstance(submission_id, six.string_types): 262 | raise TypeError("submission_id:{!r} is not a string.".format(submission_id)) 263 | elif not submission_id: 264 | raise ValueError("submission_id:{!r} cannot be empty.".format(submission_id)) 265 | #submission_id = submission_id.encode('ASCII') # TODO: Why are we encoding this? Causing Python 3 issues. 266 | 267 | # Buils args. 268 | args = self.new_args() 269 | args['Action'] = 'GetFeedSubmissionResult' 270 | args['FeedSubmissionId'] = submission_id 271 | 272 | # Send request. 273 | return self.send_request(args, debug=debug) 274 | 275 | def GetFeedSubmissionList(self, submissions=None, count=None, feed_types=None, statuses=None, from_date=None, to_date=None, debug=None): 276 | """ 277 | Requests for the list of Feed Submissions that match the specified 278 | criteria. 279 | 280 | To list only specific Feed Submissions, set *submissions*. 281 | 282 | *submissions* (**sequence**) contains the ID (``str``) of each Feed 283 | Submission to list. 284 | 285 | To list Feed Submissions based upon a query, use the following: 286 | 287 | *count* (``int``) is the maximum number of Feed Submissions to 288 | list. This cannot exceed 100. Default is ``None`` for 10. 289 | 290 | *feed_types* (**sequence**) contains each Feed Type (``str``) to 291 | filter the list of submissions to list. This can contain any of the 292 | keys or values in ``FEED_TYPES``. Default is ``None`` for all Feed 293 | Types. 294 | 295 | *statuses* (**sequence**) contains each Processing Status 296 | (``str``) to filter the list of submissions to count. This can 297 | contain any of the keys or values from ``PROCESSING_STATUSES``. 298 | Default is ``None`` for all Processing Statuses. 299 | 300 | *from_date* (``datetime.datetime`` or ``float``) is the earliest 301 | date to filter the list of submissions to count. Default is 302 | ``None`` for 180 days ago. 303 | 304 | *to_date* (``datetime.datetime`` or ``float``) is the latest date to 305 | filter the list of submissions to cancel. count is ``None`` for 306 | now. 307 | 308 | Returns the response XML (``str``). 309 | """ 310 | if submissions is not None and (count is not None or feed_types is not None or statuses is not None or from_date is not None or to_date is not None): 311 | raise ValueError("Only submissions:{subs!r}, or a query (count:{count!r}, feed_types:{feed_types!r}, statuses:{statuses!r}, from_date:{from_date!r}, and to_date:{to_date!r}) can be set.".format( 312 | subs=submissions, 313 | count=count, 314 | feed_types=feed_types, 315 | statuses=statuses, 316 | from_date=from_date, 317 | to_date=to_date 318 | )) 319 | 320 | # Build args. 321 | args = self.new_args() 322 | args['Action'] = 'GetFeedSubmissionList' 323 | 324 | if submissions is not None: 325 | args.update(submission_args(submissions, name='submissions')) 326 | 327 | if count is not None: 328 | if not isinstance(count, six.integer_types): 329 | raise TypeError("count:{!r} is not an integer.".format(count)) 330 | elif count < 1 or 100 < count : 331 | raise ValueError("count:{!r} is not between 1 and 100 inclusive.".format(count)) 332 | args['MaxCount'] = str(count) 333 | 334 | if feed_types is not None: 335 | args.update(feed_type_args(feed_types, name='feed_types')) 336 | 337 | if statuses is not None: 338 | args.update(status_args(statuses, name='statuses')) 339 | 340 | if from_date is not None: 341 | args['SubmittedFromDate'] = datetime_to_iso8601(from_date, name='from_date') 342 | 343 | if to_date is not None: 344 | args['SubmittedToDate'] = datetime_to_iso8601(to_date, name='to_date') 345 | 346 | # Send request. 347 | return self.send_request(args, debug=debug) 348 | 349 | def GetFeedSubmissionListByNextToken(self, next_token, debug=None): 350 | """ 351 | Requests the next batch of Feed Submissions being listed. 352 | 353 | *next_token* (``str``) is the token used to fetch the next batch of 354 | results. 355 | 356 | Returns the response XML (``str``). 357 | """ 358 | if not isinstance(next_token, six.string_types): 359 | raise TypeError("next_token:{!r} is not a string.".format(next_token)) 360 | elif not next_token: 361 | raise ValueError("next_token:{!r} cannot be empty.".format(next_token)) 362 | #next_token = next_token.encode('ASCII') 363 | 364 | # Build args. 365 | args = self.new_args() 366 | args['Action'] = 'GetFeedSubmissionListByNextToken' 367 | args['NextToken'] = next_token 368 | 369 | # Send request. 370 | return self.send_request(args, debug=debug) 371 | 372 | def SubmitFeed(self, feed_type, data, content_type, marketplaces=None, debug=None): 373 | """ 374 | Submits the specified feed. 375 | 376 | *feed_type* (``str``) is the type of feed being submitted. This can 377 | be any of the keys or values from ``FEED_TYPES``. 378 | 379 | *data* (``str`` or ``file``) is the feed data. This can be either 380 | the raw bytes (``str``) or a ``file`` object supporting ``read()``. 381 | 382 | *content_type* (``str``) is the content type of *data*. 383 | 384 | *marketplaces* (**sequence**) optionally contains the ID (``str``) 385 | of each Amazon Marketplace to apply the feed to. Default is ``None`` 386 | for all Amazon Marketplaces. 387 | 388 | Returns the response XML (``str``). 389 | """ 390 | if not isinstance(feed_type, six.string_types): 391 | raise TypeError("feed_type:{!r} is not a str.".format(feed_type)) 392 | if data is None: 393 | raise TypeError("data:{!r} is not a str or file.".format(data)) 394 | 395 | args = self.new_args() 396 | args['Action'] = 'SubmitFeed' 397 | args['FeedType'] = FEED_TYPES.get(feed_type, feed_type) 398 | 399 | if marketplaces is not None: 400 | args.update(marketplace_args(marketplaces, name='marketplaces')) 401 | 402 | return self.send_request(args, body=data, content_type=content_type, debug=debug) 403 | 404 | 405 | def feed_type_args(feed_types, name=None): 406 | """ 407 | Converts the specified Feed Types into their resepctive URL query 408 | arguments. 409 | 410 | *feed_types* (**sequence**) contains each Feed Type (``str``). This 411 | can contain any of the keys or values from ``FEED_TYPES``. 412 | 413 | *name* (``str``) is the name to use when an error occurs. 414 | 415 | Returns a ``list`` containing each *key*-*feed_type* ``tuple``. 416 | 417 | - *key* (``str``) is the query argument key for *feed_type*. 418 | 419 | - *feed_type* (``str``) is the Feed Type ID. 420 | """ 421 | if not name: 422 | name = 'feed_types' 423 | 424 | if not is_sequence(feed_types): 425 | raise TypeError("{}:{!r} is not a sequence.".format(name, feed_types)) 426 | 427 | args = [] 428 | for i, feed_type in enumerate(feed_types): 429 | feed_type = FEED_TYPES.get(feed_type, feed_type) 430 | if not isinstance(feed_type, six.string_types): 431 | raise TypeError("{}[{}]:{!r} is not a str.".format(name, i, feed_type)) 432 | elif not feed_type: 433 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, feed_type)) 434 | feed_type = encode_string(feed_type, 'ASCII', name="{}[{}]".format(name, i)) 435 | 436 | args.append(('FeedTypeList.Type.{}'.format(i + 1), feed_type)) 437 | 438 | return args 439 | 440 | def status_args(statuses, name=None): 441 | """ 442 | Converts the specified Feed Processing Statuses into their respective 443 | URL query arguments. 444 | 445 | *statuses* (**sequence**) contains each Feed Processing Status 446 | (``str``). This can contain any of the keys or value from ``PROCESSING_STATUSES``. 447 | 448 | *name* (``str``) is the name to use when an error occurs. 449 | 450 | Returns a ``list`` containing each *key*-*status* ``tuple``. 451 | 452 | - *key* (``str``) is the query argument key for *status*. 453 | 454 | - *status* (``str``) is the Feed Processing Status. 455 | """ 456 | if not name: 457 | name = 'statuses' 458 | 459 | if not is_sequence(statuses): 460 | raise TypeError("{}:{!r} is not a sequence.".format(name, statuses)) 461 | 462 | args = [] 463 | for i, status in enumerate(statuses): 464 | status = PROCESSING_STATUSES.get(status, status) 465 | if not isinstance(status, six.string_types): 466 | raise TypeError("{}[{}]:{!r} is not a str.".format(name, i, status)) 467 | elif not status: 468 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, status)) 469 | status = encode_string(status, 'ASCII', name="{}[{}]".format(name, i)) 470 | 471 | args.append(('FeedProcessingStatusList.Status.{}'.format(i + 1), status)) 472 | 473 | return args 474 | 475 | def submission_args(submissions, name=None): 476 | """ 477 | Converts the specified Feed Submission IDs into their resepctive URL 478 | query arguments. 479 | 480 | *submissions* (**sequence**) contains each Feed Submission ID 481 | (``str``). 482 | 483 | *name* (``str``) is the name to use when an error occurs. 484 | 485 | Returns a ``list`` containing each *key*-*submission_id* ``tuple``. 486 | 487 | - *key* (``str``) is the query argument key for *submission_id*. 488 | 489 | - *submission_id* (``str``) is the Feed Submission ID. 490 | """ 491 | if not name: 492 | name = 'submissions' 493 | 494 | if not is_sequence(submissions): 495 | raise TypeError("{}:{!r} is not a sequence.".format(name, submissions)) 496 | 497 | args = [] 498 | for i, sub_id in enumerate(submissions): 499 | if not isinstance(sub_id, six.string_types): 500 | raise TypeError("{}[{}]:{!r} is not a string.".format(name, i, sub_id)) 501 | elif not sub_id: 502 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, sub_id)) 503 | sub_id = encode_string(sub_id, 'ASCII', name="{}[{}]".format(name, i)) 504 | 505 | args.append(('FeedSubmissionIdList.Id.{}'.format(i + 1), sub_id)) 506 | 507 | return args 508 | -------------------------------------------------------------------------------- /amazonmws/mws.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This module provides the base implementation of the Amazon MWS API for 4 | authenticating, sending and receiving requests. 5 | """ 6 | 7 | __author__ = "Caleb P. Burns" 8 | __created__ = "2012-11-20" 9 | __modified__ = "2016-04-05" 10 | __modified_by___ = "Joshua D. Burns" 11 | 12 | import six # Python2/Python3 compatibility library. 13 | import base64 14 | import hashlib 15 | import hmac 16 | import os.path 17 | import platform 18 | import pprint 19 | import re 20 | import sys 21 | import urllib 22 | 23 | from amazonmws import __version__ 24 | from amazonmws.util import is_sequence 25 | 26 | #: MWS API Endpoints 27 | ENDPOINTS = { 28 | 'ca': 'https://mws.amazonservices.ca', # Canada 29 | 'cn': 'https://mws.amazonservices.com.cn', # China 30 | 'eu': 'https://mws-eu.amazonservices.com', # Europe (Germany, Spain, France, Italy, United Kingdom) 31 | 'in': 'https://mws.amazonservices.in', # India 32 | 'jp': 'https://mws.amazonservices.jp', # Japan 33 | 'us': 'https://mws.amazonservices.com', # United States 34 | } 35 | 36 | #: Marketplace IDs 37 | MARKETPLACE_IDS = { 38 | 'ca': 'A2EUQ1WTGCTBG2', # Canada 39 | 'cn': 'AAHKV2X7AFYLW', # China 40 | 'in': 'A21TJRUUN4KGV', # India 41 | 'jp': 'A1VC38T7YXB528', # Japan 42 | 'us': 'ATVPDKIKX0DER', # United States 43 | # Europe 44 | 'de': 'A1PA6795UKMFR9', # Germany 45 | 'es': 'A1RKKUPIHCS9HS', # Spain 46 | 'fr': 'A13V1IB3VIYZZH', # France 47 | 'it': 'APJ6JRA9NG5V4', # Italy 48 | 'uk': 'A1F83G8C2ARO7P', # United Kingdom 49 | } 50 | 51 | #: Envelope message types. 52 | MESSAGE_TYPES = { 53 | 'cat_pil': 'CatPIL', 54 | 'character_data': 'CharacterData', 55 | 'customer': 'Customer', 56 | 'fulfillment_center': 'FulfillmentCenter', 57 | 'fulfillment_order_cancellation_request': 'FulfillmentOrderCancellationRequest', 58 | 'fulfillment_order_request': 'FulfillmentOrderRequest', 59 | 'image': 'Image', 60 | 'inventory': 'Inventory', 61 | 'invoice_confirmation': 'InvoiceConfirmation', 62 | 'item': 'Item', 63 | 'listings': 'Listings', 64 | 'loyalty': 'Loyalty', 65 | 'multi_channel_order_report': 'MultiChannelOrderReport', 66 | 'navigation_report': 'NavigationReport', 67 | 'offer': 'Offer', 68 | 'order_acknowledgement': 'OrderAchnowledgement', 69 | 'order_adjustment': 'OrderAdjustment', 70 | 'order_fulfillment': 'OrderFulfillment', 71 | 'order_notification_report': 'OrderNotificationReport', 72 | 'order_report': 'OrderReport', 73 | 'override': 'Override', 74 | 'price': 'Price', 75 | 'processing_report': 'ProcessingReport', 76 | 'product': 'Product', 77 | 'product_image': 'ProductImage', 78 | 'relationship': 'Relationship', 79 | 'reverse_item': 'ReverseItem', 80 | 'store': 'Store', 81 | 'webstore_item': 'WebstoreItem', 82 | 'pending_order_report': 'PendingOrderReport' 83 | } 84 | 85 | #: Operation types. 86 | OPERATION_TYPES = { 87 | 'delete': 'Delete', 88 | 'update': 'Update', 89 | 'partial_update': 'PartialUpdate' 90 | } 91 | 92 | #: Carrier codes. 93 | CARRIER_CODES = { 94 | 'blue_package': 'Blue Package', 95 | 'canada_post': 'Canada Post', 96 | 'city_link': 'City Link', 97 | 'dhl': 'DHL', 98 | 'dhl_global': 'DHL Global Mail', 99 | 'fastway': 'Fastway', 100 | 'fedex': 'FedEx', 101 | 'fedex_smart_post': 'FedEx SmartPost', 102 | 'gls': 'GLS', 103 | 'go': 'GO!', 104 | 'hermes_logistik_gruppe': 'Hermes Logistik Gruppe', 105 | 'newgistics': 'Newgistics', 106 | 'nippon_express': 'NipponExpress', 107 | 'on_trac': 'OnTrac', 108 | 'osm': 'OSM', 109 | 'parcelforce': 'Parcelforce', 110 | 'royal_mail': 'Royal Mail', 111 | 'sagawa_express': 'SagawaExpress', 112 | 'streamlite': 'Streamlite', 113 | 'target': 'Target', 114 | 'tnt': 'TNT', 115 | 'usps': 'USPS', 116 | 'ups': 'UPS', 117 | 'ups_mail_innovations': 'UPS Mail Innovations', 118 | 'yamato_transport': 'YamatoTransport' 119 | } 120 | 121 | #: Currency codes. 122 | CURRENCY_CODES = { 123 | 'ca': 'CAD', # Canadian dollar 124 | 'eu': 'EUR', # Euro 125 | 'jp': 'JPY', # Japanese yen 126 | 'gb': 'GBP', # British pound sterling 127 | 'us': 'USD' # Unites states dollar 128 | } 129 | 130 | #: Language types. 131 | LANGUAGE_TYPES = { 132 | ('en', 'ca'): 'en_CA', 133 | ('en', 'gb'): 'en_GB', 134 | ('en', 'us'): 'en_US', 135 | ('fr', 'ca'): 'fr_CA', 136 | ('fr', 'fr'): 'fr_FR', 137 | ('de', 'de'): 'de_DE', 138 | ('ja', 'jp'): 'ja_JP' 139 | } 140 | 141 | #: Signature methods. 142 | SIGNATURE_METHODS = { 143 | 'hmac_sha1': 'HmacSHA1', 144 | 'hmac_sha256': 'HmacSHA256' 145 | } 146 | 147 | 148 | class MWS(object): 149 | """ 150 | The ``MWS`` class is the base class used to interact with the Amazon 151 | MWS API. This class implements authentication, sending and receiving 152 | requests. 153 | """ 154 | 155 | app_name = "PythonAmazonMWS" 156 | """ 157 | *app_name* (``str``) is the name of this API. 158 | """ 159 | 160 | app_version = __version__ 161 | """ 162 | *app_version* (``str``) is the version of this API. 163 | """ 164 | 165 | client_api_version = None 166 | """ 167 | *client_api_version* (``str``) is the version of the client API. This 168 | should be set by subclasses to indicate the version of their 169 | respective API. 170 | """ 171 | 172 | ua_enc = 'UTF-8' 173 | """ 174 | *ua_enc* (``str``) is the encoding the user agent string will be 175 | encoded as. 176 | """ 177 | 178 | ua_escape_re = re.compile(r"([\/(=);])") 179 | """ 180 | *ua_escape_re* (``re.RegexObject``) is used by *ua_escape()* to match 181 | characters which need to be escaped. 182 | """ 183 | 184 | ua_escape_repl = r"\\\1" 185 | """ 186 | *ua_escape_repl* (``str``) is used by *ua_escape()* to escape matched 187 | characters. 188 | """ 189 | 190 | max_size = 2**31 - 1 191 | """ 192 | *max_size* (``int``) is the maximum size a request body can be. 193 | """ 194 | 195 | def __init__(self, access_key, secret_key, merchant_id, endpoint, agent=None, user_agent=None): 196 | """ 197 | Initializes an ``MWS`` instance. 198 | 199 | *access_key* (``str``) is your Amazon supplied AWS Access Key ID. 200 | 201 | *secret_key* (``str``) is your Amazon supplied AWS Secret Access Key 202 | used to sign requests. 203 | 204 | *merchant_id* (``str``) is your Amazon supplied Merchant ID. 205 | 206 | *endpoint* (``str``) is the Amazon Endpoint Server. This can be 207 | either the actual endpoint URL or a key from ``ENDPOINTS``. 208 | 209 | *agent* (``MWSAgent``) optionally is the agent to use which actually 210 | sends the requests. Default is ``None`` to use a new ``MWSAgent`` 211 | instance. 212 | 213 | *user_agent* (``str``) optionally is the user agent string to use. 214 | Default is ``None`` to use the one generated by *self.ua_new()*. 215 | """ 216 | 217 | self.access_key = None 218 | """ 219 | *access_key* (``str``) is your Amazon supplised Access Key. 220 | """ 221 | 222 | self.agent = None 223 | """ 224 | *agent* (``MWSAgent``) is the agent used to actually send the 225 | requests. 226 | """ 227 | 228 | self.endpoint = None 229 | """ 230 | *endpoint* (``str``) is the Amazon MWS Endpoint Server. 231 | """ 232 | 233 | self.merchant_id = None 234 | """ 235 | *merchant_id* (``str``) is your Amazon supplised Merchant ID. 236 | """ 237 | 238 | self.user_agent = None 239 | """ 240 | *user_agent* (``str``) is the user agent string to use for requests. 241 | """ 242 | 243 | self.secret_key = None 244 | """ 245 | *secret_key* (``str``) is used the secret key used sign requests. 246 | """ 247 | 248 | if not isinstance(access_key, six.string_types): 249 | raise TypeError("access_key:{!r} must be a string.".format(access_key)) 250 | elif not access_key: 251 | raise ValueError("access_key:{!r} cannot be empty.".format(access_key)) 252 | access_key = str(access_key) 253 | 254 | if not isinstance(secret_key, six.string_types): 255 | raise TypeError("secret_key:{!r} must be a string.".format(secret_key)) 256 | elif not secret_key: 257 | raise ValueError("secret_key:{!r} cannot be empty.".format(secret_key)) 258 | secret_key = str(secret_key) 259 | 260 | endpoint = ENDPOINTS.get(endpoint, endpoint) 261 | if not isinstance(endpoint, six.string_types): 262 | raise TypeError("endpoint:{!r} must be a string.".format(endpoint)) 263 | elif not endpoint: 264 | raise ValueError("endpoint:{!r} cannot be empty.".format(endpoint)) 265 | endpoint = str(endpoint) 266 | 267 | if not isinstance(merchant_id, six.string_types): 268 | raise TypeError("merchant_id:{!r} must be a string.".format(merchant_id)) 269 | elif not merchant_id: 270 | raise ValueError("merchant_id:{!r} cannot be empty.".format(merchant_id)) 271 | merchant_id = str(merchant_id) 272 | 273 | if agent is not None and not isinstance(agent, IMWSAgent): 274 | raise TypeError("agent:{!r} is not an IMWSAgent.".format(agent)) 275 | 276 | if user_agent is not None: 277 | if not isinstance(user_agent, six.string_types): 278 | raise TypeError("user_agent:{!r} must be a string or None.".format(user_agent)) 279 | elif not user_agent: 280 | raise ValueError("user_agent:{!r} cannot be empty.".format(user_agent)) 281 | 282 | self.access_key = access_key 283 | self.secret_key = secret_key 284 | self.merchant_id = merchant_id 285 | self.endpoint = endpoint 286 | 287 | self.agent = agent or MWSAgent() 288 | 289 | #self.user_agent = six.u(user_agent or self.ua_new(self.client_api_version, self.app_name, self.app_version)).encode(self.ua_enc) 290 | self.user_agent = user_agent or self.ua_new(self.client_api_version, self.app_name, self.app_version) 291 | 292 | def send_request(self, args, body=None, content_type=None, path=None, debug=None): 293 | """ 294 | Sends the request to MWS. 295 | 296 | *args* are query parameters to send as part of the request. This can 297 | be either a ``dict`` mapping *key* to *value* or a **sequence** of 298 | *key*-*value* 2-``tuple`` pairs. The "Action" key must be set, and 299 | the "Signature", "SignatureMethod" and "SignatureVersion" keys must 300 | not be set. 301 | 302 | - *key* (``str``) is the argument key. 303 | 304 | - *value* is the argument value which can be either a single value 305 | (``str``) or a **sequence** containing each value (``str``). If a 306 | **sequence**, *key* will be repeated for each value. 307 | 308 | *body* (``str`` or ``file``) optionally is the request body to send. 309 | This can be either the body bytes (``str``) or a ``file`` supporting 310 | ``read()`` (``seek()`` and ``tell()`` are optional). Default is 311 | ``None`` for no body. 312 | 313 | .. NOTE:: If a ``file``, it will not be closed (i.e., you are still 314 | responsible for calling ``close()`` on it). 315 | 316 | *content_type* (``str``) is the content type of *body*. This must be 317 | set if *body* is set. If passing a Feed Type of XML, you will most 318 | likely want to set this to "text/XML". If feeding a Flat File, 319 | you'll need to specify one of the following, based on marketplace: 320 | - North America and Europe (US, France, Germany, Italy, Spain, UK): 321 | "text/tab-separated-values; charset=iso-8859-1" 322 | - Japan: "text/tab-separated-values; charset=Shift_JIS" 323 | - China: "text/tab-separated-values;charset=UTF-8" 324 | OR ... 325 | "text/tab-separated-values;charset=UTF-16" 326 | Default is ``None`` because *body* is ``None``. 327 | 328 | *path* (``str``) is the URL path to request. Default is ``None`` for 329 | "/". This is "/" for most of the Amazon MWS API. 330 | 331 | *debug* (``dict``) is whether debugging information should be 332 | printed. Default is ``None`` for no debugging. 333 | 334 | Returns the response which is dependent upon *self.agent*. The 335 | default *agent* returns the response body (``str``). 336 | """ 337 | return self.agent.request(self, path, args, body, content_type, debug=debug) 338 | 339 | def ua_escape(self, value): 340 | """ 341 | Escapes a user agent value. 342 | 343 | *value* (**string**) is the value to escape. 344 | 345 | Returns the escaped value (**string**). 346 | """ 347 | return self.ua_escape_re.sub(self.ua_escape_repl, value) 348 | 349 | def ua_new(self, client, app, version): 350 | """ 351 | Generates a new user agent string. 352 | 353 | *client* (**string**) is the MWS client version. 354 | 355 | *app* (**string**) is the name of the application. 356 | 357 | *version* (``str``) is the version of the application. 358 | 359 | Returns the user agent string (**string**). 360 | """ 361 | attrs = [] 362 | 363 | lang = "Language={python}/{version}".format( 364 | python=self.ua_escape(platform.python_implementation()), 365 | version=self.ua_escape(platform.python_version()) 366 | ) 367 | attrs.append(lang) 368 | 369 | plat = "Platform={system}/{machine}/{release}".format( 370 | system=self.ua_escape(platform.system()), 371 | machine=self.ua_escape(platform.machine()), 372 | release=self.ua_escape(platform.release()) 373 | ) 374 | attrs.append(plat) 375 | 376 | if client: 377 | client = "MWSClientVersion={version}".format( 378 | version=self.ua_escape(client) 379 | ) 380 | attrs.append(client) 381 | 382 | user_agent = u"{app}/{version}".format( 383 | app=self.ua_escape(app), 384 | version=self.ua_escape(version) 385 | ) 386 | if attrs: 387 | user_agent += " ({attrs})".format(attrs="; ".join(attrs)) 388 | 389 | return user_agent 390 | 391 | 392 | class IMWSAgent(object): 393 | """ 394 | The ``IMWSAgent`` class is the interface that all MWS Agent classes 395 | must implement. The Agent is what actually sends requests to Amazon. 396 | """ 397 | 398 | def request(self, mws, path, args, body, content_type, debug=None): 399 | """ 400 | Perform the request. 401 | 402 | *mws* (``MWS``) is the MWS instance. 403 | 404 | *path* (``str``) is the request path. 405 | 406 | *args* contains the query parameters. 407 | 408 | *body* (``str`` or ``file``) contains the body of the request. 409 | 410 | *content_type* (``str``) is the content type of *body*. 411 | 412 | *debug* (``dict``) optionally is whether debugging information 413 | should be printed. Default is ``None`` for no debugging. 414 | 415 | Returns the response body (``str``). 416 | 417 | .. NOTE:: Subclasses must override this. 418 | """ 419 | raise NotImplementedError("Subclasses must implement request() without calling IMWSAgent.request().") 420 | 421 | 422 | class MWSAgent(IMWSAgent): 423 | """ 424 | The ``MWSAgent`` class is the default implementation of the 425 | ``IMWSAgent`` class used by the ``MWS`` class. This implementation 426 | does everything blocking, inline and synchronously. 427 | """ 428 | 429 | req_args_required = {'Action'} 430 | """ 431 | *req_args_required* (``set``) contains the required request arguments. 432 | """ 433 | 434 | req_args_safe_chars = "~" 435 | """ 436 | *req_args_safe_chars* (``str``) contains additional charaters that do 437 | not need to be escaped in arguments. This is passed to ``urllib.quote_plus()``. 438 | """ 439 | 440 | req_args_sig = {'Signature', 'SignatureMethod', 'SignatureVersion'} 441 | """ 442 | *req_args_sig* (``set``) contains the request arguments used by the 443 | signature. 444 | """ 445 | 446 | sig_version = 2 447 | """ 448 | *sig_version* (``int``) is the signature version used by *self.sign_request()*. 449 | """ 450 | 451 | sig_method = 'hmac_sha256' 452 | """ 453 | *sig_method* (``str``) is the HMAC hash algorithm used to calculate 454 | the signature by *self.sign_request()*. 455 | """ 456 | 457 | sort_args_re = re.compile(r"(\d+|\D+)") 458 | """ 459 | *sort_args_re* (``re.RegexObject``) is used by *self.sort_args_key()* 460 | to natural sort args. 461 | """ 462 | 463 | def build_request(self, mws, path, args, body, content_type, debug=None): 464 | """ 465 | Builds the request. 466 | 467 | .. NOTE:: This should not be called directly. Use *self.request()*. 468 | 469 | *mws* (``MWS``) is the MWS instance. 470 | 471 | *path* (``str``) is the request path. 472 | 473 | *args* contains the query parameters. 474 | 475 | *body* (``str`` or ``file``) contains the body of the request. This 476 | can be ``None``. 477 | 478 | *content_type* (``str``) is the content type of *body*. 479 | 480 | *debug* (``dict``) is whether debugging information should be 481 | printed. Default is ``None`` for no debugging. 482 | 483 | - *body* (``bool``) is whether the request body should be printed 484 | (``True``), or not (``False``). Default is ``None`` for ``False``. 485 | 486 | - *info* (``bool``) is whether the request args and headers should 487 | be printed (``True``), or not (``False``). 488 | 489 | - *url* (``bool``) is whether the generated URL should be printed 490 | (``True``), or not (``False``). Default is ``None`` for ``False``. 491 | 492 | Returns a ``tuple`` containing: *method*, the request URL (``str``), 493 | the request headers (``dict`` or ``None``), and the request body 494 | (``str``, ``file`` or ``None``). 495 | """ 496 | 497 | # Before anything else, ensure body is proper data type (if string) 498 | if body is not None: 499 | if isinstance(body, six.string_types): 500 | # Ensure string types are byte-arrays. 501 | body = six.b(body) 502 | 503 | if debug is None: 504 | debug = {} 505 | 506 | if not isinstance(mws, MWS): 507 | raise TypeError("mws:{!r} is not an MWS.".format(mws)) 508 | 509 | if isinstance(args, dict): 510 | args = list(six.iteritems(args)) 511 | elif is_sequence(args): 512 | args = args[:] 513 | else: 514 | raise TypeError("args:{!r} must be a dict or sequence.".format(args)) 515 | 516 | # Check for missing and reserved args. 517 | arg_keys = set([k for k, _v in args]) 518 | missing = self.req_args_required - arg_keys 519 | if len(missing) > 1: 520 | raise KeyError("args:{!r} is missing keys: {}.".format(args, ", ".join(map(repr, missing)))) 521 | elif missing: 522 | raise KeyError("args:{!r} is missing key: {!r}.".format(args, missing.pop())) 523 | reserved = self.req_args_sig & arg_keys 524 | if len(reserved) > 1: 525 | raise KeyError("args:{!r} cannot have keys: {}.".format(args, ", ".join(map(repr, reserved)))) 526 | elif reserved: 527 | raise KeyError("args:{!r} cannot have key: {!r}.".format(args, reserved.pop())) 528 | 529 | if body is not None: 530 | body_is_str = isinstance(body, six.binary_type) 531 | body_is_file = callable(getattr(body, 'read', None)) 532 | if not body_is_str and not body_is_file: 533 | raise TypeError("body:{!r} is not a str or file.".format(body)) 534 | 535 | if not isinstance(content_type, six.string_types): 536 | raise TypeError("content_type:{!r} is not a str.".format(content_type)) 537 | elif not content_type: 538 | raise ValueError("content_type:{!r} cannot be empty.".format(content_type)) 539 | 540 | if path is not None and not isinstance(path, six.string_types): 541 | raise TypeError("path:{!r} is not a str.".format(path)) 542 | 543 | # Query. 544 | args += [ 545 | ('SignatureMethod', SIGNATURE_METHODS[self.sig_method]), 546 | ('SignatureVersion', self.sig_version) 547 | ] 548 | args = sorted(args, key=self.sort_args_key) 549 | query = "&".join(( 550 | "{}={}".format(six.moves.urllib.parse.quote(str(k), self.req_args_safe_chars), six.moves.urllib.parse.quote(str(v), self.req_args_safe_chars)) 551 | ) for k, vals in args for v in (vals if is_sequence(vals) else [vals])) 552 | 553 | # Signature 554 | method = "GET" if body is None else "POST" 555 | result = six.moves.urllib.parse.urlparse(mws.endpoint) 556 | domain = result.netloc or result.path 557 | path = six.moves.urllib.parse.quote(os.path.normpath('/' + path.lstrip('/'))) if path else "/" 558 | sig = self.sign_request(mws.secret_key, method, domain, path, query) 559 | 560 | # URL. 561 | url = "{host}{path}?{query}&Signature={sig}".format( 562 | host=mws.endpoint, 563 | path=path, 564 | query=query, 565 | sig=six.moves.urllib.parse.quote(sig, safe='/') 566 | ) 567 | 568 | # Headers. 569 | headers = { 570 | 'User-Agent': mws.user_agent 571 | } 572 | 573 | if body is not None: 574 | if body_is_str: 575 | body_len = len(body) 576 | body_md5 = base64.b64encode(hashlib.md5(body).digest()) 577 | elif body_is_file: 578 | if callable(getattr(body, 'seek', None)) and callable(getattr(body, 'tell', None)): 579 | # MD5 body and get length. 580 | pos = body.tell() 581 | md5 = hashlib.md5() 582 | while True: 583 | chunk = body.read(2**16) 584 | if not chunk: 585 | break 586 | md5.update(chunk) 587 | body_len = body.tell() - pos 588 | body_md5 = base64.b64encode(md5.digest()) 589 | body.seek(pos, os.SEEK_SET) 590 | 591 | else: 592 | body = body.read() 593 | body_len = len(body) 594 | body_md5 = base64.b64encode(hashlib.md5(body).digest()) 595 | body_is_file = False 596 | 597 | if body_len > mws.max_size: 598 | raise ValueError("body length:{!r} cannot be greater than {}.".format(body_len, mws.max_size)) 599 | 600 | headers['Content-Type'] = content_type 601 | headers['Content-Length'] = body_len 602 | headers['Content-MD5'] = body_md5 603 | else: 604 | body_len = None 605 | 606 | # Debug info. 607 | if debug: 608 | if debug.get('url', False): 609 | print("URL ({}:{})".format(url.__class__.__name__, len(url))) 610 | print("--------") 611 | print(url) 612 | print("--------") 613 | if debug.get('info', False): 614 | print("Args ({})".format(len(args))) 615 | print("---------") 616 | pprint.pprint(args) 617 | print("---------") 618 | print("Headers ({})".format(len(headers))) 619 | print("------------") 620 | pprint.pprint(headers) 621 | print("------------") 622 | if debug.get('body', False) or debug.get('info', False): 623 | print("Body ({}:{})".format(body.__class__.__name__, body_len)) 624 | if debug.get('body', False): 625 | print("-"*20) 626 | if body_is_file: 627 | pos = body.tell() 628 | print(body.read()) 629 | body.seek(pos, os.SEEK_SET) 630 | else: 631 | print(body) 632 | print("-"*20) 633 | 634 | return method, url, headers, body 635 | 636 | def request(self, mws, path, args, body, content_type, debug=None): 637 | """ 638 | Perform the request. 639 | 640 | *mws* (``MWS``) is the MWS instance. 641 | 642 | *path* (``str``) is the request path. 643 | 644 | *args* contains the query parameters. 645 | 646 | *body* (``str`` or ``file``) contains the body of the request. This 647 | can be ``None``. 648 | 649 | *content_type* (``str``) is the content type of *body*. 650 | 651 | *debug* (``dict``) is whether debugging information should be 652 | printed. Default is ``None`` for no debugging. 653 | 654 | Returns the response body (``str``). 655 | """ 656 | method, url, headers, body = self.build_request(mws, path, args, body, content_type, debug=debug) 657 | return self.send_request(method, url, headers, body, debug=debug) 658 | 659 | def send_request(self, method, url, headers, body, debug=None): 660 | """ 661 | Send the request. 662 | 663 | .. NOTE:: This should not be called directly. Use *self.request()*. 664 | 665 | *method* (``str``) is the HTTP request method. 666 | 667 | *url* (``str``) is the URL of the request. 668 | 669 | *headers* (``dict``) are any headers to send. This can be ``None``. 670 | 671 | *body* (``str`` or ``file``) is the body of the request. This can be 672 | ``None``. 673 | 674 | *debug* (``dict``) is whether debugging information should be 675 | printed. Default is ``None`` for no debugging. 676 | 677 | Returns the response body (``str``). 678 | """ 679 | if callable(getattr(body, 'read', None)): 680 | body = body.read() 681 | request = six.moves.urllib.request.Request(url, data=body, headers=headers) 682 | try: 683 | response = six.moves.urllib.request.urlopen(request, timeout=30) 684 | data = response.read() 685 | except six.moves.urllib.error.HTTPError as e: 686 | data = e.read() 687 | if not data: 688 | raise 689 | return data 690 | 691 | def sign_request(self, key, method, domain, path, query): 692 | """ 693 | Generates the request signature. 694 | 695 | *key* (``MWS``) is the secret key used to sign the request. 696 | 697 | *method* (``str``) is the HTTP method: "GET" or "POST". 698 | 699 | *domain* (``str``) is the request domain. 700 | 701 | *path* (``str``) is the request URL path. 702 | 703 | *query* (``str``) is the request URL query parameters. 704 | 705 | Returns the request signature base64 encoded (``str``). 706 | """ 707 | if self.sig_version != 2: 708 | raise SignatureError("Only signature version 2 is supported, not {!r}.".format(self.sig_version)) 709 | 710 | # Create data. 711 | data = "{method}\n{domain}\n{path}\n{query}".format( 712 | method=method.upper(), 713 | domain=domain.lower(), 714 | path=path, 715 | query=query 716 | ) 717 | 718 | # Hash data. 719 | hmac_func = getattr(self, 'sign_' + self.sig_method, None) 720 | if not callable(hmac_func): 721 | raise SignatureError("Signature method {!r} is not supported.".format(self.sig_method)) 722 | sig = hmac_func(key, data) 723 | 724 | # Returned encoded hash. 725 | return base64.b64encode(sig) 726 | 727 | def sign_hmac_sha1(self, key, data): 728 | """ 729 | Signs the request using HMAC SHA1. 730 | 731 | *key* (``str``) is the secret key used to sign *data*. 732 | 733 | *data* (``str``) is the request data to sign. 734 | 735 | Returns the data signature (``str``). 736 | """ 737 | return hmac.new(key, data, hashlib.sha1).digest() 738 | 739 | def sign_hmac_sha256(self, key, data): 740 | """ 741 | Signs the request using HMAC SHA256. 742 | 743 | *key* (``str``) is the secret key used to sign *data*. 744 | 745 | *data* (``str``) is the request data to sign. 746 | 747 | Returns the data signature (``str``). 748 | """ 749 | return hmac.new(six.b(key), six.b(data), hashlib.sha256).digest() 750 | 751 | def sort_args_key(self, key): 752 | """ 753 | 754 | NOTE: This method is deprecated. Amazon MWS expects sorting in 755 | natural BYTE ORDER, not natural-order. By sorting by 756 | natural order, when IDs over a value of 9 are encountered, 757 | MWS complains about a bad signature. 758 | 759 | This is used by *self.build_request()* to sort arguments. This 760 | implementation performs a natural sort so that when query arguments 761 | named in the style "{full_name}.{short_name}.{n}" are ordered 762 | properly for when the signature is genereted. 763 | 764 | *key* (``str``) is the key. 765 | 766 | Returns the key (``object``) to use to sort by. 767 | """ 768 | return key 769 | 770 | # If sending more than 10 Report IDs to 771 | # UpdateReportAcknowledgements, Amazon MWS gives an error stating 772 | # that the signature does not match. If sending 9, it works just 773 | # fine. We have deducted that Amazon is not performing a natural 774 | # sort on at *least* UpdateReportAcknowledgements Report IDs. 775 | # We need to test other methods which support more than 10 IDs being 776 | # passed to determine if this is a Call-specific limitation, or if 777 | # across the board all sorting should be done the default "python" 778 | # way. 779 | #return [int(s) if s.isdigit() else s for s in self.sort_args_re.findall(key[0])] # Natural sort 780 | 781 | 782 | class SignatureError(Exception): 783 | """ 784 | The `SignatureError` exception is raised when there is an error 785 | generating the signature for a request. 786 | """ 787 | -------------------------------------------------------------------------------- /amazonmws/orders.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | This is a python implementation of the Orders API for the Amazon Marketplace Web Services(MWS). 4 | This API inherits from MWS which supplies all the tools necessary to construct the proper 5 | URL and send a request to Amazon MWS server. 6 | This module consists of the necessary information to construct requests in the Orders API. 7 | Calls include: ListOrders, ListOrdersByNextToken, GetOrder, ListOrderItems, ListOrderItems, 8 | ListOrderItemsByNextToken, and GetServiceStatus. 9 | 10 | Sample xml for a 'throttled' request: 11 | HTTP ERROR 503 12 | 13 | 14 | 15 | 16 | Sender 17 | RequestThrottled 18 | Request is throttled 19 | 20 | da90a758-492a-48bf-aae2-0f08e127e34b 21 | 22 | ''' 23 | 24 | __author__ = "Caleb P. Burns" 25 | __created__ = "2012-05-03" 26 | __modified__ = "2016-03-29" 27 | __modified_by___ = "Joshua D. Burns" 28 | 29 | import six # Python2/Python3 compatibility library. 30 | from amazonmws.mws import MWS, MARKETPLACE_IDS # The MWS connection logic 31 | from amazonmws.util import datetime_to_iso8601, is_sequence 32 | import datetime 33 | import re 34 | 35 | #: Order statuses. DEPRECATED. Remove in version 2.0 36 | ORDER_STATUSES = { 37 | 'cancelled': 'Canceled', # Yes, it is "Canceled" here and not "Cancelled". 38 | 'invoice_unconfirmed': 'InvoiceUnconfirmed', 39 | 'partially_shipped': 'PartiallyShipped', 40 | 'pending': 'Pending', 41 | 'shipped': 'Shipped', 42 | 'unfulfillable': 'Unfulfillable', 43 | 'unshipped': 'Unshipped', 44 | } 45 | 46 | # DEPRECATED. Remove in version 2.0. 47 | class UnsupportedActionError(Exception): 48 | ''' 49 | This exception is raised when an action is called 50 | that the Orders API doesn't support 51 | ''' 52 | pass 53 | 54 | class Orders( MWS ): 55 | ''' 56 | The base class for all calls to the OrdersAPI 57 | This contains information pertaining to building a proper request query from 58 | the Orders API 59 | 60 | This class makes requests to the Amazon MWS Orders server by calling 'send_request' 61 | and passing it a supported `Action` with its given arguments. 62 | 63 | The arguments to pass with an action must follow these guidelines: 64 | - arguments are stored in a dict, where key is a parameter for the action 65 | specified by the Orders API docs: `https://images-na.ssl-images-amazon.com/images/G/02/mwsportal/doc/en_US/orders/MWSOrdersApiReference._V136999359_.pdf` and value is either a single string value 66 | for that parameter or a list of strings if that parameter can have multiple 67 | values. 68 | - Do not include the 'Action' argument in the dict--this api will insert that for you 69 | - If the parameter can accept multiple values, ALWAYS put the value in a dict, even 70 | if you're only passing a single value for that parameter. 71 | - It isn't necessary to supply args that is common to every Orders API call. 72 | To tell what these args are, call Orders.new_args() 73 | 74 | Example args dict: 75 | for action 'ListOrders': 76 | args = { 77 | 'CreatedAfter': '2012-05-03T15:00:13.000Z', 78 | 'OrderStatus': ['Unshipped', 'PartiallyShipped'], 79 | } 80 | 81 | for action 'ListOrdersByNextToken': 82 | args = { 83 | 'NextToken': 'jaiphegaueipraaegrajklh', 84 | } 85 | ''' 86 | client_api_version = __modified__ 87 | mws_api_version = '2013-09-01' 88 | path = '/Orders/2013-09-01' # The path to the server from the endpoint 89 | 90 | supported_actions = ('ListOrders', 'ListOrdersByNextToken', 'GetOrder', 'ListOrderItems', 'ListOrderItemsByNextToken', 'GetServiceStatus') # A list of supported actions in the Orders API 91 | 92 | def __init__(self, *args, **kwargs): 93 | MWS.__init__(self, *args, **kwargs) 94 | 95 | def new_args(self): 96 | ''' 97 | Returns base query args for the Orders API--these 98 | items are used by each class 99 | ''' 100 | return { 101 | 'AWSAccessKeyId': self.access_key, 102 | 'SellerId': self.merchant_id, 103 | 'Timestamp': datetime_to_iso8601(datetime.datetime.utcnow()), 104 | 'Version': self.mws_api_version, 105 | 'MarketplaceId': [ MARKETPLACE_IDS['us'], ] 106 | } 107 | 108 | def send_request(self, action, args_dict): 109 | """ 110 | Send an Orders API request to the Amazon MWS server. 111 | Args: 112 | action[str]: an Orders API supported action. UnsupportActionError is raises if this is an unknown action 113 | args_dict[dict]: dictionary of arguments that follow the Orders API argument guidelines 114 | """ 115 | args = self.new_args() 116 | args['Action'] = action 117 | 118 | query = self._combine_dicts( args, args_dict ) 119 | new_query = {} 120 | for key, value in six.iteritems(query): 121 | self._update_query( new_query, key, value ) 122 | 123 | return MWS.send_request(self, new_query, path=self.path ) 124 | 125 | def _combine_dicts(self, dict1, dict2): 126 | ''' 127 | Safely combine two dicts' items, if the dict's have a key in common, 128 | if the value at that key in dict1 is a list, then value(s) from dict 2 are 129 | appended to the list, otherwise, if the value at the key in dict1 is 130 | a value, then the value in dict2 will overwrite it 131 | ''' 132 | query = dict1 133 | for key, value in six.iteritems(dict2): 134 | if key in query: 135 | if isinstance( query[key], list ): 136 | if isinstance( value, list ): 137 | for item in value: 138 | query[key].append( item) 139 | else: 140 | query[key].append( value ) 141 | else: 142 | query[key] = value 143 | else: 144 | #Update new key:value pair 145 | query[key] = value 146 | 147 | return query 148 | 149 | 150 | def _update_query( self, args, key, value ): 151 | ''' 152 | Updates args with the new key, value 153 | If value is a list, then it flattens out the list into several keys: 154 | example: 155 | 'MarketplaceId : [100,101,102] 156 | ==> 157 | 'MarketplaceId.Id.1: 100, 158 | 'MarketplaceId.Id.2: 101, 159 | 'MarketplaceId.Id.3: 102, 160 | ''' 161 | if isinstance( value, list ): 162 | #Determine the base key 163 | basekey = self._get_key( key ) 164 | for idx, val in enumerate(value): 165 | args[basekey + str(idx+1)] = val 166 | else: 167 | args[key] = value 168 | 169 | def _get_key(self, key): 170 | ''' 171 | Determines the base keyname for keys that have a list as their value. 172 | This is necessary because these keys take up this notation: 173 | original key: 'MarketplaceId' 174 | new key: 'MarketplaceId.Id.x 175 | this function returns: 'MarketplaceId.Id.' 176 | ''' 177 | item = None 178 | for item in re.finditer( r"([A-Z][a-z])+([a-z]+)?", key): 179 | pass 180 | item = item.group(0)#Get the last re.MatchObject from the iterator 181 | 182 | basekey = key + "." + item + "." 183 | return basekey 184 | 185 | def __getattr__(self, api_call): 186 | """ 187 | This is a catch-all for automatic support of any future API call which 188 | has not been explicitly accounted for in this library. 189 | """ 190 | def _call(*args, **kwargs): 191 | # Grab default args. 192 | args = self.new_args() 193 | 194 | # Merge args passed to function w/ default args. 195 | args.update(kwargs) 196 | 197 | return self.send_request(api_call, args) 198 | 199 | return _call 200 | 201 | def ListOrders(self, **kwargs): 202 | """ 203 | Requests the list of Orders that match the specified criteria. 204 | 205 | Returns the response XML (``str``). 206 | 207 | For a complete list of arguments and values: 208 | http://docs.developer.amazonservices.com/en_US/orders/2013-09-01/Orders_ListOrders.html 209 | """ 210 | # Grab default args. 211 | args = self.new_args() 212 | 213 | # Merge args passed to function w/ default args. 214 | args.update(kwargs) 215 | 216 | # Ensure our dates are properly formatted. 217 | if 'CreatedAfter' in kwargs and kwargs['CreatedAfter']: 218 | args['CreatedAfter'] = datetime_to_iso8601(kwargs['CreatedAfter'], name='CreatedAfter') 219 | 220 | if 'CreatedBefore' in kwargs and kwargs['CreatedBefore']: 221 | args['CreatedBefore'] = datetime_to_iso8601(kwargs['CreatedBefore'], name='CreatedBefore') 222 | 223 | if 'LastUpdatedAfter' in kwargs and kwargs['LastUpdatedAfter']: 224 | args['LastUpdatedAfter'] = datetime_to_iso8601(kwargs['LastUpdatedAfter'], name='LastUpdatedAfter') 225 | 226 | if 'LastUpdatedBefore' in kwargs and kwargs['LastUpdatedBefore']: 227 | args['LastUpdatedBefore'] = datetime_to_iso8601(kwargs['LastUpdatedBefore'], name='LastUpdatedBefore') 228 | 229 | return self.send_request('ListOrders', args) 230 | 231 | def ListOrdersByNextToken(self, **kwargs): 232 | """ 233 | Requests the next batch of Orders being listed. 234 | 235 | Returns the response XML (``str``). 236 | 237 | For a complete list of arguments and values: 238 | http://docs.developer.amazonservices.com/en_US/orders/2013-09-01/Orders_ListOrdersByNextToken.html 239 | """ 240 | # Grab default args. 241 | args = self.new_args() 242 | 243 | # Merge args passed to function w/ default args. 244 | args.update(kwargs) 245 | 246 | return self.send_request('ListOrdersByNextToken', args) 247 | -------------------------------------------------------------------------------- /amazonmws/products.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import division 3 | """ 4 | This module provides an implementation of the Amazon MWS Products API. 5 | """ 6 | 7 | __author__ = "Caleb P. Burns" 8 | __created__ = "2012-12-04" 9 | __modified__ = "2018-05-17" 10 | __modified_by___ = "Joshua D. Burns" 11 | 12 | import six # Python2/Python3 compatibility library. 13 | import datetime 14 | import amazonmws.mws 15 | from amazonmws.util import datetime_to_iso8601, is_sequence 16 | 17 | #: Actions. 18 | ACTIONS = { 19 | 'list_matching': 'ListMatchingProducts', 20 | 'get_products': 'GetMatchingProduct', 21 | 'get_products_for_id': 'GetMatchingProductForId', 22 | 'get_competitive_pricing_for_sku': 'GetCompetitivePricingForSKU', 23 | 'get_competitive_pricing_for_asin': 'GetCompetitivePricingForASIN', 24 | 'get_lowest_listings_for_sku': 'GetLowestOfferListingsForSKU', 25 | 'get_lowest_listings_for_asin': 'GetLowestOfferListingsForASIN', 26 | 'get_my_price_for_sku': 'GetMyPriceForSKU', 27 | 'get_my_price_for_asin': 'GetMyPriceForASIN', 28 | 'get_categories_for_sku': 'GetProductCategoriesForSKU', 29 | 'get_categories_for_asin': 'GetProductCategoriesForASIN' 30 | } 31 | 32 | #: Maximum number of requests before being throttled. 33 | THROTTLE_MAX_REQUESTS = { 34 | 'list_matching': 20, 35 | 'get_products': 20, 36 | 'get_products_for_id': 20, 37 | 'get_competitive_pricing_for_sku': 20, 38 | 'get_competitive_pricing_for_asin': 20, 39 | 'get_lowest_listings_for_sku': 20, 40 | 'get_lowest_listings_for_asin': 20, 41 | 'get_my_price_for_sku': 20, 42 | 'get_my_price_for_asin': 20, 43 | 'get_categories_for_sku': 20, 44 | 'get_categories_for_asin': 20 45 | } 46 | 47 | #: The number of seconds it takes to restore 1 request from the quota. 48 | THROTTLE_RESTORE_RATES = { 49 | 'list_matching': 5, 50 | 'get_products': 1 / 2, 51 | 'get_products_for_id': 1 / 5, 52 | 'get_competitive_pricing_for_sku': 1 / 10, 53 | 'get_competitive_pricing_for_asin': 1 / 10, 54 | 'get_lowest_listings_for_sku': 1 / 10, 55 | 'get_lowest_listings_for_asin': 1 / 10, 56 | 'get_my_price_for_sku': 1 / 10, 57 | 'get_my_price_for_asin': 1 / 10, 58 | 'get_categories_for_sku': 5, 59 | 'get_categories_for_asin': 5 60 | } 61 | 62 | #: ID types. 63 | ID_TYPES = { 64 | 'asin': 'ASIN', 65 | 'ean': 'EAN', 66 | 'isbn': 'ISBN', 67 | 'jan': 'JAN', 68 | 'sku': 'SellerSKU', 69 | 'upc': 'UPC' 70 | } 71 | 72 | #: Item conditions. 73 | ITEM_CONDITIONS = { 74 | 'new': 'New', 75 | 'used': 'Used', 76 | 'collectible': 'Collectible', 77 | 'refurbished': 'Refurbished', 78 | 'club': 'Club' 79 | } 80 | 81 | #: Query contexts. 82 | QUERY_CONTEXTS = { 83 | 'all': 'All', 84 | 'apparel': 'Apparel', 85 | 'appliances': 'Appliances', 86 | 'arts_and_crafts': 'ArtsAndCrafts', 87 | 'automotive': 'Automotive', 88 | 'baby': 'Baby', 89 | 'beauty': 'Beauty', 90 | 'books': 'Books', 91 | 'classical': 'Classical', 92 | 'digital_music': 'DigitalMusic', 93 | 'dvd': 'DVD', 94 | 'electronics': 'Electronics', 95 | 'foreign_books': 'ForeignBooks', 96 | 'garden': 'Garden', 97 | 'grocery': 'Grocery', 98 | 'health_personal_care': 'HealthPersonalCare', 99 | 'hobbies': 'Hobbies', 100 | 'home': 'Home', 101 | 'home_garden': 'HomeGarden', 102 | 'home_improvement': 'HomeImprovement', 103 | 'industrial': 'Industrial', 104 | 'jewelry': 'Jewelry', 105 | 'kindle_store': 'KindleStore', 106 | 'kitchen': 'Kitchen', 107 | 'lighting': 'Lighting', 108 | 'magazines': 'Magazines', 109 | 'miscellaneous': 'Miscellaneous', 110 | 'mobile_apps': 'MobileApps', 111 | 'mp3_downloads': 'MP3Downloads', 112 | 'music': 'Music', 113 | 'musical_instruments': 'MusicalInstruments', 114 | 'music_tracks': 'MusicTracks', 115 | 'office_products': 'OfficeProducts', 116 | 'outdoor_living': 'OutdoorLiving', 117 | 'outlet': 'Outlet', 118 | 'pc_hardware': 'PCHardware', 119 | 'pet_supplies': 'PetSupplies', 120 | 'photo': 'Photo', 121 | 'shoes': 'Shoes', 122 | 'software': 'Software', 123 | 'software_video_games': 'SoftwareVideoGames', 124 | 'sporting_goods': 'SportingGoods', 125 | 'tools': 'Tools', 126 | 'toys': 'Toys', 127 | 'unbox_video': 'UnboxVideo', 128 | 'vhs': 'VHS', 129 | 'video': 'Video', 130 | 'video_games': 'VideoGames', 131 | 'watches': 'Watches', 132 | 'wireless': 'Wireless', 133 | 'wireless_accessories': 'WirelessAccessories' 134 | 135 | } 136 | 137 | 138 | class MWSProducts(amazonmws.mws.MWS): 139 | """ 140 | The ``MWSProducts`` class is used to send requests to the Amazon MWS 141 | Products API. The primary purpose of this class is to allow sellers to 142 | get information about products on Amazon that match a seller's 143 | existing products. 144 | """ 145 | 146 | client_api_version = __modified__ 147 | """ 148 | *client_api_version* (``str``) is the version of this client API. 149 | """ 150 | 151 | products_api_version = '2011-07-01' 152 | """ 153 | *products_api_version* (``str``) is the version of the MWS Products 154 | API implemented. 155 | """ 156 | 157 | path = "/Products/2011-10-01" 158 | """ 159 | *path* (``str``) is path all Sellers API requests are sent to. 160 | """ 161 | 162 | def get_categories(self, marketplace_id, id_type, id_, debug=None): 163 | """ 164 | Requests the categories for the specified marketplace product. 165 | 166 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace the 167 | products belong to. 168 | 169 | *id_type* (``str``) is the type of ID used. This can only be 170 | "ASIN" or "SellerSKU". 171 | 172 | *id_* (``str``) is either the ASIN or SKU of the product. 173 | 174 | Returns the response XML (``str``). 175 | """ 176 | if not isinstance(marketplace_id, six.string_types): 177 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 178 | elif not marketplace_id: 179 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 180 | 181 | if not isinstance(id_type, six.string_types): 182 | raise TypeError("id_type:{!r} is not a str.".format(id_type)) 183 | elif not id_type: 184 | raise ValueError("id_type:{!r} cannot be empty.".format(id_type)) 185 | 186 | if not isinstance(id_, six.string_types): 187 | raise TypeError("id_:{!r} is not a str.".format(id_)) 188 | elif not id_: 189 | raise ValueError("id_:{!r} cannot be empty.".format(id_)) 190 | 191 | args = self.new_args() 192 | if id_type == 'ASIN': 193 | args['Action'] = ACTIONS['get_my_price_for_asin'] 194 | args['ASIN'] = id_ 195 | elif id_type == 'SellerSKU': 196 | args['Actions'] = ACTIONS['get_my_price_for_sku'] 197 | args['SellerSKU'] = id_ 198 | else: 199 | raise ValueError("id_type:{!r} is not 'ASIN' or 'SellerSKU'.".format(id_type)) 200 | 201 | return self.send_request(args, path=self.path, debug=debug) 202 | 203 | def get_competitive_pricing(self, marketplace_id, id_type, id_list, debug=None): 204 | """ 205 | Requests the competitive pricing for the specified marketplace 206 | products. 207 | 208 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace the 209 | products belong to. 210 | 211 | *id_type* (``str``) is the type of ID used. This can only be 212 | "ASIN" or "SellerSKU". 213 | 214 | *id_list* (**sequence**) contains the ID (``str``) of each product 215 | to get. A maximum of 20 IDs can be requested at a single time. 216 | 217 | Returns the response XML (``str``). 218 | """ 219 | if not isinstance(marketplace_id, six.string_types): 220 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 221 | elif not marketplace_id: 222 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 223 | 224 | if not isinstance(id_type, six.string_types): 225 | raise TypeError("id_type:{!r} is not a str.".format(id_type)) 226 | elif not id_type: 227 | raise ValueError("id_type:{!r} cannot be empty.".format(id_type)) 228 | 229 | if not is_sequence(id_list): 230 | raise TypeError("id_list:{!r} is not a sequence.".format(id_list)) 231 | elif not id_list: 232 | raise ValueError("id_list:{!r} cannot be empty.".format(id_list)) 233 | elif len(id_list) > 20: 234 | raise ValueError("id_list length:{} cannot be greater than 20.".format(len(id_list))) 235 | 236 | args = self.new_args() 237 | args['IdType'] = id_type 238 | if id_type == 'ASIN': 239 | args['Action'] = ACTIONS['get_competitive_pricing_for_asin'] 240 | args.update({'ASINList.ASIN.{}'.format(i): asin for i, asin in enumerate(id_list, 1)}) 241 | elif id_type == 'SellerSKU': 242 | args['Actions'] = ACTIONS['get_competitive_pricing_for_sku'] 243 | args.update({'SellerSKUList.SellerSKU.{}'.format(i): sku for i, sku in enumerate(id_list, 1)}) 244 | else: 245 | raise ValueError("id_type:{!r} is not 'ASIN' or 'SellerSKU'.".format(id_type)) 246 | 247 | return self.send_request(args, path=self.path, debug=debug) 248 | 249 | def get_lowest_listings(self, marketplace_id, id_type, id_list, condition=None, exclude_me=None, debug=None): 250 | """ 251 | Requests the lowest offer listings for the specified marketplace 252 | products. 253 | 254 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace the 255 | products belong to. 256 | 257 | *id_type* (``str``) is the type of ID used. This can only be 258 | "ASIN" or "SellerSKU". 259 | 260 | *id_list* (**sequence**) contains the ID (``str``) of each product 261 | to get. A maximum of 20 IDs can be requested at a single time. 262 | 263 | *condition* (``str``) optionally filters the returned listings to be 264 | based upon item condition. This can be any key or value from 265 | ``ITEM_CONDITIONS``. Default is ``None`` for no filter. 266 | 267 | *exclude_me* (``bool``) optionally filters out listings that belong 268 | to the seller from the returned listings. This is only valid when 269 | *id_type* is "SellerSKU". Default is ``None`` for no filter. 270 | 271 | Returns the response XML (``str``). 272 | """ 273 | if not isinstance(marketplace_id, six.string_types): 274 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 275 | elif not marketplace_id: 276 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 277 | 278 | if not isinstance(id_type, six.string_types): 279 | raise TypeError("id_type:{!r} is not a str.".format(id_type)) 280 | elif not id_type: 281 | raise ValueError("id_type:{!r} cannot be empty.".format(id_type)) 282 | 283 | if not is_sequence(id_list): 284 | raise TypeError("id_list:{!r} is not a sequence.".format(id_list)) 285 | elif not id_list: 286 | raise ValueError("id_list:{!r} cannot be empty.".format(id_list)) 287 | elif len(id_list) > 20: 288 | raise ValueError("id_list length:{} cannot be greater than 20.".format(len(id_list))) 289 | 290 | if condition is not None: 291 | condition = ITEM_CONDITIONS.get(condition, condition) 292 | if not isinstance(condition, six.string_types): 293 | raise TypeError("condition:{!r} is not a str.".format(condition)) 294 | elif not condition: 295 | raise ValueError("condition:{!r} cannot be empty.".format(condition)) 296 | 297 | args = self.new_args() 298 | args['IdType'] = id_type 299 | if id_type == 'ASIN': 300 | if exclude_me is not None: 301 | raise ValueError("exclude_me:{!r} can only be set when id_type:{!r} is 'SellerSKU'.".format(exclude_me, id_type)) 302 | args['Action'] = ACTIONS['get_lowest_listing_for_asin'] 303 | args.update({'ASINList.ASIN.{}'.format(i): asin for i, asin in enumerate(id_list, 1)}) 304 | elif id_type == 'SellerSKU': 305 | if exclude_me is not None: 306 | args['ExcludeMe'] = 'true' if exclude_me else 'false' 307 | args['Actions'] = ACTIONS['get_lowest_listing_for_sku'] 308 | args.update({'SellerSKUList.SellerSKU.{}'.format(i): sku for i, sku in enumerate(id_list, 1)}) 309 | else: 310 | raise ValueError("id_type:{!r} is not 'ASIN' or 'SellerSKU'.".format(id_type)) 311 | if condition is not None: 312 | args['ItemCondition'] = condition 313 | 314 | return self.send_request(args, path=self.path, debug=debug) 315 | 316 | def get_products(self, marketplace_id, id_type, id_list, debug=None): 317 | """ 318 | Requests the information for the specified marketplace products. 319 | 320 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace the 321 | products are coming from. 322 | 323 | *id_type* (``str``) is the type of ID used. This can be only of the 324 | types listed under ``ID_TYPES``. 325 | 326 | *id_list* (**sequence**) contains the ID (``str``) of each product 327 | to get. A maximum of 10 ASINs, or 5 IDs of another type can be 328 | requested at a single time. 329 | 330 | Returns the response XML (``str``). 331 | """ 332 | if not isinstance(marketplace_id, six.string_types): 333 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 334 | elif not marketplace_id: 335 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 336 | 337 | if not isinstance(id_type, six.string_types): 338 | raise TypeError("id_type:{!r} is not a str.".format(id_type)) 339 | elif not id_type: 340 | raise ValueError("id_type:{!r} cannot be empty.".format(id_type)) 341 | 342 | if not is_sequence(id_list): 343 | raise TypeError("id_list:{!r} is not a sequence.".format(id_list)) 344 | elif not id_list: 345 | raise ValueError("id_list:{!r} cannot be empty.".format(id_list)) 346 | 347 | if len(id_list) > 5: 348 | raise ValueError("id_list length:{} cannot be greater than 5.".format(len(id_list))) 349 | 350 | args = self.new_args() 351 | if id_type == 'ASIN': 352 | args['Action'] = ACTIONS['get_products'] 353 | args.update({'ASINList.ASIN.{}'.format(i): asin for i, asin in enumerate(id_list, 1)}) 354 | else: 355 | args['IdType'] = id_type 356 | args['Action'] = ACTIONS['get_products_for_id'] 357 | args.update({'IDList.ID.{}'.format(i): id_ for i, id_ in enumerate(id_list, 1)}) 358 | 359 | return self.send_request(args, path=self.path, debug=debug) 360 | 361 | def get_my_price(self, marketplace_id, id_type, id_list, condition=None, debug=None): 362 | """ 363 | Requests the seller's price for the specified marketplace products. 364 | 365 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace the 366 | products belong to. 367 | 368 | *id_type* (``str``) is the type of ID used. This can only be 369 | "ASIN" or "SellerSKU". 370 | 371 | *id_list* (**sequence**) contains the ID (``str``) of each product 372 | to get. A maximum of 20 IDs can be requested at a single time. 373 | 374 | *condition* (``str``) optionally filters the returned listings to be 375 | based upon item condition. This can be any key or value from 376 | ``ITEM_CONDITIONS``. Default is ``None`` for no filter. 377 | 378 | Returns the response XML (``str``). 379 | """ 380 | if not isinstance(marketplace_id, six.string_types): 381 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 382 | elif not marketplace_id: 383 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 384 | 385 | if not isinstance(id_type, six.string_types): 386 | raise TypeError("id_type:{!r} is not a str.".format(id_type)) 387 | elif not id_type: 388 | raise ValueError("id_type:{!r} cannot be empty.".format(id_type)) 389 | 390 | if not is_sequence(id_list): 391 | raise TypeError("id_list:{!r} is not a sequence.".format(id_list)) 392 | elif not id_list: 393 | raise ValueError("id_list:{!r} cannot be empty.".format(id_list)) 394 | elif len(id_list) > 20: 395 | raise ValueError("id_list length:{} cannot be greater than 20.".format(len(id_list))) 396 | 397 | if condition is not None: 398 | condition = ITEM_CONDITIONS.get(condition, condition) 399 | if not isinstance(condition, six.string_types): 400 | raise TypeError("condition:{!r} is not a str.".format(condition)) 401 | elif not condition: 402 | raise ValueError("condition:{!r} cannot be empty.".format(condition)) 403 | 404 | args = self.new_args() 405 | args['IdType'] = id_type 406 | if id_type == 'ASIN': 407 | args['Action'] = ACTIONS['get_my_price_for_asin'] 408 | args.update({'ASINList.ASIN.{}'.format(i): asin for i, asin in enumerate(id_list, 1)}) 409 | elif id_type == 'SellerSKU': 410 | args['Actions'] = ACTIONS['get_my_price_for_sku'] 411 | args.update({'SellerSKUList.SellerSKU.{}'.format(i): sku for i, sku in enumerate(id_list, 1)}) 412 | else: 413 | raise ValueError("id_type:{!r} is not 'ASIN' or 'SellerSKU'.".format(id_type)) 414 | if condition is not None: 415 | args['ItemCondition'] = condition 416 | 417 | return self.send_request(args, path=self.path, debug=debug) 418 | 419 | def list_matching(self, marketplace_id, query, context, debug=None): 420 | """ 421 | Requests the marketplace products that match the query. 422 | 423 | *marketplace_id* (``str``) is the ID of the Amazon Marketplace to 424 | list the products from. 425 | 426 | *query* (``str``) is a basic search string. No special functionality 427 | is supported by Amazon for the search query beyond *context*. 428 | 429 | *context* (``str``) is the query context. This is basically the 430 | generic category to search under. This can be any key or value from 431 | ``QUERY_CONTEXTS``. 432 | 433 | Returns the response XML (``str``). 434 | """ 435 | if not isinstance(marketplace_id, six.string_types): 436 | raise TypeError("marketplace_id:{!r} is not a str.".format(marketplace_id)) 437 | elif not marketplace_id: 438 | raise ValueError("marketplace_id:{!r} cannot be empty.".format(marketplace_id)) 439 | 440 | if not isinstance(query, six.string_types): 441 | raise TypeError("query:{!r} is not a str.".format(query)) 442 | elif not query: 443 | raise ValueError("query:{!r} cannot be empty.".format(query)) 444 | 445 | if context is not None: 446 | context = QUERY_CONTEXTS.get(context, context) 447 | if not isinstance(context, six.string_types): 448 | raise TypeError("context:{!r} is not a str.".format(context)) 449 | elif not context: 450 | raise ValueError("context:{!r} cannot be empty.".format(context)) 451 | 452 | args = self.new_args() 453 | args['Action'] = ACTIONS['list_matching'] 454 | args['MarketplaceId'] = marketplace_id 455 | args['Query'] = query 456 | if context is not None: 457 | args['QueryContextId'] = context 458 | 459 | return self.send_request(args, path=self.path, debug=debug) 460 | 461 | def new_args(self): 462 | """ 463 | Returns a new ``dict`` of default arguments. 464 | """ 465 | return { 466 | 'AWSAccessKeyId': self.access_key, 467 | 'SellerId': self.merchant_id, 468 | 'Timestamp': datetime_to_iso8601(datetime.datetime.utcnow()), 469 | 'Version': self.products_api_version 470 | } 471 | -------------------------------------------------------------------------------- /amazonmws/reports.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This module provides an implementation of the Amazon MWS Reports API. 4 | """ 5 | 6 | __author__ = "Caleb P. Burns" 7 | __created__ = "2012-12-31" 8 | __modified__ = "2016-03-29" 9 | __modified_by___ = "Joshua D. Burns" 10 | 11 | import six # Python2/Python3 compatibility library. 12 | import datetime 13 | import amazonmws.mws 14 | from amazonmws.util import datetime_to_iso8601, encode_string, is_sequence, marketplace_args 15 | 16 | #: Report types. 17 | REPORT_TYPES = { 18 | 'listing_cancelled': '_GET_MERCHANT_CANCELLED_LISTINGS_DATA_', 19 | 'listing_compat': '_GET_MERCHANT_LISTINGS_DATA_BACK_COMPAT_', 20 | 'listing_data': '_GET_MERCHANT_LISTINGS_DATA_', 21 | 'listing_defect': '_GET_MERCHANT_LISTINGS_DEFECT_DATA_', 22 | 'listing_lite': '_GET_MERCHANT_LISTINGS_DATA_LITE_', 23 | 'listing_liter': '_GET_MERCHANT_LISTINGS_DATA_LITER_', 24 | 'listing_open': '_GET_FLAT_FILE_OPEN_LISTINGS_DATA_', 25 | 'order_actionable': '_GET_FLAT_FILE_ACTIONABLE_ORDER_DATA_', 26 | 'order_data': '_GET_FLAT_FILE_ORDERS_DATA_', 27 | 'order_reports': '_GET_FLAT_FILE_ORDER_REPORT_DATA_', 28 | 'order_reports_converged': '_GET_CONVERGED_FLAT_FILE_ORDER_REPORT_DATA_', 29 | 'settlement_alt': '_GET_ALT_FLAT_FILE_PAYMENT_SETTLEMENT_DATA_', 30 | 'settlement_csv': '_GET_FLAT_FILE_PAYMENT_SETTLEMENT_DATA_', 31 | 'settlement_xml': '_GET_PAYMENT_SETTLEMENT_DATA_', 32 | } 33 | 34 | #: Report schedules. 35 | REPORT_SCHEDULES = { 36 | '15_min': '_15_MINUTES_', 37 | '20_min': '_30_MINUTES_', 38 | '1_hr': '_1_HOUR_', 39 | '2_hr': '_2_HOURS_', 40 | '4_hr': '_4_HOURS_', 41 | '8_hr': '_8_HOURS_', 42 | '12_hr': '_12_HOURS_', 43 | '72_hr': '_72_HOURS_', 44 | '1_day': '_1_DAYS_', 45 | '2_day': '_2_DAYS_', 46 | '3_day': '_72_HOURS_', 47 | '7_day': '_7_DAYS_', 48 | '14_day': '_14_DAYS_', 49 | '15_day': '_15_DAYS_', 50 | '30_day': '_30_DAYS_', 51 | 'never': '_NEVER_', 52 | } 53 | 54 | #: Report processing statuses. 55 | REPORT_STATUSES = { 56 | 'cancelled': '_CANCELLED_', 57 | 'done': '_DONE_', 58 | 'done_no_data': '_DONE_NO_DATA_', 59 | 'in_progress': '_IN_PROGRESS_', 60 | 'submitted': '_SUBMITTED_', 61 | } 62 | 63 | 64 | class MWSReports(amazonmws.mws.MWS): 65 | """ 66 | The ``MWSReports`` class is used to send requests to the Amazon MWS 67 | Reports API. The primary purpose of this class is to allow sellers to 68 | request various reports. 69 | """ 70 | 71 | client_api_version = __modified__ 72 | """ 73 | *client_api_version* (``str``) is the version of this client API. 74 | """ 75 | 76 | reports_api_version = '2009-01-01' 77 | """ 78 | *products_api_version* (``str``) is the version of the MWS Products 79 | API implemented. 80 | """ 81 | 82 | path = "/" 83 | """ 84 | *path* (``str``) is path all Sellers API requests are sent to. 85 | """ 86 | 87 | def cancel_report_requests(self, requests=None, report_types=None, statuses=None, from_date=None, to_date=None, marketplaces=None, debug=None): 88 | """ 89 | Cancels all Report Requests that match the query. 90 | 91 | .. NOTE:: Report Requests that have already begun processing cannot 92 | be cancelled. 93 | 94 | To cancel Report Requests based upon ID use *requests*. 95 | 96 | *requests* (**sequence**) is used to filter on Report Request ID 97 | (``str``). If not ``None``, no other query arguments will be used 98 | and only those Report Requests with matching IDs will be returned. 99 | Default is ``None`` to not use Report Request IDs. 100 | 101 | To cancel Report Requests based upon a query use any combination of 102 | the following: *report_types*, *statuses*, *from_date*, *to_date*, 103 | and *marketplaces*. 104 | 105 | *report_types* (**sequence**) is used to filter on Report Type 106 | (``str``). This can contain any keys or values from 107 | ``REPORT_TYPES``. Default is ``None`` to not filter on Report Type. 108 | 109 | *statuses* (**sequence**) is used to filter on Report Processing 110 | Status (``str``). This can contain any keys or values from 111 | ``REPORT_STATUSES``. Default is ``None`` to not filter on Report 112 | Processing Status. 113 | 114 | *from_date* (``datetime`` or ``float``) is the start of the date 115 | range to use for selecting Report Requests. Default is ``None`` for 116 | 90 days ago. 117 | 118 | *to_date* (``datetime`` or ``float``) is the end of the date range 119 | to use for selecting Report Requests. Default is ``None`` for now. 120 | 121 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 122 | (``str``). Default is ``None`` for all marketplaces. 123 | 124 | Returns the raw XML response (``str``). 125 | """ 126 | if from_date is not None: 127 | from_date = datetime_to_iso8601(from_date, name='from_date') 128 | 129 | if to_date is not None: 130 | to_date = datetime_to_iso8601(to_date, name='to_date') 131 | 132 | # Build args. 133 | args = self.new_args() 134 | args['Action'] = 'CancelReportRequests' 135 | 136 | if requests: 137 | args.update(request_args(requests, name='requests')) 138 | 139 | else: 140 | if report_types is not None: 141 | args.update(report_type_args(report_types, name='report_types')) 142 | 143 | if statuses is not None: 144 | args.update(status_args(statuses, name='statuses')) 145 | 146 | if from_date: 147 | args['RequestedFromDate'] = from_date 148 | 149 | if to_date: 150 | args['RequestedToDate'] = to_date 151 | 152 | if marketplaces is not None: 153 | args.update(marketplace_args(marketplaces, name='marketplaces')) 154 | 155 | # Send request. 156 | return self.send_request(args, debug=debug) 157 | 158 | def get_report(self, report_id, marketplaces=None, debug=None): 159 | """ 160 | Gets the contents of the Report. 161 | 162 | *report_id* (``int``) is the Report ID. 163 | 164 | *marketplaces* (**sequence**) is the list of marketplace IDs 165 | (``str``). Default is ``None`` for all marketplaces. 166 | 167 | Returns the contents of the Report. 168 | """ 169 | if not isinstance(report_id, six.integer_types): 170 | raise TypeError("report_id:{!r} is not an integer.".format(report_id)) 171 | elif report_id < 0: 172 | raise ValueError("report_id:{!r} cannot be less than 0.".format(report_id)) 173 | 174 | # Build args. 175 | args = self.new_args() 176 | args['Action'] = 'GetReport' 177 | args['ReportId'] = report_id 178 | 179 | if marketplaces is not None: 180 | args.update(marketplace_args(marketplaces, name='marketplaces')) 181 | 182 | # Send request. 183 | return self.send_request(args, debug=debug) 184 | 185 | def get_report_count(self, report_types=None, acknowledged=None, from_date=None, to_date=None, marketplaces=None, debug=None): 186 | """ 187 | Gets the total number of Reports that match the query. 188 | 189 | *report_types* (**sequence**) is used to filter on Report Type 190 | (``str``). This can contain any keys or values from 191 | ``REPORT_TYPES``. Default is ``None`` to not filter on Report Type. 192 | 193 | *acknowledged* (``bool``) is used to filter on whether Order Reports 194 | have been acknowledged (``True``), or not (``False``). Default is 195 | ``None`` to not filter on Acknowledged state. 196 | 197 | .. NOTE:: Setting *acknowledged* to ``True`` will result in only 198 | Order Reports (not Listing Reports) being returned. 199 | 200 | *from_date* (``datetime`` or ``float``) is the start of the date 201 | range to use for selecting Report Requests. Default is ``None`` for 202 | 90 days ago. 203 | 204 | *to_date* (``datetime`` or ``float``) is the end of the date range 205 | to use for selecting Report Requests. Default is ``None`` for now. 206 | 207 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 208 | (``str``). Default is ``None`` for all marketplaces. 209 | 210 | Returns the raw XML response (``str``). 211 | """ 212 | if acknowledged is not None: 213 | acknowledged = bool(acknowledged) 214 | 215 | if from_date is not None: 216 | from_date = datetime_to_iso8601(from_date, name='from_date') 217 | 218 | if to_date is not None: 219 | to_date = datetime_to_iso8601(to_date, name='to_date') 220 | 221 | # Build args. 222 | args = self.new_args() 223 | args['Action'] = 'GetReportCount' 224 | 225 | if report_types is not None: 226 | args.update(report_type_args(report_types, name='report_types')) 227 | 228 | if acknowledged is not None: 229 | args['Acknowledged'] = 'true' if acknowledged else 'false' 230 | 231 | if from_date: 232 | args['RequestedFromDate'] = from_date 233 | 234 | if to_date: 235 | args['RequestedToDate'] = to_date 236 | 237 | if marketplaces is not None: 238 | args.update(marketplace_args(marketplaces, name='marketplaces')) 239 | 240 | # Send request. 241 | return self.send_request(args, debug=debug) 242 | 243 | def get_report_list(self, requests=None, max_count=None, report_types=None, acknowledged=None, from_date=None, to_date=None, marketplaces=None, debug=None): 244 | """ 245 | Lists the Reports that match the query. 246 | 247 | To list Reports based upon Request ID use *requests*. 248 | 249 | *requests* (**sequence**) is used to filter on Report Request ID 250 | (``str``). If not ``None``, no other query arguments will be used 251 | and only those Report Requests with matching IDs will be returned. 252 | Default is ``None`` to not use Report Request IDs. 253 | 254 | To list Reports based upon a query use any combination of the 255 | following: *max_count*, *report_types*, *acknowledged*, *from_date*, 256 | *to_date*, and *marketplaces*. 257 | 258 | *max_count* (``int``) is the maximum number of Reports to return per 259 | response. This must between 1 and 100 inclusive. Default is 10. 260 | 261 | *report_types* (**sequence**) is used to filter on Report Type 262 | (``str``). This can contain any keys or values from 263 | ``REPORT_TYPES``. Default is ``None`` to not filter on Report Type. 264 | 265 | *acknowledged* (``bool``) is used to filter on whether Order Reports 266 | have been acknowledged (``True``), or not (``False``). Default is 267 | ``None`` to not filter on Acknowledged state. 268 | 269 | .. NOTE:: Setting *acknowledged* to ``True`` will result in only 270 | Order Reports (not Listing Reports) being returned. 271 | 272 | *from_date* (``datetime`` or ``float``) is the start of the date 273 | range to use for selecting Report Requests. Default is ``None`` for 274 | 90 days ago. 275 | 276 | *to_date* (``datetime`` or ``float``) is the end of the date range 277 | to use for selecting Report Requests. Default is ``None`` for now. 278 | 279 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 280 | (``str``). Default is ``None`` for all marketplaces. 281 | 282 | Returns the raw XML response (``str``). 283 | """ 284 | if max_count is not None: 285 | if not isinstance(max_count, six.integer_types): 286 | raise TypeError("max_count:{!r} is not an int.".format(max_count)) 287 | elif max_count < 1 or 100 < max_count: 288 | raise ValueError("max_count:{!r} is not between 1 and 100 inclusive.".format(max_count)) 289 | 290 | if acknowledged is not None: 291 | acknowledged = bool(acknowledged) 292 | 293 | if from_date is not None: 294 | from_date = datetime_to_iso8601(from_date, name='from_date') 295 | 296 | if to_date is not None: 297 | to_date = datetime_to_iso8601(to_date, name='to_date') 298 | 299 | # Build args. 300 | args = self.new_args() 301 | args['Action'] = 'GetReportList' 302 | 303 | if requests: 304 | args.update(request_args(requests, name='requests')) 305 | 306 | else: 307 | if max_count: 308 | args['MaxCount'] = max_count 309 | 310 | if report_types is not None: 311 | args.update(report_type_args(report_types, name='report_types')) 312 | 313 | if acknowledged is not None: 314 | args['Acknowledged'] = 'true' if acknowledged else 'false' 315 | 316 | if from_date: 317 | args['RequestedFromDate'] = from_date 318 | 319 | if to_date: 320 | args['RequestedToDate'] = to_date 321 | 322 | if marketplaces is not None: 323 | args.update(marketplace_args(marketplaces, name='marketplaces')) 324 | 325 | # Send request. 326 | return self.send_request(args, debug=debug) 327 | 328 | def get_report_list_next(self, next_token, debug=None): 329 | """ 330 | Requests the next batch of Reports that match the original Get 331 | Report List query. 332 | 333 | *next_token* (``str``) is the token returned from the last request. 334 | 335 | Returns the raw XML response (``str``). 336 | """ 337 | if not isinstance(next_token, six.string_types): 338 | raise TypeError("next_token:{!r} is not a string.".format(next_token)) 339 | elif not next_token: 340 | raise ValueError("next_token:{!r} cannot be empty.".format(next_token)) 341 | next_token = next_token.encode('ASCII') 342 | 343 | # Build args. 344 | args = self.new_args() 345 | args['Action'] = 'GetReportListByNextToken' 346 | args['NextToken'] = next_token 347 | 348 | # Send request. 349 | return self.send_request(args, debug=debug) 350 | 351 | def get_report_request_count(self, report_types=None, statuses=None, from_date=None, to_date=None, marketplaces=None, debug=None): 352 | """ 353 | Gets the total number of Report Requests that match the query. 354 | 355 | *report_types* (**sequence**) is used to filter on Report Type 356 | (``str``). This can contain any keys or values from 357 | ``REPORT_TYPES``. Default is ``None`` to not filter on Report Type. 358 | 359 | *statuses* (**sequence**) is used to filter on Report Processing 360 | Status (``str``). This can contain any keys or values from 361 | ``REPORT_STATUSES``. Default is ``None`` to not filter on Report 362 | Processing Status. 363 | 364 | *from_date* (``datetime`` or ``float``) is the start of the date 365 | range to use for selecting Report Requests. Default is ``None`` for 366 | 90 days ago. 367 | 368 | *to_date* (``datetime`` or ``float``) is the end of the date range 369 | to use for selecting Report Requests. Default is ``None`` for now. 370 | 371 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 372 | (``str``). Default is ``None`` for all marketplaces. 373 | 374 | Returns the raw XML response (``str``). 375 | """ 376 | if from_date is not None: 377 | from_date = datetime_to_iso8601(from_date, name='from_date') 378 | 379 | if to_date is not None: 380 | to_date = datetime_to_iso8601(to_date, name='to_date') 381 | 382 | # Build args. 383 | args = self.new_args() 384 | args['Action'] = 'GetReportRequestCount' 385 | 386 | if report_types is not None: 387 | args.update(report_type_args(report_types, name='report_types')) 388 | 389 | if statuses is not None: 390 | args.update(status_args(statuses, name='statuses')) 391 | 392 | if from_date: 393 | args['RequestedFromDate'] = from_date 394 | 395 | if to_date: 396 | args['RequestedToDate'] = to_date 397 | 398 | if marketplaces is not None: 399 | args.update(marketplace_args(marketplaces, name='marketplaces')) 400 | 401 | # Send request. 402 | return self.send_request(args, debug=debug) 403 | 404 | def get_report_request_list(self, requests=None, max_count=None, report_types=None, statuses=None, from_date=None, to_date=None, marketplaces=None, debug=None): 405 | """ 406 | Requests for the list of Report Requests that match the query. 407 | 408 | To list Report Requests based upon ID use *requests*. 409 | 410 | *requests* (**sequence**) is used to filter on Report Request ID 411 | (``str``). If not ``None``, no other query arguments will be used 412 | and only those Report Requests with matching IDs will be returned. 413 | Default is ``None`` to not use Report Request IDs. 414 | 415 | To list Report Requests based upon a query use any combination of 416 | the following: *max_count*, *report_types*, *statuses*, *from_date*, 417 | *to_date*, and *marketplaces*. 418 | 419 | *max_count* (``int``) is the maximum number of Report Requests to 420 | return per response. This must between 1 and 100 inclusive. Default 421 | is 10. 422 | 423 | *report_types* (**sequence**) is used to filter on Report Type 424 | (``str``). This can contain any keys or values from 425 | ``REPORT_TYPES``. Default is ``None`` to not filter on Report Type. 426 | 427 | *statuses* (**sequence**) is used to filter on Report Processing 428 | Status (``str``). This can contain any keys or values from 429 | ``REPORT_STATUSES``. Default is ``None`` to not filter on Report 430 | Processing Status. 431 | 432 | *from_date* (``datetime`` or ``float``) is the start of the date 433 | range to use for selecting Report Requests. Default is ``None`` for 434 | 90 days ago. 435 | 436 | *to_date* (``datetime`` or ``float``) is the end of the date range 437 | to use for selecting Report Requests. Default is ``None`` for now. 438 | 439 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 440 | (``str``). Default is ``None`` for all marketplaces. 441 | 442 | Returns the raw XML response (``str``). 443 | """ 444 | if max_count is not None: 445 | if not isinstance(max_count, six.integer_types): 446 | raise TypeError("max_count:{!r} is not an int.".format(max_count)) 447 | elif max_count < 1 or 100 < max_count: 448 | raise ValueError("max_count:{!r} is not between 1 and 100 inclusive.".format(max_count)) 449 | 450 | if from_date is not None: 451 | from_date = datetime_to_iso8601(from_date, name='from_date') 452 | 453 | if to_date is not None: 454 | to_date = datetime_to_iso8601(to_date, name='to_date') 455 | 456 | # Build args. 457 | args = self.new_args() 458 | args['Action'] = 'GetReportRequestList' 459 | 460 | if requests: 461 | args.update(request_args(requests, name='requests')) 462 | 463 | else: 464 | if max_count: 465 | args['MaxCount'] = max_count 466 | 467 | if report_types is not None: 468 | args.update(report_type_args(report_types, name='report_types')) 469 | 470 | if statuses is not None: 471 | args.update(status_args(statuses, name='statuses')) 472 | 473 | if from_date: 474 | args['RequestedFromDate'] = from_date 475 | 476 | if to_date: 477 | args['RequestedToDate'] = to_date 478 | 479 | if marketplaces is not None: 480 | args.update(marketplace_args(marketplaces, name='marketplaces')) 481 | 482 | # Send request. 483 | return self.send_request(args, debug=debug) 484 | 485 | def get_report_request_list_next(self, next_token, debug=None): 486 | """ 487 | Requests the next batch of Report Requests that match the original 488 | Get Report Request List query. 489 | 490 | *next_token* (``str``) is the token returned from the last request. 491 | 492 | Returns the raw XML response (``str``). 493 | """ 494 | if not isinstance(next_token, six.string_types): 495 | raise TypeError("next_token:{!r} is not a str.".format(next_token)) 496 | elif not next_token: 497 | raise ValueError("next_token:{!r} cannot be empty.".format(next_token)) 498 | next_token = next_token.encode('ASCII') 499 | 500 | # Build args. 501 | args = self.new_args() 502 | args['Action'] = 'GetReportRequestListByNextToken' 503 | args['NextToken'] = next_token 504 | 505 | # Send request. 506 | return self.send_request(args, debug=debug) 507 | 508 | def new_args(self): 509 | """ 510 | Returns a new set of default arguments (``dict``). 511 | 512 | *marketplaces* (**sequence**) is the list of marketplace IDs 513 | (``str``). Default is ``None``. 514 | """ 515 | return { 516 | 'AWSAccessKeyId': self.access_key, 517 | 'SellerId': self.merchant_id, 518 | 'Timestamp': datetime_to_iso8601(datetime.datetime.utcnow()), 519 | 'Version': self.reports_api_version 520 | } 521 | 522 | def request_report(self, report_type, start_date=None, end_date=None, show_sales_channel=None, marketplaces=None, debug=None): 523 | """ 524 | Requests that the specified Report be created. 525 | 526 | *report_type* (``str``) is the report type. 527 | 528 | *start_date* (``datetime`` or ``float``) is the start of the date 529 | range used for selecting the data to report. Default is ``None`` for 530 | now. 531 | 532 | *end_date* (``datetime`` or ``float``) is the end of the date range 533 | used for selecting the data to report. Default is ``None`` for now. 534 | 535 | *show_sales_channel* (``bool``) indicates that an additional column 536 | for several Order Reports should be shown (``True``), or not 537 | (``False``). Default is ``None`` to not show the additional column. 538 | 539 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 540 | (``str``). Default is ``None`` for all marketplaces. 541 | 542 | Returns the Report Request ID (``str``) if the response is to be 543 | parsed; otherwise, the raw XML response (``str``) 544 | """ 545 | report_type = REPORT_TYPES.get(report_type, report_type) 546 | if not isinstance(report_type, six.string_types): 547 | raise TypeError("report_type:{!r} is not a string.".format(report_type)) 548 | elif not report_type: 549 | raise ValueError("report_type:{!r} cannot be empty.".format(report_type)) 550 | report_type = report_type.encode('ASCII') 551 | 552 | if start_date is not None: 553 | start_date = datetime_to_iso8601(start_date, name='start_date') 554 | 555 | if end_date is not None: 556 | end_date = datetime_to_iso8601(end_date, name='end_date') 557 | 558 | if show_sales_channel is not None: 559 | show_sales_channel = bool(show_sales_channel) 560 | 561 | # Build request. 562 | args = self.new_args() 563 | args['Action'] = 'RequestReport' 564 | args['ReportType'] = report_type 565 | 566 | if start_date: 567 | args['StartDate'] = start_date 568 | 569 | if end_date: 570 | args['EndDate'] = end_date 571 | 572 | if show_sales_channel is not None: 573 | args['ReportOptions=ShowSalesChannel'] = 'true' if show_sales_channel else 'false' 574 | 575 | if marketplaces is not None: 576 | args.update(marketplace_args(marketplaces, name='marketplaces')) 577 | 578 | # Send request. 579 | return self.send_request(args, debug=debug) 580 | 581 | def update_report_acknowledgements(self, reports, acknowledged=None, marketplaces=None, debug=None): 582 | """ 583 | Updates the acknowledged status of the specified Reports. 584 | 585 | *reports* (**sequence**) is the list of Report IDs (``int``) to 586 | update. The maximum number of Reports that can be specified is 100. 587 | 588 | *acknowledged* (**boolean**) is whether or not to mark the reports 589 | passed as acknowledged. Default is ``None``. Amazon MWS treats the 590 | absense of acknowledged by defaulting the value to True. 591 | 592 | *marketplaces* (**sequence**) is the list of Amazon Marketplace IDs 593 | (``str``). Default is ``None`` for all marketplaces. 594 | """ 595 | if not is_sequence(reports): 596 | raise TypeError("reports:{!r} is not a sequence.".format(reports)) 597 | elif len(reports) < 0 or 100 < len(reports): 598 | raise ValueError("reports len:{!r} must be between 1 and 100 inclusive.".format(len(reports))) 599 | 600 | # Build args. 601 | args = self.new_args() 602 | args['Action'] = 'UpdateReportAcknowledgements' 603 | 604 | if acknowledged is True: 605 | args['Acknowledged'] = 'true' 606 | elif acknowledged is False: 607 | args['Acknowledged'] = 'false' 608 | elif acknowledged is not None: 609 | raise TypeError("reports['acknowledged']:{!r} is not boolean.".format(acknowledged)) 610 | 611 | for i, report_id in enumerate(reports, 1): 612 | if not isinstance(report_id, six.string_types): 613 | raise TypeError("reports[{}]:{!r} is not a string.".format(i, report_id)) 614 | elif not report_id: 615 | raise ValueError("reports[{}]:{!r} cannot be empty.".format(i, report_id)) 616 | report_id = encode_string(report_id, 'ASCII', name="reports[{}]".format(i)) 617 | 618 | args['ReportIdList.Id.{}'.format(i)] = report_id 619 | 620 | if marketplaces is not None: 621 | args.update(marketplace_args(marketplaces, name='marketplaces')) 622 | 623 | # Send Request. 624 | return self.send_request(args, debug=debug) 625 | 626 | def manage_report_schedule(self): 627 | # TODO 628 | raise NotImplementedError() 629 | 630 | def get_report_schedule_count(self): 631 | # TODO 632 | raise NotImplementedError() 633 | 634 | def get_report_schedule_list(self): 635 | # TODO 636 | raise NotImplementedError() 637 | 638 | def get_report_schedule_list_next(self): 639 | # TODO 640 | raise NotImplementedError() 641 | 642 | 643 | def report_type_args(report_types, name=None): 644 | """ 645 | Converts the specified Report Types into their respective URL query 646 | arguments. 647 | 648 | *report_types* (**sequence**) contains each Report Type (``str``). 649 | This can contain any keys or values from ``REPORT_TYPES``. 650 | 651 | *name* (``str``) is the name to use when an error occurs. 652 | 653 | Returns a ``list`` containing each *key*-*report_type* ``tuple``. 654 | 655 | - *key* (``str``) is the query argument key for *report_type*. 656 | 657 | - *report_type* (``str``) is the Report Type. 658 | """ 659 | if not name: 660 | name = 'report_types' 661 | 662 | if not is_sequence(report_types): 663 | raise TypeError("{}:{!r} is not a sequence.".format(name, report_types)) 664 | 665 | args = [] 666 | for i, report_type in enumerate(report_types, 0): 667 | report_type = REPORT_TYPES.get(report_type, report_type) 668 | if not isinstance(report_type, six.string_types): 669 | raise TypeError("{}[{}]:{!r} is not a string.".format(name, i, report_type)) 670 | elif not report_type: 671 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, report_type)) 672 | report_type = encode_string(report_type, 'ASCII', name="{}[{}]".format(name, i)) 673 | 674 | args.append(('ReportTypeList.Type.{}'.format(i + 1), report_type)) 675 | 676 | return args 677 | 678 | def request_args(requests, name=None): 679 | """ 680 | Converts the specified Report Request IDs into their respective URL 681 | query arguments. 682 | 683 | *requests* (**sequence**) contains each Report Request ID (``str``). 684 | 685 | *name* (``str``) is the name to use when an error occurs. 686 | 687 | Returns a ``list`` containing each *key*-*request_id* ``tuple``. 688 | 689 | - *key* (``str``) is the query argument key for *request_id*. 690 | 691 | - *request_id* (``str``) is the Report Request ID. 692 | """ 693 | if not name: 694 | name = 'requests' 695 | 696 | if not is_sequence(requests): 697 | raise TypeError("{}:{!r} is not a sequence.".format(name, requests)) 698 | 699 | args = [] 700 | for i, request_id in enumerate(requests, 0): 701 | if not isinstance(request_id, six.string_types): 702 | raise TypeError("{}[{}]:{!r} is not a string.".format(name, i, request_id)) 703 | elif not request_id: 704 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, request_id)) 705 | request_id = encode_string(request_id, 'ASCII', name="{}[{}]".format(name, i)) 706 | 707 | args.append(('ReportRequestIdList.Id.{}'.format(i + 1), request_id)) 708 | 709 | return args 710 | 711 | def status_args(statuses, name=None): 712 | """ 713 | Converts the specified Report Processing Status into their respective 714 | URL query arguments. 715 | 716 | *statuses* (**sequence**) contains each Report Processing Status 717 | (``str``). This contain any keys or values from ``REPORT_STATUSES``. 718 | 719 | *name* (``str``) is the name to use when an error occurs. 720 | 721 | Returns a ``list`` containing each *key*-*status* ``tuple``. 722 | 723 | - *key* (``str``) is the query argument key for *status*. 724 | 725 | - *request_id* (``str``) is the Report Processing Status. 726 | """ 727 | if not name: 728 | name = 'statuses' 729 | 730 | if not is_sequence(statuses): 731 | raise TypeError("{}:{!r} is not a statuses.".format(name, statuses)) 732 | 733 | args = [] 734 | for i, status in enumerate(statuses, 0): 735 | if not isinstance(status, six.string_types): 736 | raise TypeError("{}[{}]:{!r} is not a string.".format(name, i, status)) 737 | elif not status: 738 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, status)) 739 | status = encode_string(status, 'ASCII', name="{}[{}]".format(name, i)) 740 | 741 | args.append(('ReportProcessingStatusList.Status.{}'.format(i + 1), status)) 742 | 743 | return args 744 | -------------------------------------------------------------------------------- /amazonmws/sellers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This module provides an implementation of the Amazon MWS Sellers API. 4 | """ 5 | 6 | __author__ = "Caleb P. Burns" 7 | __created__ = "2012-12-03" 8 | __modified__ = "2016-03-29" 9 | __modified_by___ = "Joshua D. Burns" 10 | 11 | import six # Python2/Python3 compatibility library. 12 | import datetime 13 | import amazonmws.mws 14 | from amazonmws.util import datetime_to_iso8601 15 | 16 | #: Actions. 17 | ACTIONS = { 18 | 'get_status': 'GetServiceStatus', 19 | 'list_marketplaces': 'ListMarketplaceParticipations', 20 | 'list_marketplaces_next': 'ListMarketplaceParticipationsByNextToken', 21 | } 22 | 23 | 24 | class MWSSellers(amazonmws.mws.MWS): 25 | """ 26 | The ``MWSSellers`` class is used to send requests to the Amazon MWS 27 | Sellers API. The primary purpose of this class is to allow sellers 28 | request information about their seller account. 29 | """ 30 | 31 | client_api_version = __modified__ 32 | """ 33 | *client_api_version* (``str``) is the version of this client API. 34 | """ 35 | 36 | sellers_api_version = '2011-07-01' 37 | """ 38 | *sellers_api_version* (``str``) is the version of the MWS Sellers API 39 | implemented. 40 | """ 41 | 42 | path = "/Sellers/2011-07-01" 43 | """ 44 | *path* (``str``) is path all Sellers API requests are sent to. 45 | """ 46 | 47 | def get_status(self, debug=None): 48 | """ 49 | Requests the operational status of the Sellers API. 50 | 51 | Returns the response XML (``str``). 52 | """ 53 | args = self.new_args() 54 | args['Action'] = ACTIONS['get_status'] 55 | return self.send_request(args, path=self.path, debug=debug) 56 | 57 | def list_marketplaces(self, debug=None): 58 | """ 59 | Requests the marketplaces the seller can sell in. 60 | 61 | Returns the response XML (``str``). 62 | """ 63 | args = self.new_args() 64 | args['Action'] = ACTIONS['list_marketplaces'] 65 | return self.send_request(args, path=self.path, debug=debug) 66 | 67 | def list_marketplaces_next(self, next_token, debug=None): 68 | """ 69 | Requests the next batch of marketplaces the seller can sell in. 70 | 71 | *next_token* (``str``) is the token used to fetch the next batch of 72 | results. 73 | 74 | Returns the response XML (``str``). 75 | """ 76 | if not isinstance(next_token, six.string_types): 77 | raise TypeError("next_token:{!r} is not a str.".format(next_token)) 78 | elif not next_token: 79 | raise ValueError("next_token:{!r} cannot be empty.".format(next_token)) 80 | 81 | args = self.new_args() 82 | args['Action'] = ACTIONS['list_marketplaces_next'] 83 | args['NextToken'] = next_token 84 | return self.send_request(args, path=self.path, debug=debug) 85 | 86 | def new_args(self): 87 | """ 88 | Returns a new ``dict`` of default arguments. 89 | """ 90 | return { 91 | 'AWSAccessKeyId': self.access_key, 92 | 'SellerId': self.merchant_id, 93 | 'Timestamp': datetime_to_iso8601(datetime.datetime.utcnow()), 94 | 'Version': self.sellers_api_version 95 | } 96 | -------------------------------------------------------------------------------- /amazonmws/util.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This module contains utility methods that are not necessarily specific 4 | to Amazon MWS, but are used throughout the implementation of its APIs. 5 | """ 6 | 7 | __author__ = "Caleb P. Burns" 8 | __created__ = "2012-11-26" 9 | __modified__ = "2016-04-05" 10 | __modified_by___ = "Joshua D. Burns" 11 | 12 | import six # Python2/Python3 compatibility library. 13 | import collections 14 | import datetime 15 | 16 | def datetime_to_iso8601(dt, name=None): 17 | """ 18 | Formats a datetime as an ISO 8601 string. 19 | 20 | *dt* is the datetime. If this is a ``float`` (or ``int`` or ``long``), 21 | it will be considered to be the seconds passed since the UNIX epoch 22 | relative to UTC. If this is a **naive** ``datetime``, it will also be 23 | considered relative to UTC. If this is a ``date``, it will be 24 | converted a ``datetime`` at midnight relative to UTC. 25 | 26 | *name* (``str``) is the name to use when an error occurs. 27 | 28 | Returns the formatted datetime (``str``). 29 | """ 30 | if not isinstance(dt, datetime.datetime): 31 | if isinstance(dt, (float,)) or isinstance(dt, six.integer_types): 32 | dt = datetime.datetime.utcfromtimestamp(dt) 33 | elif isinstance(dt, datetime.date): 34 | dt = datetime.datetime.combine(dt, datetime.time(0)) 35 | else: 36 | raise TypeError("{}:{!r} is not a datetime or float.".format(name or 'dt', dt)) 37 | datestr = dt.isoformat() 38 | if dt.utcoffset() is None: 39 | datestr += '+00:00' 40 | return datestr 41 | 42 | def encode_string(value, encoding, name=None): 43 | """ 44 | Encodes the specified string. 45 | 46 | *value* (**string**) is the string to encode. 47 | 48 | *encoding* (``str``) is the encoding to use. 49 | 50 | *name* (``str``) is the name to use when an error occurs. 51 | 52 | Returns the encoded string (``str``). 53 | """ 54 | try: 55 | return value.encode(encoding) 56 | except UnicodeDecodeError as e: 57 | if name: 58 | raise UnicodeDecodeError(e.encoding, e.object, e.start, e.end, e.reason + " for {}".format(name)) 59 | raise 60 | 61 | def is_sequence(obj): 62 | """ 63 | Determines whether the specified object is a sequence. 64 | 65 | .. NOTE:: This excludes strings. 66 | 67 | *obj* (``object``) is the object to check. 68 | 69 | Returns whether the specified object is a sequence (``bool``). 70 | """ 71 | return isinstance(obj, collections.Sequence) and not isinstance(obj, six.string_types) 72 | 73 | def marketplace_args(marketplaces, name=None): 74 | """ 75 | Converts the specified Amazon Marketplace IDs into their respective 76 | URL query arguments. 77 | 78 | *marketplaces* (``sequence``) contains each Amazon Marketplace ID 79 | (``str``). 80 | 81 | *name* (``str``) is the name to use when an error occurs. 82 | 83 | Returns a ``list`` containing each *key*-*marketplace_id* ``tuple``. 84 | 85 | - *key* (``str``) is the query argument key for *marketplace_id*. 86 | 87 | - *marketplace_id* (``str``) is the Amazon Marketplace ID. 88 | """ 89 | if not name: 90 | name = 'marketplaces' 91 | 92 | if not is_sequence(marketplaces): 93 | raise TypeError("{}:{!r} is not a sequence.".format(name, marketplaces)) 94 | 95 | args = [] 96 | for i, marketplace_id in enumerate(marketplaces, 0): 97 | if not isinstance(marketplace_id, six.string_types): 98 | raise TypeError("{}[{}]:{!r} is not a string.".format(name, i, marketplace_id)) 99 | elif not marketplace_id: 100 | raise ValueError("{}[{}]:{!r} cannot be empty.".format(name, i, marketplace_id)) 101 | #marketplace_id = encode_string(marketplace_id, 'ASCII', name="{}[{}]".format(name, i)) # TODO: Why were we encoding? This was causing issues in python 3. 102 | 103 | args.append(('MarketplaceIdList.Id.{}'.format(i + 1), marketplace_id)) 104 | 105 | return args 106 | 107 | def validate_dict(value, name=None, keys=None): 108 | """ 109 | Validates the specified dictionary. 110 | 111 | *value* (``dict``) is the dictionary to validate. 112 | 113 | *name* (``str``) optionally is the name to use when an error occurs. 114 | 115 | *keys* (**sequence**) optionally contains each key (``object``) that 116 | must be in *value*. 117 | 118 | Raises an ``Exception`` if *value* is not valid. 119 | """ 120 | if not name: 121 | name = 'value' 122 | 123 | if not isinstance(value, dict): 124 | raise TypeError("{}:{!r} is not a dict.".format(name, value)) 125 | 126 | if keys is not None: 127 | for key in keys: 128 | if key not in value: 129 | raise KeyError("{}[{!r}] is not set.".format(name, key)) 130 | 131 | def validate_float(value, name=None, range_=None): 132 | """ 133 | Validates the specified float. 134 | 135 | *value* (``float``) is the float to validate. 136 | 137 | *name* (``str``) optionally is the name to use when an error occurs. 138 | 139 | *range_* (``tuple``) optionally contains: the minumum (``float``) and 140 | maximum (``float``) values of *value*. The minimum and maximum values 141 | can be ``None`` for no restriction. Default is ``None`` for 142 | ``(None, None)``. 143 | 144 | Raises an ``Exception`` if *value* is not valid. 145 | """ 146 | if not name: 147 | name = 'value' 148 | if range_ is not None: 149 | min_, max_ = range_ 150 | else: 151 | min_ = None 152 | max_ = None 153 | 154 | if not isinstance(value, (float,)) or isinstance(value, six.integer_types): 155 | raise TypeError("{}:{!r} is not a float.".format(name, value)) 156 | 157 | if min_ is not None and value < min_: 158 | raise ValueError("{}:{!r} cannot be less than {}.".format(name, value, min_)) 159 | elif max_ is not None and value > max_: 160 | raise ValueError("{}:{!r} cannot be greater than {}.".format(name, value, max_)) 161 | 162 | def validate_integer(value, name=None, range_=None): 163 | """ 164 | Validates the specified integer. 165 | 166 | *value* (``int`` or ``long``) is the integer to validate. 167 | 168 | *name* (``str``) optionally is the name to use when an error occurs. 169 | 170 | *range_* (``tuple``) optionally contains: the minumum (``int``) and 171 | maximum (``int``) values of *value*. The minimum and maximum values 172 | can be ``None`` for no restriction. Default is ``None`` for 173 | ``(None, None)``. 174 | 175 | Raises an ``Exception`` if *value* is not valid. 176 | """ 177 | if not name: 178 | name = 'value' 179 | if range_ is not None: 180 | min_, max_ = range_ 181 | else: 182 | min_ = None 183 | max_ = None 184 | 185 | if not isinstance(value, six.integer_types): 186 | raise TypeError("{}:{!r} is not an integer.".format(name, value)) 187 | 188 | if min_ is not None and value < min_: 189 | raise ValueError("{}:{!r} cannot be less than {}.".format(name, value, min_)) 190 | elif max_ is not None and value > max_: 191 | raise ValueError("{}:{!r} cannot be greater than {}.".format(name, value, max_)) 192 | 193 | def validate_sequence(value, name=None, size=None): 194 | """ 195 | Validates the specified sequence. 196 | 197 | *value* (**sequence**) is the sequence to validate. 198 | 199 | *name* (``str``) optionally is the name to use when an error occurs. 200 | 201 | *size* (``tuple``) optionally contains: the minimum (``int``) and 202 | maximum (``int``) lengths of *value*. The minimum and maximum lengths 203 | can be ``None`` for no restriction. Default is ``None`` for 204 | ``(None, None)``. 205 | 206 | Raises an ``Exception`` if *value* is not valid. 207 | """ 208 | if not name: 209 | name = 'value' 210 | if size is not None: 211 | min_, max_ = size 212 | else: 213 | min_ = None 214 | max_ = None 215 | 216 | if not is_sequence(value): 217 | raise TypeError("{}:{!r} is not a sequence.".format(name, value)) 218 | 219 | if min_ is not None and len(value) < min_: 220 | raise ValueError("{}:{!r} length {} cannot be less than {}.".format(name, value, len(value), min_)) 221 | elif max_ is not None and len(value) > max_: 222 | raise ValueError("{} length {} cannot be greater than {}.".format(name, len(value), max_)) 223 | 224 | def validate_string(value, name=None, size=None, startswith=None, values=None): 225 | """ 226 | Validates the specified string. 227 | 228 | *value* (**string**) is the string to validate. 229 | 230 | *name* (``str``) optionally is the name to use when an error occurs. 231 | 232 | *size* (``tuple``) optionally contains: the minimum (``int``) and 233 | maximum (``int``) lengths of *value*. The minimum and maximum lengths 234 | can be ``None`` for no restriction. Default is ``None`` for 235 | ``(None, None)``. 236 | 237 | *startswith* (**string**) optionally is the string that *value* must 238 | start with. 239 | 240 | *values* (**container**) optionally contains the set of valid values, 241 | one of which *value* must be. 242 | 243 | Raises an ``Exception`` if *value* is not valid. 244 | """ 245 | if not name: 246 | name = 'value' 247 | if size is not None: 248 | min_, max_ = size 249 | else: 250 | min_ = None 251 | max_ = None 252 | 253 | if not isinstance(value, six.string_types): 254 | raise TypeError("{}:{!r} is not a string.".format(name, value)) 255 | 256 | if min_ is not None and len(value) < min_: 257 | raise ValueError("{}:{!r} length {} cannot be less than {}.".format(name, value, len(value), min_)) 258 | elif max_ is not None and len(value) > max_: 259 | raise ValueError("{} length {} cannot be greater than {}.".format(name, len(value), max_)) 260 | 261 | if startswith is not None and not value.startswith(startswith): 262 | raise ValueError("{}:{!r} does not start with {!r}.".format(name, value, startswith)) 263 | 264 | if values is not None and value not in values: 265 | raise LookupError("{}:{!r} is not {}.".format(name, value, ", ".join(map(repr, values)))) 266 | -------------------------------------------------------------------------------- /dev/twisted.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # pylint: disable=C0103 3 | """ 4 | This module contains helpers to support using the Amazon MWS API from 5 | Twisted. 6 | 7 | .. NOTE:: The ``MWSAgent`` defined in this module is not working with Amazon. 8 | I get an SSL timeout whenever I try using this class to submit a request. 9 | """ 10 | from __future__ import absolute_import 11 | 12 | __created__ = "2012-12-31" 13 | __modified__ = "2013-03-28" 14 | 15 | from cStringIO import StringIO 16 | 17 | import twisted.internet.defer as defer 18 | import twisted.internet.protocol as protocol 19 | import twisted.internet.reactor as reactor 20 | import twisted.internet.threads as threads 21 | import twisted.web.client as client 22 | import twisted.web.http as http 23 | from twisted.web.http_headers import Headers 24 | 25 | import amazonmws.mws 26 | 27 | class MWSAgent(amazonmws.mws.MWSAgent): 28 | """ 29 | The ``MWSAgent`` class is an alternate implementation of the 30 | ``amazonmws.mws.IMWSAgent`` class. This implementation uses threads 31 | for blocking code, and ``twisted.web.client.Agent`` to asynchronously 32 | send requests. 33 | """ 34 | 35 | def __init__(self, pool=None): 36 | """ 37 | Initializes the ``MWSAgent`` instance. 38 | 39 | *pool* (``twisted.web.client.HTTPConnectionPool``) optionally is the 40 | connection pool to use. Default is ``None``. 41 | """ 42 | 43 | self.tx_agent = None 44 | """ 45 | *tx_agent* (``twisted.web.client.Agent``) is the Twisted Agent used 46 | to send requests. 47 | """ 48 | 49 | # Create twisted agent. 50 | self.tx_agent = client.Agent(reactor, pool=pool) 51 | 52 | @defer.inlineCallbacks 53 | def request(self, mws, path, args, body, content_type, verbose=None): 54 | """ 55 | Perform the request. 56 | 57 | *mws* (``MWS``) is the MWS instance. 58 | 59 | *path* (``str``) is the request path. 60 | 61 | *args* contains the query parameters. 62 | 63 | *body* (``str`` or ``file``) contains the body of the request. 64 | 65 | *content_type* (``str``) is the content type of *body*. 66 | 67 | *verbose* (``int``) optionally is whether verbose debugging 68 | information should be printed. Default is ``None`` for ``0``. 69 | 70 | Returns a deferred (``twisted.internet.defer.Deferred``) which is 71 | fired with the response body (``str``) once the request completes. 72 | """ 73 | # Build and send request. 74 | method, url, headers, body = yield threads.deferToThread(self.build_request, mws, path, args, body, content_type, verbose=verbose) 75 | 76 | 77 | print "BODY {} ({})".format(body, body.tell()) 78 | print "-"*20 79 | pos = body.tell() 80 | print body.read() 81 | body.seek(pos, 0) 82 | print "-"*20 83 | 84 | 85 | response = yield self.send_request(method, url, headers, body) 86 | 87 | # Get and return response. 88 | response = Response(response) 89 | yield response.body 90 | defer.returnValue(response) 91 | 92 | 93 | # Get and return response. 94 | d = defer.Deferred() 95 | response.deliverBody(BodyReceiver(d)) 96 | result = yield d 97 | defer.returnValue(result) 98 | 99 | def send_request(self, method, url, headers, body): 100 | """ 101 | Send the request. 102 | 103 | .. NOTE:: This should not be called directly. Use *self.request()*. 104 | 105 | *method* (``str``) is the HTTP request method. 106 | 107 | *url* (``str``) is the URL of the request. 108 | 109 | *headers* (``dict``) are any headers to send. This can be ``None``. 110 | 111 | *body* (``str`` or ``file``) is the body of the request. This can be 112 | ``None``. 113 | 114 | Returns a deferred (``twisted.internet.defer.Deferred``) which is 115 | fired with the response (``twisted.web.iweb.IResponse``) once all 116 | response headers have been received. 117 | """ 118 | body_is_file = callable(getattr(body, 'read', None)) 119 | headers = Headers({key: [val] for key, val in headers.iteritems()}) if headers else None 120 | #producer = FileBodyProducer(body if body_is_file else StringIO(body)) if body is not None else None 121 | producer = XXXStringProducer(body.read() if body_is_file else body) if body is not None else None 122 | return self.tx_agent.request(method, url, headers=headers, bodyProducer=producer) 123 | 124 | 125 | class Response(object): 126 | 127 | def __init__(self, response): 128 | self.version = response.version 129 | self.code = response.code 130 | self.phrase = response.phrase 131 | self.headers = response.headers 132 | self.length = response.length 133 | 134 | self.body = defer.Deferred() 135 | self.body.addCallback(self._body_cb) 136 | response.deliverBody(BodyReceiver(self.body)) 137 | 138 | 139 | def __str__(self): 140 | return str(self.body) 141 | 142 | def _body_cb(self, body): 143 | self.body = body 144 | return body 145 | 146 | 147 | class XXXStringProducer(object): 148 | def __init__(self, body): 149 | self.body = body 150 | self.length = len(body) 151 | 152 | def startProducing(self, consumer): 153 | consumer.write(self.body) 154 | return defer.succeed(None) 155 | 156 | def pauseProducing(self): 157 | pass 158 | 159 | def resumeProducing(self): 160 | pass 161 | 162 | def stopProducing(self): 163 | pass 164 | 165 | 166 | class FileBodyProducer(client.FileBodyProducer): 167 | """ 168 | The ``FileBodyProducer`` class is used to send the request body. This 169 | extends ``twisted.web.client.FileBodyProducer`` so that the file is 170 | not closed. 171 | """ 172 | 173 | def stopProducing(self): 174 | if self._task._completionState is None: 175 | # Only stop when we have not already been stopped. Twisted is 176 | # calling this twice for some odd reason. 177 | self._task.stop() 178 | 179 | 180 | class BodyReceiver(protocol.Protocol): 181 | """ 182 | The ``BodyReceiver`` class is used to buffer the response body and 183 | fire a deferred once the body is fully received. 184 | """ 185 | 186 | def __init__(self, deferred): 187 | """ 188 | Instantiates the ``BodyReceiver`` instance. 189 | 190 | *deferred* (``twisted.internet.defer.Deferred``) will be fired with 191 | the response body (``str``) once it has been fully received. 192 | """ 193 | self.__buffer = StringIO() 194 | self.__deferred = deferred 195 | 196 | def connectionLost(self, reason): 197 | """ 198 | Called when the connection is lost. 199 | 200 | *reason* (``twisted.python.failure.Failure``) is the reason the 201 | connection was lost. 202 | """ 203 | buff, self.__buffer = self.__buffer, None 204 | d, self.__deferred = self.__deferred, None 205 | if reason.check(client.ResponseDone, http.PotentialDataLoss): 206 | d.callback(buff.getvalue()) 207 | else: 208 | d.errback(reason) 209 | 210 | def dataReceived(self, data): 211 | """ 212 | Called when data is received. 213 | 214 | *data* (``str``) is the bytes received. 215 | """ 216 | self.__buffer.write(data) 217 | -------------------------------------------------------------------------------- /git-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git add -A 3 | git commit -am "$1" 4 | -------------------------------------------------------------------------------- /github-upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git push origin master 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from setuptools import find_packages, setup 3 | 4 | from amazonmws import __authors__, __doc__, __license__, __project__, __version__ 5 | 6 | setup( 7 | name=__project__, 8 | version=__version__, 9 | author=", ".join(__authors__), 10 | url="https://github.com/cpburnz/python-amazon-mws.git", 11 | description="Amazon MWS API for Python.", 12 | long_description=__doc__, 13 | classifiers=[ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 2", 19 | "Programming Language :: Python :: 2.7", 20 | "Programming Language :: Python :: 3", 21 | "Topic :: Office/Business", 22 | "Topic :: Software Development :: Libraries :: Python Modules" 23 | ], 24 | license=__license__, 25 | packages=find_packages(), 26 | install_requires=[ 27 | "six" 28 | ], 29 | ) 30 | --------------------------------------------------------------------------------