├── .travis.yml ├── CREDITS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── doc └── pymag.rst ├── magento ├── __init__.py ├── api.py ├── catalog.py ├── checkout.py ├── client.py ├── customer.py ├── directory.py ├── miscellaneous.py ├── sales.py ├── utils.py └── version.py └── setup.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: 6 | - python setup.py install 7 | script: python setup.py test 8 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | CREDITS 2 | ~~~~~~~ 3 | 4 | Openlabs Technologies & Consulting (P) LTD - info (AT) openlabs.co.in 5 | Jim Karsten - iiijjjiii (AT) gmail.com 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TERMS AND CONDITIONS 2 | 3 | 0. Definitions. 4 | 5 | "This License" refers to version 3 of the GNU Affero General Public License. 6 | 7 | "Copyright" also means copyright-like laws that apply to other kinds of 8 | works, such as semiconductor masks. 9 | 10 | "The Program" refers to any copyrightable work licensed under this 11 | License. Each licensee is addressed as "you". "Licensees" and 12 | "recipients" may be individuals or organizations. 13 | 14 | To "modify" a work means to copy from or adapt all or part of the work 15 | in a fashion requiring copyright permission, other than the making of an 16 | exact copy. The resulting work is called a "modified version" of the 17 | earlier work or a work "based on" the earlier work. 18 | 19 | A "covered work" means either the unmodified Program or a work based 20 | on the Program. 21 | 22 | To "propagate" a work means to do anything with it that, without 23 | permission, would make you directly or secondarily liable for 24 | infringement under applicable copyright law, except executing it on a 25 | computer or modifying a private copy. Propagation includes copying, 26 | distribution (with or without modification), making available to the 27 | public, and in some countries other activities as well. 28 | 29 | To "convey" a work means any kind of propagation that enables other 30 | parties to make or receive copies. Mere interaction with a user through 31 | a computer network, with no transfer of a copy, is not conveying. 32 | 33 | An interactive user interface displays "Appropriate Legal Notices" 34 | to the extent that it includes a convenient and prominently visible 35 | feature that (1) displays an appropriate copyright notice, and (2) 36 | tells the user that there is no warranty for the work (except to the 37 | extent that warranties are provided), that licensees may convey the 38 | work under this License, and how to view a copy of this License. If 39 | the interface presents a list of user commands or options, such as a 40 | menu, a prominent item in the list meets this criterion. 41 | 42 | 1. Source Code. 43 | 44 | The "source code" for a work means the preferred form of the work 45 | for making modifications to it. "Object code" means any non-source 46 | form of a work. 47 | 48 | A "Standard Interface" means an interface that either is an official 49 | standard defined by a recognized standards body, or, in the case of 50 | interfaces specified for a particular programming language, one that 51 | is widely used among developers working in that language. 52 | 53 | The "System Libraries" of an executable work include anything, other 54 | than the work as a whole, that (a) is included in the normal form of 55 | packaging a Major Component, but which is not part of that Major 56 | Component, and (b) serves only to enable use of the work with that 57 | Major Component, or to implement a Standard Interface for which an 58 | implementation is available to the public in source code form. A 59 | "Major Component", in this context, means a major essential component 60 | (kernel, window system, and so on) of the specific operating system 61 | (if any) on which the executable work runs, or a compiler used to 62 | produce the work, or an object code interpreter used to run it. 63 | 64 | The "Corresponding Source" for a work in object code form means all 65 | the source code needed to generate, install, and (for an executable 66 | work) run the object code and to modify the work, including scripts to 67 | control those activities. However, it does not include the work's 68 | System Libraries, or general-purpose tools or generally available free 69 | programs which are used unmodified in performing those activities but 70 | which are not part of the work. For example, Corresponding Source 71 | includes interface definition files associated with source files for 72 | the work, and the source code for shared libraries and dynamically 73 | linked subprograms that the work is specifically designed to require, 74 | such as by intimate data communication or control flow between those 75 | subprograms and other parts of the work. 76 | 77 | The Corresponding Source need not include anything that users 78 | can regenerate automatically from other parts of the Corresponding 79 | Source. 80 | 81 | The Corresponding Source for a work in source code form is that 82 | same work. 83 | 84 | 2. Basic Permissions. 85 | 86 | All rights granted under this License are granted for the term of 87 | copyright on the Program, and are irrevocable provided the stated 88 | conditions are met. This License explicitly affirms your unlimited 89 | permission to run the unmodified Program. The output from running a 90 | covered work is covered by this License only if the output, given its 91 | content, constitutes a covered work. This License acknowledges your 92 | rights of fair use or other equivalent, as provided by copyright law. 93 | 94 | You may make, run and propagate covered works that you do not 95 | convey, without conditions so long as your license otherwise remains 96 | in force. You may convey covered works to others for the sole purpose 97 | of having them make modifications exclusively for you, or provide you 98 | with facilities for running those works, provided that you comply with 99 | the terms of this License in conveying all material for which you do 100 | not control copyright. Those thus making or running the covered works 101 | for you must do so exclusively on your behalf, under your direction 102 | and control, on terms that prohibit them from making any copies of 103 | your copyrighted material outside their relationship with you. 104 | 105 | Conveying under any other circumstances is permitted solely under 106 | the conditions stated below. Sublicensing is not allowed; section 10 107 | makes it unnecessary. 108 | 109 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 110 | 111 | No covered work shall be deemed part of an effective technological 112 | measure under any applicable law fulfilling obligations under article 113 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 114 | similar laws prohibiting or restricting circumvention of such 115 | measures. 116 | 117 | When you convey a covered work, you waive any legal power to forbid 118 | circumvention of technological measures to the extent such circumvention 119 | is effected by exercising rights under this License with respect to 120 | the covered work, and you disclaim any intention to limit operation or 121 | modification of the work as a means of enforcing, against the work's 122 | users, your or third parties' legal rights to forbid circumvention of 123 | technological measures. 124 | 125 | 4. Conveying Verbatim Copies. 126 | 127 | You may convey verbatim copies of the Program's source code as you 128 | receive it, in any medium, provided that you conspicuously and 129 | appropriately publish on each copy an appropriate copyright notice; 130 | keep intact all notices stating that this License and any 131 | non-permissive terms added in accord with section 7 apply to the code; 132 | keep intact all notices of the absence of any warranty; and give all 133 | recipients a copy of this License along with the Program. 134 | 135 | You may charge any price or no price for each copy that you convey, 136 | and you may offer support or warranty protection for a fee. 137 | 138 | 5. Conveying Modified Source Versions. 139 | 140 | You may convey a work based on the Program, or the modifications to 141 | produce it from the Program, in the form of source code under the 142 | terms of section 4, provided that you also meet all of these conditions: 143 | 144 | a) The work must carry prominent notices stating that you modified 145 | it, and giving a relevant date. 146 | 147 | b) The work must carry prominent notices stating that it is 148 | released under this License and any conditions added under section 149 | 7. This requirement modifies the requirement in section 4 to 150 | "keep intact all notices". 151 | 152 | c) You must license the entire work, as a whole, under this 153 | License to anyone who comes into possession of a copy. This 154 | License will therefore apply, along with any applicable section 7 155 | additional terms, to the whole of the work, and all its parts, 156 | regardless of how they are packaged. This License gives no 157 | permission to license the work in any other way, but it does not 158 | invalidate such permission if you have separately received it. 159 | 160 | d) If the work has interactive user interfaces, each must display 161 | Appropriate Legal Notices; however, if the Program has interactive 162 | interfaces that do not display Appropriate Legal Notices, your 163 | work need not make them do so. 164 | 165 | A compilation of a covered work with other separate and independent 166 | works, which are not by their nature extensions of the covered work, 167 | and which are not combined with it such as to form a larger program, 168 | in or on a volume of a storage or distribution medium, is called an 169 | "aggregate" if the compilation and its resulting copyright are not 170 | used to limit the access or legal rights of the compilation's users 171 | beyond what the individual works permit. Inclusion of a covered work 172 | in an aggregate does not cause this License to apply to the other 173 | parts of the aggregate. 174 | 175 | 6. Conveying Non-Source Forms. 176 | 177 | You may convey a covered work in object code form under the terms 178 | of sections 4 and 5, provided that you also convey the 179 | machine-readable Corresponding Source under the terms of this License, 180 | in one of these ways: 181 | 182 | a) Convey the object code in, or embodied in, a physical product 183 | (including a physical distribution medium), accompanied by the 184 | Corresponding Source fixed on a durable physical medium 185 | customarily used for software interchange. 186 | 187 | b) Convey the object code in, or embodied in, a physical product 188 | (including a physical distribution medium), accompanied by a 189 | written offer, valid for at least three years and valid for as 190 | long as you offer spare parts or customer support for that product 191 | model, to give anyone who possesses the object code either (1) a 192 | copy of the Corresponding Source for all the software in the 193 | product that is covered by this License, on a durable physical 194 | medium customarily used for software interchange, for a price no 195 | more than your reasonable cost of physically performing this 196 | conveying of source, or (2) access to copy the 197 | Corresponding Source from a network server at no charge. 198 | 199 | c) Convey individual copies of the object code with a copy of the 200 | written offer to provide the Corresponding Source. This 201 | alternative is allowed only occasionally and noncommercially, and 202 | only if you received the object code with such an offer, in accord 203 | with subsection 6b. 204 | 205 | d) Convey the object code by offering access from a designated 206 | place (gratis or for a charge), and offer equivalent access to the 207 | Corresponding Source in the same way through the same place at no 208 | further charge. You need not require recipients to copy the 209 | Corresponding Source along with the object code. If the place to 210 | copy the object code is a network server, the Corresponding Source 211 | may be on a different server (operated by you or a third party) 212 | that supports equivalent copying facilities, provided you maintain 213 | clear directions next to the object code saying where to find the 214 | Corresponding Source. Regardless of what server hosts the 215 | Corresponding Source, you remain obligated to ensure that it is 216 | available for as long as needed to satisfy these requirements. 217 | 218 | e) Convey the object code using peer-to-peer transmission, provided 219 | you inform other peers where the object code and Corresponding 220 | Source of the work are being offered to the general public at no 221 | charge under subsection 6d. 222 | 223 | A separable portion of the object code, whose source code is excluded 224 | from the Corresponding Source as a System Library, need not be 225 | included in conveying the object code work. 226 | 227 | A "User Product" is either (1) a "consumer product", which means any 228 | tangible personal property which is normally used for personal, family, 229 | or household purposes, or (2) anything designed or sold for incorporation 230 | into a dwelling. In determining whether a product is a consumer product, 231 | doubtful cases shall be resolved in favor of coverage. For a particular 232 | product received by a particular user, "normally used" refers to a 233 | typical or common use of that class of product, regardless of the status 234 | of the particular user or of the way in which the particular user 235 | actually uses, or expects or is expected to use, the product. A product 236 | is a consumer product regardless of whether the product has substantial 237 | commercial, industrial or non-consumer uses, unless such uses represent 238 | the only significant mode of use of the product. 239 | 240 | "Installation Information" for a User Product means any methods, 241 | procedures, authorization keys, or other information required to install 242 | and execute modified versions of a covered work in that User Product from 243 | a modified version of its Corresponding Source. The information must 244 | suffice to ensure that the continued functioning of the modified object 245 | code is in no case prevented or interfered with solely because 246 | modification has been made. 247 | 248 | If you convey an object code work under this section in, or with, or 249 | specifically for use in, a User Product, and the conveying occurs as 250 | part of a transaction in which the right of possession and use of the 251 | User Product is transferred to the recipient in perpetuity or for a 252 | fixed term (regardless of how the transaction is characterized), the 253 | Corresponding Source conveyed under this section must be accompanied 254 | by the Installation Information. But this requirement does not apply 255 | if neither you nor any third party retains the ability to install 256 | modified object code on the User Product (for example, the work has 257 | been installed in ROM). 258 | 259 | The requirement to provide Installation Information does not include a 260 | requirement to continue to provide support service, warranty, or updates 261 | for a work that has been modified or installed by the recipient, or for 262 | the User Product in which it has been modified or installed. Access to a 263 | network may be denied when the modification itself materially and 264 | adversely affects the operation of the network or violates the rules and 265 | protocols for communication across the network. 266 | 267 | Corresponding Source conveyed, and Installation Information provided, 268 | in accord with this section must be in a format that is publicly 269 | documented (and with an implementation available to the public in 270 | source code form), and must require no special password or key for 271 | unpacking, reading or copying. 272 | 273 | 7. Additional Terms. 274 | 275 | "Additional permissions" are terms that supplement the terms of this 276 | License by making exceptions from one or more of its conditions. 277 | Additional permissions that are applicable to the entire Program shall 278 | be treated as though they were included in this License, to the extent 279 | that they are valid under applicable law. If additional permissions 280 | apply only to part of the Program, that part may be used separately 281 | under those permissions, but the entire Program remains governed by 282 | this License without regard to the additional permissions. 283 | 284 | When you convey a copy of a covered work, you may at your option 285 | remove any additional permissions from that copy, or from any part of 286 | it. (Additional permissions may be written to require their own 287 | removal in certain cases when you modify the work.) You may place 288 | additional permissions on material, added by you to a covered work, 289 | for which you have or can give appropriate copyright permission. 290 | 291 | Notwithstanding any other provision of this License, for material you 292 | add to a covered work, you may (if authorized by the copyright holders of 293 | that material) supplement the terms of this License with terms: 294 | 295 | a) Disclaiming warranty or limiting liability differently from the 296 | terms of sections 15 and 16 of this License; or 297 | 298 | b) Requiring preservation of specified reasonable legal notices or 299 | author attributions in that material or in the Appropriate Legal 300 | Notices displayed by works containing it; or 301 | 302 | c) Prohibiting misrepresentation of the origin of that material, or 303 | requiring that modified versions of such material be marked in 304 | reasonable ways as different from the original version; or 305 | 306 | d) Limiting the use for publicity purposes of names of licensors or 307 | authors of the material; or 308 | 309 | e) Declining to grant rights under trademark law for use of some 310 | trade names, trademarks, or service marks; or 311 | 312 | f) Requiring indemnification of licensors and authors of that 313 | material by anyone who conveys the material (or modified versions of 314 | it) with contractual assumptions of liability to the recipient, for 315 | any liability that these contractual assumptions directly impose on 316 | those licensors and authors. 317 | 318 | All other non-permissive additional terms are considered "further 319 | restrictions" within the meaning of section 10. If the Program as you 320 | received it, or any part of it, contains a notice stating that it is 321 | governed by this License along with a term that is a further 322 | restriction, you may remove that term. If a license document contains 323 | a further restriction but permits relicensing or conveying under this 324 | License, you may add to a covered work material governed by the terms 325 | of that license document, provided that the further restriction does 326 | not survive such relicensing or conveying. 327 | 328 | If you add terms to a covered work in accord with this section, you 329 | must place, in the relevant source files, a statement of the 330 | additional terms that apply to those files, or a notice indicating 331 | where to find the applicable terms. 332 | 333 | Additional terms, permissive or non-permissive, may be stated in the 334 | form of a separately written license, or stated as exceptions; 335 | the above requirements apply either way. 336 | 337 | 8. Termination. 338 | 339 | You may not propagate or modify a covered work except as expressly 340 | provided under this License. Any attempt otherwise to propagate or 341 | modify it is void, and will automatically terminate your rights under 342 | this License (including any patent licenses granted under the third 343 | paragraph of section 11). 344 | 345 | However, if you cease all violation of this License, then your 346 | license from a particular copyright holder is reinstated (a) 347 | provisionally, unless and until the copyright holder explicitly and 348 | finally terminates your license, and (b) permanently, if the copyright 349 | holder fails to notify you of the violation by some reasonable means 350 | prior to 60 days after the cessation. 351 | 352 | Moreover, your license from a particular copyright holder is 353 | reinstated permanently if the copyright holder notifies you of the 354 | violation by some reasonable means, this is the first time you have 355 | received notice of violation of this License (for any work) from that 356 | copyright holder, and you cure the violation prior to 30 days after 357 | your receipt of the notice. 358 | 359 | Termination of your rights under this section does not terminate the 360 | licenses of parties who have received copies or rights from you under 361 | this License. If your rights have been terminated and not permanently 362 | reinstated, you do not qualify to receive new licenses for the same 363 | material under section 10. 364 | 365 | 9. Acceptance Not Required for Having Copies. 366 | 367 | You are not required to accept this License in order to receive or 368 | run a copy of the Program. Ancillary propagation of a covered work 369 | occurring solely as a consequence of using peer-to-peer transmission 370 | to receive a copy likewise does not require acceptance. However, 371 | nothing other than this License grants you permission to propagate or 372 | modify any covered work. These actions infringe copyright if you do 373 | not accept this License. Therefore, by modifying or propagating a 374 | covered work, you indicate your acceptance of this License to do so. 375 | 376 | 10. Automatic Licensing of Downstream Recipients. 377 | 378 | Each time you convey a covered work, the recipient automatically 379 | receives a license from the original licensors, to run, modify and 380 | propagate that work, subject to this License. You are not responsible 381 | for enforcing compliance by third parties with this License. 382 | 383 | An "entity transaction" is a transaction transferring control of an 384 | organization, or substantially all assets of one, or subdividing an 385 | organization, or merging organizations. If propagation of a covered 386 | work results from an entity transaction, each party to that 387 | transaction who receives a copy of the work also receives whatever 388 | licenses to the work the party's predecessor in interest had or could 389 | give under the previous paragraph, plus a right to possession of the 390 | Corresponding Source of the work from the predecessor in interest, if 391 | the predecessor has it or can get it with reasonable efforts. 392 | 393 | You may not impose any further restrictions on the exercise of the 394 | rights granted or affirmed under this License. For example, you may 395 | not impose a license fee, royalty, or other charge for exercise of 396 | rights granted under this License, and you may not initiate litigation 397 | (including a cross-claim or counterclaim in a lawsuit) alleging that 398 | any patent claim is infringed by making, using, selling, offering for 399 | sale, or importing the Program or any portion of it. 400 | 401 | 11. Patents. 402 | 403 | A "contributor" is a copyright holder who authorizes use under this 404 | License of the Program or a work on which the Program is based. The 405 | work thus licensed is called the contributor's "contributor version". 406 | 407 | A contributor's "essential patent claims" are all patent claims 408 | owned or controlled by the contributor, whether already acquired or 409 | hereafter acquired, that would be infringed by some manner, permitted 410 | by this License, of making, using, or selling its contributor version, 411 | but do not include claims that would be infringed only as a 412 | consequence of further modification of the contributor version. For 413 | purposes of this definition, "control" includes the right to grant 414 | patent sublicenses in a manner consistent with the requirements of 415 | this License. 416 | 417 | Each contributor grants you a non-exclusive, worldwide, royalty-free 418 | patent license under the contributor's essential patent claims, to 419 | make, use, sell, offer for sale, import and otherwise run, modify and 420 | propagate the contents of its contributor version. 421 | 422 | In the following three paragraphs, a "patent license" is any express 423 | agreement or commitment, however denominated, not to enforce a patent 424 | (such as an express permission to practice a patent or covenant not to 425 | sue for patent infringement). To "grant" such a patent license to a 426 | party means to make such an agreement or commitment not to enforce a 427 | patent against the party. 428 | 429 | If you convey a covered work, knowingly relying on a patent license, 430 | and the Corresponding Source of the work is not available for anyone 431 | to copy, free of charge and under the terms of this License, through a 432 | publicly available network server or other readily accessible means, 433 | then you must either (1) cause the Corresponding Source to be so 434 | available, or (2) arrange to deprive yourself of the benefit of the 435 | patent license for this particular work, or (3) arrange, in a manner 436 | consistent with the requirements of this License, to extend the patent 437 | license to downstream recipients. "Knowingly relying" means you have 438 | actual knowledge that, but for the patent license, your conveying the 439 | covered work in a country, or your recipient's use of the covered work 440 | in a country, would infringe one or more identifiable patents in that 441 | country that you have reason to believe are valid. 442 | 443 | If, pursuant to or in connection with a single transaction or 444 | arrangement, you convey, or propagate by procuring conveyance of, a 445 | covered work, and grant a patent license to some of the parties 446 | receiving the covered work authorizing them to use, propagate, modify 447 | or convey a specific copy of the covered work, then the patent license 448 | you grant is automatically extended to all recipients of the covered 449 | work and works based on it. 450 | 451 | A patent license is "discriminatory" if it does not include within 452 | the scope of its coverage, prohibits the exercise of, or is 453 | conditioned on the non-exercise of one or more of the rights that are 454 | specifically granted under this License. You may not convey a covered 455 | work if you are a party to an arrangement with a third party that is 456 | in the business of distributing software, under which you make payment 457 | to the third party based on the extent of your activity of conveying 458 | the work, and under which the third party grants, to any of the 459 | parties who would receive the covered work from you, a discriminatory 460 | patent license (a) in connection with copies of the covered work 461 | conveyed by you (or copies made from those copies), or (b) primarily 462 | for and in connection with specific products or compilations that 463 | contain the covered work, unless you entered into that arrangement, 464 | or that patent license was granted, prior to 28 March 2007. 465 | 466 | Nothing in this License shall be construed as excluding or limiting 467 | any implied license or other defenses to infringement that may 468 | otherwise be available to you under applicable patent law. 469 | 470 | 12. No Surrender of Others' Freedom. 471 | 472 | If conditions are imposed on you (whether by court order, agreement or 473 | otherwise) that contradict the conditions of this License, they do not 474 | excuse you from the conditions of this License. If you cannot convey a 475 | covered work so as to satisfy simultaneously your obligations under this 476 | License and any other pertinent obligations, then as a consequence you may 477 | not convey it at all. For example, if you agree to terms that obligate you 478 | to collect a royalty for further conveying from those to whom you convey 479 | the Program, the only way you could satisfy both those terms and this 480 | License would be to refrain entirely from conveying the Program. 481 | 482 | 13. Remote Network Interaction; Use with the GNU General Public License. 483 | 484 | Notwithstanding any other provision of this License, if you modify the 485 | Program, your modified version must prominently offer all users 486 | interacting with it remotely through a computer network (if your version 487 | supports such interaction) an opportunity to receive the Corresponding 488 | Source of your version by providing access to the Corresponding Source 489 | from a network server at no charge, through some standard or customary 490 | means of facilitating copying of software. This Corresponding Source 491 | shall include the Corresponding Source for any work covered by version 3 492 | of the GNU General Public License that is incorporated pursuant to the 493 | following paragraph. 494 | 495 | Notwithstanding any other provision of this License, you have 496 | permission to link or combine any covered work with a work licensed 497 | under version 3 of the GNU General Public License into a single 498 | combined work, and to convey the resulting work. The terms of this 499 | License will continue to apply to the part which is the covered work, 500 | but the work with which it is combined will remain governed by version 501 | 3 of the GNU General Public License. 502 | 503 | 14. Revised Versions of this License. 504 | 505 | The Free Software Foundation may publish revised and/or new versions of 506 | the GNU Affero General Public License from time to time. Such new versions 507 | will be similar in spirit to the present version, but may differ in detail to 508 | address new problems or concerns. 509 | 510 | Each version is given a distinguishing version number. If the 511 | Program specifies that a certain numbered version of the GNU Affero General 512 | Public License "or any later version" applies to it, you have the 513 | option of following the terms and conditions either of that numbered 514 | version or of any later version published by the Free Software 515 | Foundation. If the Program does not specify a version number of the 516 | GNU Affero General Public License, you may choose any version ever published 517 | by the Free Software Foundation. 518 | 519 | If the Program specifies that a proxy can decide which future 520 | versions of the GNU Affero General Public License can be used, that proxy's 521 | public statement of acceptance of a version permanently authorizes you 522 | to choose that version for the Program. 523 | 524 | Later license versions may give you additional or different 525 | permissions. However, no additional obligations are imposed on any 526 | author or copyright holder as a result of your choosing to follow a 527 | later version. 528 | 529 | 15. Disclaimer of Warranty. 530 | 531 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 532 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 533 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 534 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 535 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 536 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 537 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 538 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 539 | 540 | 16. Limitation of Liability. 541 | 542 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 543 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 544 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 545 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 546 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 547 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 548 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 549 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 550 | SUCH DAMAGES. 551 | 552 | 17. Interpretation of Sections 15 and 16. 553 | 554 | If the disclaimer of warranty and limitation of liability provided 555 | above cannot be given local legal effect according to their terms, 556 | reviewing courts shall apply local law that most closely approximates 557 | an absolute waiver of all civil liability in connection with the 558 | Program, unless a warranty or assumption of liability accompanies a 559 | copy of the Program in return for a fee. 560 | 561 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Magento Python API 2 | ================== 3 | 4 | Python library to connect to Magento Webservices. 5 | 6 | Check documentation source code 7 | 8 | Usage 9 | ----- 10 | 11 | .. code-block:: python 12 | 13 | import magento 14 | 15 | url = 'http://domain.com/' 16 | apiuser = 'user' 17 | apipass = 'password' 18 | 19 | with magento.Product(url, apiuser, apipass) as product_api: 20 | order_filter = {'created_at':{'from':'2011-09-15 00:00:00'}} 21 | products = product_api.list(order_filter) 22 | 23 | with magento.ProductTypes(url, apiuser, apipass) as product_type_api: 24 | product_type = product_type_api.list() 25 | 26 | with magento.Product(url, apiuser, apipass) as product_api: 27 | sku = 'prod1' 28 | product = product_api.info(sku) 29 | 30 | with magento.API(url, apiuser, apipass) as magento_api: 31 | # Calling custom APIs if you have extension modules on your 32 | # magento installation 33 | websites = magento_api.call('ol_websites.list', []) 34 | store_group = magento_api.call('ol_groups.list', []) 35 | store_views = magento_api.call('ol_storeviews.list', []) 36 | 37 | with magento.Order(url, apiuser, apipass) as order_api: 38 | order_increment_id = '100000001 ' 39 | status = 'canceled' 40 | order_api.addcomment(order_increment_id, status) 41 | 42 | with magento.Store(url, apiuser, apipass) as store_api: 43 | store_id = '1' 44 | store_view_info = store_api.info(store_id) 45 | store_views = store_api.list() 46 | 47 | with magento.Magento(url, apiuser, apipass) as magento_api: 48 | magento_info = magento_api.info() 49 | 50 | 51 | License 52 | ------- 53 | 54 | GNU Affero General Public License version 3 55 | 56 | See LICENSE for more details 57 | -------------------------------------------------------------------------------- /doc/pymag.rst: -------------------------------------------------------------------------------- 1 | Magento API 2 | *********** 3 | 4 | Magento Module 5 | -------------- 6 | 7 | .. automodule:: magento 8 | 9 | API 10 | --- 11 | 12 | .. automodule:: api 13 | 14 | .. autoclass:: API 15 | 16 | .. automethod:: __init__ 17 | .. automethod:: connect 18 | .. automethod:: __enter__ 19 | .. automethod:: __exit__ 20 | .. automethod:: call 21 | .. automethod:: multiCall 22 | 23 | Catalog 24 | ------- 25 | 26 | .. automodule:: catalog 27 | 28 | .. autoclass:: Category 29 | 30 | .. automethod:: currentStore 31 | .. automethod:: tree 32 | .. automethod:: level 33 | .. automethod:: info 34 | .. automethod:: create 35 | .. automethod:: update 36 | .. automethod:: move 37 | .. automethod:: delete 38 | .. automethod:: assignedproducts 39 | .. automethod:: assignproduct 40 | .. automethod:: updateproduct 41 | .. automethod:: removeproduct 42 | 43 | 44 | .. autoclass:: CategoryAttribute 45 | 46 | .. automethod:: currentStore 47 | .. automethod:: options 48 | .. automethod:: list 49 | 50 | 51 | 52 | .. autoclass:: Product 53 | 54 | .. automethod:: currentStore 55 | .. automethod:: list 56 | .. automethod:: info 57 | .. automethod:: create 58 | .. automethod:: update 59 | .. automethod:: setSpecialPrice 60 | .. automethod:: getSpecialPrice 61 | .. automethod:: delete 62 | 63 | .. autoclass:: ProductAttribute 64 | 65 | .. autoclass:: ProductAttributeSet 66 | 67 | .. autoclass:: ProductTypes 68 | 69 | .. autoclass:: ProductImages 70 | 71 | .. autoclass:: ProductTierPrice 72 | 73 | .. autoclass:: ProductLinks 74 | 75 | .. autoclass:: Inventory 76 | 77 | Customer 78 | -------- 79 | 80 | .. automodule:: customer 81 | 82 | .. autoclass:: Customer 83 | 84 | .. automethod:: list 85 | .. automethod:: create 86 | .. automethod:: info 87 | .. automethod:: update 88 | .. automethod:: delete 89 | 90 | .. autoclass:: CustomerGroup 91 | 92 | .. automethod:: list 93 | 94 | .. autoclass:: CustomerAddress 95 | 96 | .. automethod:: list 97 | .. automethod:: create 98 | .. automethod:: info 99 | .. automethod:: update 100 | .. automethod:: delete 101 | 102 | Directory 103 | --------- 104 | 105 | .. automodule:: directory 106 | 107 | .. autoclass:: Country 108 | 109 | .. automethod:: list 110 | 111 | .. autoclass:: Region 112 | 113 | .. automethod:: list 114 | 115 | Utils 116 | ----- 117 | 118 | .. automodule:: utils 119 | 120 | .. autofunction:: expand_url 121 | 122 | .. automodule:: custom_api 123 | -------------------------------------------------------------------------------- /magento/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento API 4 | 5 | :copyright: (c) 2010 by Sharoon Thomas. 6 | :copyright: (c) 2010-2013 by Openlabs Technologies & Consulting (P) LTD 7 | :license: AGPLv3, see LICENSE for more details 8 | ''' 9 | __all__ = [ 10 | 'API', 'Store', 'Magento', 11 | 'Customer', 'CustomerGroup', 'CustomerAddress', 12 | 'Country', 'Region', 13 | 'Cart', 'CartCoupon', 'CartCustomer', 14 | 'CartPayment', 'CartProduct', 'CartShipping', 15 | 16 | 'Category', 'CategoryAttribute', 'Product', 'ProductAttribute', 17 | 'ProductAttributeSet', 'ProductTypes', 'ProductImages', 18 | 'ProductTierPrice', 'ProductLinks', 'ProductConfigurable', 19 | 'Inventory', 'Order', 'Shipment', 'Invoice', '__version__', 20 | 'Client', 21 | ] 22 | 23 | from .api import API 24 | from .client import Client 25 | from .checkout import Cart, CartCoupon, CartCustomer 26 | from .checkout import CartPayment, CartProduct, CartShipping 27 | from .miscellaneous import Store, Magento 28 | from .customer import Customer, CustomerGroup, CustomerAddress 29 | from .directory import Country, Region 30 | from .catalog import Category, CategoryAttribute 31 | from .catalog import Product, ProductAttribute, ProductAttributeSet 32 | from .catalog import ProductTypes, ProductImages, ProductTierPrice 33 | from .catalog import ProductLinks, ProductConfigurable, Inventory 34 | from .sales import Order, Shipment, Invoice 35 | from .version import VERSION as __version__ 36 | -------------------------------------------------------------------------------- /magento/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.api 4 | 5 | Generic API for magento 6 | 7 | :copyright: (c) 2010 by Sharoon Thomas. 8 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD 9 | :license: AGPLv3, see LICENSE for more details 10 | ''' 11 | 12 | PROTOCOLS = [] 13 | try: 14 | from xmlrpclib import ServerProxy 15 | except ImportError: 16 | pass 17 | else: 18 | PROTOCOLS.append('xmlrpc') 19 | 20 | try: 21 | from suds.client import Client 22 | except ImportError: 23 | pass 24 | else: 25 | PROTOCOLS.append('soap') 26 | 27 | from magento.utils import expand_url 28 | 29 | 30 | class API(object): 31 | """ 32 | Generic API to connect to magento 33 | """ 34 | 35 | def __init__(self, url, username, password, 36 | version='1.3.2.4', full_url=False, protocol='xmlrpc', transport=None): 37 | """ 38 | This is the Base API class which other APIs have to subclass. By 39 | default the inherited classes also get the properties of this 40 | class which will allow the use of the API with the `with` statement 41 | 42 | A typical example to extend the API for your subclass is given below:: 43 | 44 | from magento.api import API 45 | 46 | class Core(API): 47 | 48 | def websites(self): 49 | return self.call('ol_websites.list', []) 50 | 51 | def stores(self): 52 | return self.call('ol_groups.list', []) 53 | 54 | def store_views(self): 55 | return self.call('ol_storeviews.list', []) 56 | 57 | The above real life example extends the API for the custom API 58 | implementation for the magento extension 59 | 60 | magento-community/Openlabs_OpenERPConnector 61 | 62 | Example usage :: 63 | 64 | from magento.api import API 65 | 66 | with API(url, username, password) as magento_api: 67 | return magento_api.call('customer.list', []) 68 | 69 | .. note:: Python with statement has to be imported from __future__ 70 | in older versions of python. *from __future__ import with_statement* 71 | 72 | If you want to use the API as a normal class, then you have to manually 73 | end the session. A typical example is below:: 74 | 75 | from magento.api import API 76 | 77 | api = API(url, username, password) 78 | api.connect() 79 | try: 80 | return api.call('customer.list', []) 81 | finally: 82 | api.client.endSession(api.session) 83 | 84 | :param url: URL to the magento instance. 85 | By default the URL is treated as a base url 86 | of the domain to which the api part of the URL 87 | is added. If you want to specify the complete 88 | URL, set the full_url flag as True. 89 | :param username: API username of the Web services user. Note 90 | that this is NOT magento admin username 91 | :param password: API password of the Web services user. 92 | :param version: The version of magento the connection is being made to. 93 | It is recommended to specify this as there could be 94 | API specific changes in certain calls. Default value is 95 | 1.3.2.4 96 | :param full_url: If set to true, then the `url` is expected to 97 | be a complete URL 98 | :param protocol: 'xmlrpc' and 'soap' are valid values 99 | :param transport: optional xmlrpclib.Transport subclass for 100 | use in xmlrpc requests 101 | """ 102 | assert protocol \ 103 | in PROTOCOLS, "protocol must be %s" % ' OR '.join(PROTOCOLS) 104 | self.url = str(full_url and url or expand_url(url, protocol)) 105 | self.username = username 106 | self.password = password 107 | self.protocol = protocol 108 | self.version = version 109 | self.transport = transport 110 | self.session = None 111 | self.client = None 112 | 113 | def connect(self): 114 | """ 115 | Connects to the service 116 | but does not login. This could be used as a connection test 117 | """ 118 | if self.protocol == 'xmlrpc': 119 | if self.transport: 120 | self.client = ServerProxy( 121 | self.url, allow_none=True, transport=self.transport) 122 | else: 123 | self.client = ServerProxy(self.url, allow_none=True) 124 | else: 125 | self.client = Client(self.url) 126 | 127 | def __enter__(self): 128 | """ 129 | Entry point for with statement 130 | Logs in and creates a session 131 | """ 132 | if self.client is None: 133 | self.connect() 134 | if self.protocol == 'xmlrpc': 135 | self.session = self.client.login( 136 | self.username, self.password) 137 | else: 138 | self.session = self.client.service.login( 139 | self.username, self.password) 140 | return self 141 | 142 | def __exit__(self, type, value, traceback): 143 | """ 144 | Exit point 145 | 146 | Closes session with magento 147 | """ 148 | if self.protocol == 'xmlrpc': 149 | self.client.endSession(self.session) 150 | else: 151 | self.client.service.endSession(self.session) 152 | self.session = None 153 | 154 | def call(self, resource_path, arguments): 155 | """ 156 | Proxy for SOAP call API 157 | """ 158 | if self.protocol == 'xmlrpc': 159 | return self.client.call(self.session, resource_path, arguments) 160 | else: 161 | return self.client.service.call( 162 | self.session, resource_path, arguments) 163 | 164 | def multiCall(self, calls): 165 | """ 166 | Proxy for multicalls 167 | """ 168 | if self.protocol == 'xmlrpc': 169 | return self.client.multiCall(self.session, calls) 170 | else: 171 | return self.client.service.multiCall(self.session, calls) 172 | 173 | -------------------------------------------------------------------------------- /magento/catalog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.catalog 4 | 5 | Product Catalog API for magento 6 | 7 | :copyright: (c) 2010 by Sharoon Thomas. 8 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD 9 | :license: AGPLv3, see LICENSE for more details 10 | ''' 11 | 12 | import warnings 13 | 14 | from magento.api import API 15 | 16 | 17 | class Category(API): 18 | """ 19 | Product Category API 20 | """ 21 | __slots__ = () 22 | 23 | def currentStore(self, store_view=None): 24 | """ 25 | Set/Get current store view 26 | 27 | :param store_view: Store view ID or Code 28 | :return: int 29 | """ 30 | args = [store_view] if store_view else [] 31 | return int(self.call('catalog_category.currentStore', args)) 32 | 33 | def tree(self, parent_id=None, store_view=None): 34 | """ 35 | Retrieve hierarchical tree of categories. 36 | 37 | :param parent_id: Integer ID of parent category (optional) 38 | :param store_view: Store View (optional) 39 | :return: dictionary of values 40 | """ 41 | return self.call('catalog_category.tree', [parent_id, store_view]) 42 | 43 | def level(self, website=None, store_view=None, parent_category=None): 44 | """ 45 | Retrieve one level of categories by website/store view/parent category 46 | 47 | :param website: Website code or ID 48 | :param store_view: storeview code or ID 49 | :param parent_category: Parent Category ID 50 | :return: Dictionary 51 | """ 52 | return self.call( 53 | 'catalog_category.level', [website, store_view, parent_category] 54 | ) 55 | 56 | def info(self, category_id, store_view=None, attributes=None): 57 | """ 58 | Retrieve Category details 59 | 60 | :param category_id: ID of category to retrieve 61 | :param store_view: Store view ID or code 62 | :param attributes: Return the fields specified 63 | :return: Dictionary of data 64 | """ 65 | return self.call( 66 | 'catalog_category.info', [category_id, store_view, attributes] 67 | ) 68 | 69 | def create(self, parent_id, data, store_view=None): 70 | """ 71 | Create new category and return its ID 72 | 73 | :param parent_id: ID of parent 74 | :param data: Data for category 75 | :param store_view: Store view ID or Code 76 | :return: Integer ID 77 | """ 78 | return int(self.call( 79 | 'catalog_category.create', [parent_id, data, store_view]) 80 | ) 81 | 82 | def update(self, category_id, data, store_view=None): 83 | """ 84 | Update Category 85 | 86 | :param category_id: ID of category 87 | :param data: Category Data 88 | :param store_view: Store view ID or code 89 | :return: Boolean 90 | """ 91 | return bool( 92 | self.call( 93 | 'catalog_category.update', [category_id, data, store_view] 94 | ) 95 | ) 96 | 97 | def move(self, category_id, parent_id, after_id=None): 98 | """ 99 | Move category in tree 100 | 101 | :param category_id: ID of category to move 102 | :param parent_id: New parent of the category 103 | :param after_id: Category ID after what position it will be moved 104 | :return: Boolean 105 | """ 106 | return bool(self.call( 107 | 'catalog_category.move', [category_id, parent_id, after_id]) 108 | ) 109 | 110 | def delete(self, category_id): 111 | """ 112 | Delete category 113 | 114 | :param category_id: ID of category 115 | :return: Boolean 116 | """ 117 | return bool(self.call('catalog_category.delete', [category_id])) 118 | 119 | def assignedproducts(self, category_id, store): 120 | """ 121 | Retrieve list of assigned products 122 | 123 | :param category_id: Category ID 124 | :param store: Store ID or Code 125 | 126 | :return: Dictionary 127 | """ 128 | return self.call( 129 | 'catalog_category.assignedProducts', [category_id, store] 130 | ) 131 | 132 | #: A proxy for :meth:`assignedproducts` 133 | assigned_products = assignedproducts 134 | 135 | def assignproduct(self, category_id, product, position=None): 136 | """ 137 | Assign product to a category 138 | 139 | :param category_id: ID of a category 140 | :param product: ID or Code of the product 141 | :param position: Position of product in category 142 | 143 | :return: boolean 144 | """ 145 | return bool(self.call( 146 | 'catalog_category.assignProduct', [category_id, product, position]) 147 | ) 148 | 149 | #: A proxy for :meth:`assignproduct` 150 | assign_product = assignproduct 151 | 152 | def updateproduct(self, category_id, product, position=None): 153 | """ 154 | Update assigned product 155 | 156 | :param category_id: ID of a category 157 | :param product: ID or Code of the product 158 | :param position: Position of product in category 159 | 160 | :return: boolean 161 | """ 162 | return bool(self.call( 163 | 'catalog_category.updateProduct', [category_id, product, position]) 164 | ) 165 | 166 | #: A proxy for :meth:`updateproduct` 167 | update_product = updateproduct 168 | 169 | def removeproduct(self, category_id, product): 170 | """ 171 | Remove product from category 172 | 173 | :param category_id: ID of a category 174 | :param product: ID or Code of the product 175 | 176 | :return: boolean 177 | """ 178 | return bool(self.call( 179 | 'catalog_category.removeProduct', [category_id, product]) 180 | ) 181 | 182 | #: A proxy for :meth:`removeproduct` 183 | remove_product = removeproduct 184 | 185 | 186 | class CategoryAttribute(API): 187 | """ 188 | Product Category Attribute API to connect to magento 189 | Allows to get attributes and options for category. 190 | """ 191 | __slots__ = () 192 | 193 | def currentStore(self, store_view=None): 194 | """ 195 | Set/Get current store view 196 | 197 | :param store_view: Store view ID or Code 198 | :return: int 199 | """ 200 | args = [store_view] if store_view else [] 201 | return int(self.call('catalog_category_attribute.currentStore', args)) 202 | 203 | def list(self): 204 | """ 205 | Retrieve Category attrbutes 206 | """ 207 | return self.call('category_attribute.list', []) 208 | 209 | def options(self, attribute_id, store_view=None): 210 | """ 211 | Retrieve attribute options 212 | 213 | :param attribute_id: ID of the attribute whose options are reqd 214 | :param store_view: ID or Code of the store view 215 | 216 | :return: list of dictionary 217 | """ 218 | return self.call( 219 | 'category_attribute.options', [attribute_id, store_view] 220 | ) 221 | 222 | 223 | class Product(API): 224 | """ 225 | Product API for magento 226 | """ 227 | __slots__ = () 228 | 229 | def currentStore(self, store_view=None): 230 | """ 231 | Set/Get current store view 232 | 233 | :param store_view: Store view ID or Code 234 | :return: int 235 | """ 236 | args = [store_view] if store_view else [] 237 | return int(self.call('catalog_product.currentStore', args)) 238 | 239 | def list(self, filters=None, store_view=None): 240 | """ 241 | Retrieve product list by filters 242 | 243 | :param filters: Dictionary of filters. 244 | 245 | Format : 246 | `{:{:}}` 247 | Example : 248 | `{'firstname':{'ilike':'sharoon'}}` 249 | 250 | :param store_view: Code or ID of store view 251 | :return: `list` of `dict` 252 | """ 253 | return self.call('catalog_product.list', [filters, store_view]) 254 | 255 | def info(self, product, store_view=None, attributes=None): 256 | """ 257 | Retrieve product data 258 | 259 | :param product: ID or SKU of product 260 | :param store_view: ID or Code of store view 261 | :param attributes: List of fields required 262 | :return: `dict` of values 263 | """ 264 | return self.call( 265 | 'catalog_product.info', [product, store_view, attributes] 266 | ) 267 | 268 | def create(self, product_type, attribute_set_id, sku, data): 269 | """ 270 | Create Product and return ID 271 | 272 | :param product_type: String type of product 273 | :param attribute_set_id: ID of attribute set 274 | :param sku: SKU of the product 275 | :param data: Dictionary of data 276 | :return: INT id of product created 277 | """ 278 | return int(self.call( 279 | 'catalog_product.create', 280 | [product_type, attribute_set_id, sku, data] 281 | ) 282 | ) 283 | 284 | def update(self, product, data, store_view=None): 285 | """ 286 | Update product Information 287 | 288 | :param product: ID or SKU of product 289 | :param data: Dictionary of attributes to update 290 | :param store_view: ID or Code of store view 291 | 292 | :return: Boolean 293 | """ 294 | return bool(self.call( 295 | 'catalog_product.update', [product, data, store_view]) 296 | ) 297 | 298 | def setSpecialPrice(self, product, special_price=None, 299 | from_date=None, to_date=None, store_view=None): 300 | """ 301 | Update product's special price 302 | 303 | :param product: ID or SKU of product 304 | :param special_price: Special Price 305 | :param from_date: From date 306 | :param to_date: To Date 307 | :param store_view: ID or Code of Store View 308 | 309 | :return: Boolean 310 | """ 311 | return bool(self.call( 312 | 'catalog_product.setSpecialPrice', 313 | [product, special_price, from_date, to_date, store_view] 314 | ) 315 | ) 316 | 317 | def getSpecialPrice(self, product, store_view=None): 318 | """ 319 | Get product special price data 320 | 321 | :param product: ID or SKU of product 322 | :param store_view: ID or Code of Store view 323 | 324 | :return: Dictionary 325 | """ 326 | return self.call( 327 | 'catalog_product.getSpecialPrice', [product, store_view] 328 | ) 329 | 330 | def delete(self, product): 331 | """ 332 | Delete a product 333 | 334 | :param product: ID or SKU of product 335 | :return: Boolean 336 | """ 337 | return bool(self.call('catalog_product.delete', [product])) 338 | 339 | 340 | class ProductAttribute(API): 341 | """ 342 | Product Attribute API 343 | """ 344 | __slots__ = () 345 | 346 | def currentStore(self, store_view=None): 347 | """ 348 | Set/Get current store view 349 | 350 | :param store_view: Store view ID or Code 351 | :return: int 352 | """ 353 | args = [store_view] if store_view else [] 354 | return int(self.call('catalog_product_attribute.currentStore', args)) 355 | 356 | def list(self, attribute_set_id): 357 | """ 358 | Retrieve product attribute list 359 | 360 | :param attribute_set_id: ID of attribute set 361 | :return: `list` of `dict` 362 | """ 363 | return self.call('catalog_product_attribute.list', [attribute_set_id]) 364 | 365 | def info(self, attribute): 366 | """ 367 | Retrieve product attribute info 368 | 369 | :param attribute: ID or Code of the attribute 370 | :return: `list` of `dict` 371 | """ 372 | return self.call('catalog_product_attribute.info', [attribute]) 373 | 374 | def options(self, attribute, store_view=None): 375 | """ 376 | Retrieve product attribute options 377 | 378 | :param attribute: ID or Code of the attribute 379 | :return: `list` of `dict` 380 | """ 381 | return self.call('catalog_product_attribute.options', 382 | [attribute, store_view]) 383 | 384 | def addOption(self, attribute, data): 385 | """ 386 | Create new options to attribute (Magento > 1.7.0) 387 | 388 | :param attribute: ID or Code of the attribute. 389 | :param data: Dictionary of Data. 390 | {'label':[{'store_id':[0,1], 'value':'Value'},], 'order':1, 'is_default':1} 391 | :return: True if created. 392 | """ 393 | return bool(self.call('product_attribute.addOption', 394 | [attribute, data])) 395 | 396 | def createOption(self, *a, **kw): 397 | warnings.warn( 398 | "ProductAttribute: createOption is deprecated, use addOption instead." 399 | ) 400 | return self.addOption(*a, **kw) 401 | 402 | def removeOption(self, attribute, option): 403 | """ 404 | Remove option to attribute (Magento > 1.7.0) 405 | 406 | :param attribute: ID or Code of the attribute. 407 | :param option: Option ID. 408 | :return: True if the option is removed. 409 | """ 410 | return bool(self.call('product_attribute.removeOption', 411 | [attribute, option])) 412 | 413 | def create(self, data): 414 | """ 415 | Create attribute entity. 416 | 417 | :param data: Dictionary of entity data to create attribute with. 418 | 419 | :return: Integer ID of attribute created 420 | """ 421 | return self.call('catalog_product_attribute.create', [data]) 422 | 423 | def update(self, attribute, data): 424 | """ 425 | Update attribute entity data. 426 | 427 | :param attribute: ID or Code of the attribute. 428 | :param data: Dictionary of entity data to update on attribute. 429 | 430 | :return: Boolean 431 | """ 432 | return self.call('catalog_product_attribute.update', [attribute, data]) 433 | 434 | 435 | class ProductAttributeSet(API): 436 | """ 437 | Product Attribute Set API 438 | """ 439 | __slots__ = () 440 | 441 | def list(self): 442 | """ 443 | Retrieve list of product attribute sets 444 | 445 | :return: `list` of `dict` 446 | """ 447 | return self.call('catalog_product_attribute_set.list', []) 448 | 449 | def create(self, attribute_set_name, skeleton_set_id): 450 | """ 451 | Create a new attribute set based on a "skeleton" attribute set. 452 | If unsure, use the "Default" attribute set as a skeleton. 453 | 454 | :param attribute_set_name: name of the new attribute set 455 | :param skeleton_set_id: id of the skeleton attribute set to base this set on. 456 | 457 | :return: Integer ID of new attribute set 458 | """ 459 | return self.call('catalog_product_attribute_set.create', [attribute_set_name, skeleton_set_id]) 460 | 461 | def attributeAdd(self, attribute_id, attribute_set_id): 462 | """ 463 | Add an existing attribute to an attribute set. 464 | 465 | :param attribute_id: ID of the attribute to add 466 | :param attribute_set_id: ID of the attribute set to add to 467 | 468 | :return: Boolean 469 | """ 470 | return self.call('catalog_product_attribute_set.attributeAdd', [attribute_id, attribute_set_id]) 471 | 472 | def attributeRemove(self, attribute_id, attribute_set_id): 473 | """ 474 | Remove an existing attribute to an attribute set. 475 | 476 | :param attribute_id: ID of the attribute to remove 477 | :param attribute_set_id: ID of the attribute set to remove from 478 | 479 | :return: Boolean 480 | """ 481 | return self.call('catalog_product_attribute_set.attributeRemove', [attribute_id, attribute_set_id]) 482 | 483 | 484 | 485 | class ProductTypes(API): 486 | """ 487 | Product Types API 488 | """ 489 | __slots__ = () 490 | 491 | def list(self): 492 | """ 493 | Retrieve list of product types 494 | 495 | :return: `list` of `dict` 496 | """ 497 | return self.call('catalog_product_type.list', []) 498 | 499 | 500 | class ProductImages(API): 501 | """ 502 | Product Images API 503 | """ 504 | __slots__ = () 505 | 506 | def currentStore(self, store_view=None): 507 | """ 508 | Set/Get current store view 509 | 510 | :param store_view: Store view ID or Code 511 | :return: int 512 | """ 513 | args = [] 514 | if store_view: 515 | args = [store_view] 516 | return int(self.call('catalog_product_attribute_media.currentStore', 517 | args)) 518 | 519 | def list(self, product, store_view=None): 520 | """ 521 | Retrieve product image list 522 | 523 | :param product: ID or SKU of product 524 | :param store_view: Code or ID of store view 525 | :return: `list` of `dict` 526 | """ 527 | return self.call('catalog_product_attribute_media.list', 528 | [product, store_view]) 529 | 530 | def info(self, product, image_file, store_view=None): 531 | """ 532 | Retrieve product image data 533 | 534 | :param product: ID or SKU of product 535 | :param store_view: ID or Code of store view 536 | :param attributes: List of fields required 537 | :return: `list` of `dict` 538 | """ 539 | return self.call('catalog_product_attribute_media.info', 540 | [product, image_file, store_view]) 541 | 542 | def types(self, attribute_set_id): 543 | """ 544 | Retrieve product image types (image, small_image, thumbnail, etc.) 545 | 546 | :param attribute_set_id: ID of attribute set 547 | :return: `list` of `dict` 548 | """ 549 | return self.call('catalog_product_attribute_media.types', 550 | [attribute_set_id]) 551 | 552 | def create(self, product, data, store_view=None): 553 | """ 554 | Upload a new product image. 555 | 556 | :param product: ID or SKU of product 557 | :param data: `dict` of image data (label, position, exclude, types) 558 | Example: { 'label': 'description of photo', 559 | 'position': '1', 'exclude': '0', 560 | 'types': ['image', 'small_image', 'thumbnail']} 561 | :param store_view: Store view ID or Code 562 | :return: string - image file name 563 | """ 564 | return self.call('catalog_product_attribute_media.create', 565 | [product, data, store_view]) 566 | 567 | def update(self, product, img_file_name, data, store_view=None): 568 | """ 569 | Update a product image. 570 | 571 | :param product: ID or SKU of product 572 | :param img_file_name: The image file name 573 | Example: '/m/y/my_image_thumb.jpg' 574 | :param data: `dict` of image data (label, position, exclude, types) 575 | Example: { 'label': 'description of photo', 576 | 'position': '1', 'exclude': '0', 577 | 'types': ['image', 'small_image', 'thumbnail']} 578 | :param store_view: Store view ID or Code 579 | :return: string - image file name 580 | """ 581 | return self.call('catalog_product_attribute_media.update', 582 | [product, img_file_name, data, store_view]) 583 | 584 | def remove(self, product, img_file_name): 585 | """ 586 | Remove a product image. 587 | 588 | :param product: ID or SKU of product 589 | :param img_file_name: The image file name 590 | Example: '/m/y/my_image_thumb.jpg' 591 | :return: boolean 592 | """ 593 | return self.call('catalog_product_attribute_media.remove', 594 | [product, img_file_name]) 595 | 596 | 597 | class ProductTierPrice(API): 598 | """ 599 | Product Tier Price API 600 | """ 601 | __slots__ = () 602 | 603 | def info(self, product): 604 | """ 605 | Retrieve product data 606 | 607 | :param product: ID or SKU of product 608 | :return: `list` of `dict` 609 | """ 610 | return self.call('catalog_product_attribute_tier_price.info', [product]) 611 | 612 | def update(self, product, data): 613 | """ 614 | Update product tier prices. 615 | 616 | Note: All existing tier prices for the product are replaced by the tier 617 | prices provided in data. 618 | 619 | :param product: ID or SKU of product 620 | :param data: List of dictionaries of tier price information 621 | Example: 622 | [{ 623 | 'website': 'all', 624 | 'customer_group_id': '1', 625 | 'qty': '99.0000', 626 | 'price': '123.9900' 627 | }, 628 | { 629 | 'website': 'all', 630 | ... 631 | },...] 632 | 633 | :return: Boolean 634 | """ 635 | return bool(self.call('catalog_product_attribute_tier_price.update', 636 | [product, data])) 637 | 638 | 639 | class ProductLinks(API): 640 | """ 641 | Product links API (related, cross sells, up sells, grouped) 642 | """ 643 | __slots__ = () 644 | 645 | def list(self, link_type, product): 646 | """ 647 | Retrieve list of linked products 648 | 649 | :param link_type: type of link, one of 'cross_sell', 'up_sell', 650 | 'related' or 'grouped' 651 | :param product: ID or SKU of product 652 | :return: `list` of `dict` 653 | """ 654 | return self.call('catalog_product_link.list', [link_type, product]) 655 | 656 | def assign(self, link_type, product, linked_product, data=None): 657 | """ 658 | Assign a product link 659 | 660 | :param link_type: type of link, one of 'cross_sell', 'up_sell', 661 | 'related' or 'grouped' 662 | :param product: ID or SKU of product 663 | :param linked_product: ID or SKU of linked product 664 | :param data: dictionary of link data, (position, qty, etc.) 665 | Example: { 'position': '0', 'qty': 1} 666 | :return: boolean 667 | """ 668 | return bool(self.call('catalog_product_link.assign', 669 | [link_type, product, linked_product, data])) 670 | 671 | def update(self, link_type, product, linked_product, data=None): 672 | """ 673 | Update a product link 674 | 675 | :param link_type: type of link, one of 'cross_sell', 'up_sell', 676 | 'related' or 'grouped' 677 | :param product: ID or SKU of product 678 | :param linked_product: ID or SKU of linked product 679 | :param data: dictionary of link data, (position, qty, etc.) 680 | Example: { 'position': '0', 'qty': 1} 681 | :return: boolean 682 | """ 683 | return bool(self.call('catalog_product_link.update', 684 | [link_type, product, linked_product, data])) 685 | 686 | def remove(self, link_type, product, linked_product): 687 | """ 688 | Remove a product link 689 | 690 | :param link_type: type of link, one of 'cross_sell', 'up_sell', 691 | 'related' or 'grouped' 692 | :param product: ID or SKU of product 693 | :param linked_product: ID or SKU of linked product to unlink 694 | :return: boolean 695 | """ 696 | return bool(self.call('catalog_product_link.remove', 697 | [link_type, product, linked_product])) 698 | 699 | def types(self): 700 | """ 701 | Retrieve a list of product link types 702 | 703 | :return: `list` of types 704 | """ 705 | return self.call('catalog_product_link.types', []) 706 | 707 | def attributes(self, link_type): 708 | """ 709 | Retrieve a list of attributes of a product link type 710 | 711 | :param link_type: type of link, one of 'cross_sell', 'up_sell', 712 | 'related' or 'grouped' 713 | :return: `list` of `dict` 714 | Format : 715 | `[{'code': , 'type': }, ...]` 716 | Example : 717 | `[{'code': 'position', 'type': 'int'}, 718 | {'code': 'qty', 'type': 'decimal'}]` 719 | """ 720 | return self.call('catalog_product_link.attributes', [link_type]) 721 | 722 | 723 | class ProductConfigurable(API): 724 | """ 725 | Product Configurable API for magento. 726 | 727 | These API endpoints only work if you have zikzakmedia's 728 | magento_webservices Magento plugin installed. 729 | """ 730 | __slots__ = () 731 | 732 | def info(self, product): 733 | """ 734 | Configurable product Info 735 | 736 | :param product: ID or SKU of product 737 | :return: List 738 | """ 739 | return self.call('ol_catalog_product_link.list', [product]) 740 | 741 | def getSuperAttributes(self, product): 742 | """ 743 | Configurable Attributes product 744 | 745 | :param product: ID or SKU of product 746 | :return: List 747 | """ 748 | return self.call('ol_catalog_product_link.listSuperAttributes', 749 | [product]) 750 | 751 | def setSuperAttributeValues(self, product, attribute): 752 | """ 753 | Configurable Attributes product 754 | 755 | :param product: ID or SKU of product 756 | :param attribute: ID attribute 757 | :return: List 758 | """ 759 | return self.call('ol_catalog_product_link.setSuperAttributeValues', 760 | [product, attribute]) 761 | 762 | def update(self, product, linked_products, attributes): 763 | """ 764 | Configurable Update product 765 | 766 | :param product: ID or SKU of product 767 | :param linked_products: List ID or SKU of linked product to link 768 | :param attributes: dicc 769 | :return: True/False 770 | """ 771 | return bool(self.call('ol_catalog_product_link.assign', 772 | [product, linked_products, attributes])) 773 | 774 | def remove(self, product, linked_products): 775 | """ 776 | Remove a product link configurable 777 | 778 | :param product: ID or SKU of product 779 | :param linked_products: List ID or SKU of linked product to unlink 780 | """ 781 | return bool(self.call('ol_catalog_product_link.remove', 782 | [product, linked_products])) 783 | 784 | 785 | class Inventory(API): 786 | """ 787 | Allows to update stock attributes (status, quantity) 788 | """ 789 | __slots__ = () 790 | 791 | def list(self, products): 792 | """ 793 | Retrieve inventory stock data by product ids 794 | 795 | :param products: list of IDs or SKUs of products 796 | :return: `list` of `dict` 797 | """ 798 | return self.call('cataloginventory_stock_item.list', [products]) 799 | 800 | def update(self, product, data): 801 | """ 802 | Update inventory stock data 803 | 804 | :param product: ID or SKU of product 805 | :param data: Dictionary of data to change, 806 | eg dict(qty=99, is_in_stock='1') 807 | 808 | :return: boolean 809 | """ 810 | return bool( 811 | self.call( 812 | 'cataloginventory_stock_item.update', 813 | [product, data] 814 | ) 815 | ) 816 | -------------------------------------------------------------------------------- /magento/checkout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.checkout 4 | 5 | Checkout API for magento 6 | Allows you to manage shopping carts and the checkout process. 7 | 8 | :copyright: (c) 2010 by Sharoon Thomas. 9 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD. 10 | 11 | :license: AGPLv3, see LICENSE for more details 12 | ''' 13 | 14 | from .api import API 15 | 16 | 17 | class Cart(API): 18 | """ 19 | Allows you to manage shopping carts. 20 | """ 21 | __slots__ = () 22 | 23 | def create(self, store_view=None): 24 | """ 25 | Create an empty shopping cart (quote). 26 | 27 | :param store_view: Store view ID or code 28 | :return: integer, ID of the created empty shopping cart 29 | """ 30 | return self.call('cart.create', [store_view]) 31 | 32 | def info(self, quote_id, store_view=None): 33 | """ 34 | Retrieve full information about the shopping cart (quote). 35 | 36 | :param quote_id: Shopping cart ID (quote ID) 37 | :param store_view: Store view ID or code 38 | :return: dict representing shopping cart info 39 | """ 40 | return self.call('cart.info', [quote_id, store_view]) 41 | 42 | def license(self, quote_id, store_view=None): 43 | """ 44 | Retrieve the website license agreement for the quote according to the 45 | website (store). 46 | 47 | :param quote_id: Shopping cart ID (quote ID) 48 | :param store_view: Store view ID or code 49 | :return: dict representing shopping cart license 50 | """ 51 | return self.call('cart.license', [quote_id, store_view]) 52 | 53 | def order(self, quote_id, store_view=None, license_id=None): 54 | """ 55 | Allows you to create an order from a shopping cart (quote). 56 | Before placing the order, you need to add the customer, customer 57 | address, shipping and payment methods. 58 | 59 | :param quote_id: Shopping cart ID (quote ID) 60 | :param store_view: Store view ID or code 61 | :param license_id: Website license ID 62 | :return: string, result of creating order 63 | """ 64 | return self.call('cart.order', [quote_id, store_view, license_id]) 65 | 66 | def totals(self, quote_id, store_view=None): 67 | """ 68 | Allows you to retrieve total prices for a shopping cart (quote). 69 | 70 | :param quote_id: Shopping cart ID (quote ID) 71 | :param store_view: Store view ID or code 72 | :return: dict representing shopping cart totals 73 | """ 74 | return self.call('cart.totals', [quote_id, store_view]) 75 | 76 | 77 | class CartCoupon(API): 78 | """ 79 | Allows you to add and remove coupon codes for a shopping cart. 80 | """ 81 | __slots__ = () 82 | 83 | def add(self, quote_id, coupon_code, store_view=None): 84 | """ 85 | Add a coupon code to a quote. 86 | 87 | :param quote_id: Shopping cart ID (quote ID) 88 | :param coupon_code, string, Coupon code 89 | :param store_view: Store view ID or code 90 | :return: boolean, True if the coupon code is added 91 | """ 92 | return bool( 93 | self.call('cart_coupon.add', [quote_id, coupon_code, store_view]) 94 | ) 95 | 96 | def remove(self, quote_id, store_view=None): 97 | """ 98 | Remove a coupon code from a quote 99 | 100 | :param quote_id: Shopping cart ID (quote ID) 101 | :param store_view: Store view ID or code 102 | :return: boolean, True if the coupon code is removed 103 | """ 104 | return bool( 105 | self.call('cart_coupon.remove', [quote_id, store_view]) 106 | ) 107 | 108 | 109 | class CartCustomer(API): 110 | """ 111 | Allows you to add customer information and addresses into a shopping cart. 112 | """ 113 | __slots__ = () 114 | 115 | def addresses(self, quote_id, address_data, store_view=None): 116 | """ 117 | Add customer information into a shopping cart 118 | 119 | :param quote_id: Shopping cart ID (quote ID) 120 | :param address_data, list of dicts of address details, example 121 | [ 122 | { 123 | 'mode': 'billing', 124 | 'address_id': 'customer_address_id' 125 | }, 126 | { 127 | 'mode': 'shipping', 128 | 'firstname': 'testFirstname', 129 | 'lastname': 'testLastname', 130 | 'company': 'testCompany', 131 | 'street': 'testStreet', 132 | 'city': 'testCity', 133 | 'region': 'testRegion', 134 | 'region_id': 'testRegionId', 135 | 'postcode': 'testPostcode', 136 | 'country_id': 'id', 137 | 'telephone': '0123456789', 138 | 'fax': '0123456789', 139 | 'is_default_shipping': 0, 140 | 'is_default_billing': 0 141 | }, 142 | ] 143 | :param store_view: Store view ID or code 144 | :return: boolean, True if the address is set 145 | """ 146 | return bool( 147 | self.call('cart_customer.addresses', 148 | [quote_id, address_data, store_view]) 149 | ) 150 | 151 | def set(self, quote_id, customer_data, store_view=None): 152 | """ 153 | Add customer information into a shopping cart 154 | 155 | :param quote_id: Shopping cart ID (quote ID) 156 | :param customer_data, dict of customer details, example 157 | { 158 | 'firstname': 'testFirstname', 159 | 'lastname': 'testLastName', 160 | 'email': 'testEmail', 161 | 'website_id': '0', 162 | 'store_id': '0', 163 | 'mode': 'guest' 164 | } 165 | :param store_view: Store view ID or code 166 | :return: boolean, True if information added 167 | """ 168 | return bool( 169 | self.call('cart_customer.set', 170 | [quote_id, customer_data, store_view]) 171 | ) 172 | 173 | 174 | class CartPayment(API): 175 | """ 176 | Allows you to retrieve and set payment methods for a shopping cart. 177 | """ 178 | __slots__ = () 179 | 180 | def list(self, quote_id, store_view=None): 181 | """ 182 | Get the list of available payment methods for a shopping cart 183 | 184 | :param quote_id: Shopping cart ID (quote ID) 185 | :param store_view: Store view ID or code 186 | :return: list of dicts, example 187 | [{ 188 | 'code': 'payment method code', 189 | 'title': 'payment method title', 190 | 'cctypes': ['cc_type1', 'cc_type2', ...], 191 | }] 192 | 193 | """ 194 | return self.call('cart_payment.list', [quote_id, store_view]) 195 | 196 | def method(self, quote_id, payment_data, store_view=None): 197 | """ 198 | Allows you to set a payment method for a shopping cart (quote). 199 | 200 | :param quote_id: Shopping cart ID (quote ID) 201 | :param payment_data, dict of payment details, example 202 | { 203 | 'po_number': '', 204 | 'method': 'checkmo', 205 | 'cc_cid': '', 206 | 'cc_owner': '', 207 | 'cc_number': '', 208 | 'cc_type': '', 209 | 'cc_exp_year': '', 210 | 'cc_exp_month': '' 211 | } 212 | :param store_view: Store view ID or code 213 | :return: boolean, True on success 214 | """ 215 | return bool( 216 | self.call('cart_payment.method', 217 | [quote_id, payment_data, store_view]) 218 | ) 219 | 220 | 221 | class CartProduct(API): 222 | """ 223 | Allows you to manage products in a shopping cart. 224 | """ 225 | __slots__ = () 226 | 227 | def add(self, quote_id, product_data, store_view=None): 228 | """ 229 | Allows you to add one or more products to the shopping cart (quote). 230 | 231 | :param quote_id: Shopping cart ID (quote ID) 232 | :param product_data, list of dicts of product details, example 233 | [ 234 | { 235 | 'product_id': 1, 236 | 'qty': 2, 237 | 'options': { 238 | 'option_1': 'value_1', 239 | 'option_2': 'value_2', 240 | ... 241 | }, 242 | 'bundle_option': {}, 243 | 'bundle_option_qty': {}, 244 | 'links': [], 245 | }, 246 | { 247 | 'sku': 'S0012345', 248 | 'qty': 4, 249 | }, 250 | ] 251 | :param store_view: Store view ID or code 252 | :return: boolean, True on success (if the product is added to the 253 | shopping cart) 254 | """ 255 | return bool( 256 | self.call('cart_product.add', [quote_id, product_data, store_view]) 257 | ) 258 | 259 | def list(self, quote_id, store_view=None): 260 | """ 261 | Allows you to retrieve the list of products in the shopping cart (quote). 262 | 263 | :param quote_id: Shopping cart ID (quote ID) 264 | :param store_view: Store view ID or code 265 | :return: list of dicts, example 266 | [{ 267 | 'product_id': 12345, 268 | 'sku': 'S00012345, 269 | 'name': 'Product Name', 270 | 'set': 1, 271 | 'type': 'Product type', 272 | 'category_ids: [category_id1, ...], 273 | 'website_ids: [website_id1, ...], 274 | }] 275 | 276 | """ 277 | return self.call('cart_product.list', [quote_id, store_view]) 278 | 279 | def move_to_customer_quote(self, quote_id, product_data, store_view=None): 280 | """ 281 | Allows you to move products from the current quote to a customer quote. 282 | 283 | :param quote_id: Shopping cart ID (quote ID) 284 | :param product_data, list of dicts of product details, example 285 | [ 286 | { 287 | 'product_id': 1, 288 | 'qty': 2, 289 | 'options': { 290 | 'option_1': 'value_1', 291 | 'option_2': 'value_2', 292 | ... 293 | }, 294 | 'bundle_option': {}, 295 | 'bundle_option_qty': {}, 296 | 'links': [], 297 | }, 298 | { 299 | 'sku': 'S0012345', 300 | 'qty': 4, 301 | }, 302 | ] 303 | :param store_view: Store view ID or code 304 | :return: boolean, True if the product is moved to customer quote 305 | """ 306 | return bool( 307 | self.call('cart_product.moveToCustomerQuote', 308 | [quote_id, product_data, store_view]) 309 | ) 310 | 311 | #: A proxy for :meth:`move_to_customer_quote` 312 | moveToCustomerQuote = move_to_customer_quote 313 | 314 | def remove(self, quote_id, product_data, store_view=None): 315 | """ 316 | Allows you to remove one or several products from a shopping cart 317 | (quote). 318 | 319 | :param quote_id: Shopping cart ID (quote ID) 320 | :param product_data, list of dicts of product details, see def add() 321 | :param store_view: Store view ID or code 322 | :return: boolean, True if the product is removed 323 | """ 324 | return bool( 325 | self.call('cart_product.remove', 326 | [quote_id, product_data, store_view]) 327 | ) 328 | 329 | def update(self, quote_id, product_data, store_view=None): 330 | """ 331 | Allows you to update one or several products in the shopping cart 332 | (quote). 333 | 334 | :param quote_id: Shopping cart ID (quote ID) 335 | :param product_data, list of dicts of product details, see def add() 336 | :param store_view: Store view ID or code 337 | :return: boolean, True if the product is updated . 338 | """ 339 | return bool( 340 | self.call('cart_product.update', 341 | [quote_id, product_data, store_view]) 342 | ) 343 | 344 | 345 | class CartShipping(API): 346 | """ 347 | Allows you to retrieve and set shipping methods for a shopping cart. 348 | """ 349 | __slots__ = () 350 | 351 | def list(self, quote_id, store_view=None): 352 | """ 353 | Allows you to retrieve the list of available shipping methods for a 354 | shopping cart (quote). 355 | 356 | :param quote_id: Shopping cart ID (quote ID) 357 | :param store_view: Store view ID or code 358 | :return: list of strings, shipping method codes 359 | """ 360 | return self.call('cart_shipping.list', [quote_id, store_view]) 361 | 362 | def method(self, quote_id, shipping_method, store_view=None): 363 | """ 364 | Allows you to set a shipping method for a shopping cart (quote). 365 | 366 | :param quote_id: Shopping cart ID (quote ID) 367 | :param shipping_method, string, shipping method code 368 | :param store_view: Store view ID or code 369 | :return: boolean, True if the shipping method is set 370 | """ 371 | return bool( 372 | self.call('cart_shipping.method', 373 | [quote_id, shipping_method, store_view]) 374 | ) 375 | -------------------------------------------------------------------------------- /magento/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.client 4 | 5 | Client API for magento 6 | 7 | :copyright: (c) 2014 by Openlabs Technologies & Consulting (P) LTD 8 | :license: BSD, see LICENSE for more details 9 | ''' 10 | from threading import RLock 11 | 12 | from .api import API 13 | from .catalog import Category, CategoryAttribute, Product, ProductAttribute, \ 14 | ProductAttributeSet, ProductTypes, ProductImages, ProductTierPrice, \ 15 | ProductLinks, ProductConfigurable, Inventory 16 | 17 | 18 | _missing = [] 19 | 20 | 21 | class api_class_property(object): 22 | """A function that converts a function into a lazy property. 23 | The class wrapped is called the first time to retrieve the result 24 | and then that calculated result is used the next time you access 25 | the value. 26 | 27 | Works like the one in Werkzeug but has a lock for thread safety. 28 | """ 29 | 30 | def __init__(self, klass, name=None, doc=None): 31 | self.__name__ = name or klass.__name__ 32 | self.__module__ = klass.__module__ 33 | self.__doc__ = doc or klass.__doc__ 34 | self.klass = klass 35 | self.lock = RLock() 36 | 37 | def __get__(self, obj, type=None): 38 | if obj is None: 39 | return self 40 | with self.lock: 41 | value = obj.__dict__.get(self.__name__, _missing) 42 | if value is _missing: 43 | value = self.klass( 44 | obj.url, 45 | obj.username, 46 | obj.password, 47 | obj.version, 48 | True, 49 | obj.protocol, 50 | ) 51 | obj.__dict__[self.__name__] = value.__enter__() 52 | return value 53 | 54 | 55 | class Client(API): 56 | """ 57 | A convenient API which works more closer to the semantics of the WS API 58 | rather than the with context manager. 59 | 60 | Example usage:: 61 | 62 | from magento import Client 63 | client = Client('http://yourstore.com', 'api username', 'api password') 64 | client.catalog_category.tree() 65 | client.catalog_product.list() 66 | 67 | :param url: URL to the magento instance. 68 | By default the URL is treated as a base url 69 | of the domain to which the api part of the URL 70 | is added. If you want to specify the complete 71 | URL, set the full_url flag as True. 72 | :param username: API username of the Web services user. Note 73 | that this is NOT magento admin username 74 | :param password: API password of the Web services user. 75 | :param version: The version of magento the connection is being made to. 76 | It is recommended to specify this as there could be 77 | API specific changes in certain calls. Default value is 78 | 1.3.2.4 79 | :param full_url: If set to true, then the `url` is expected to 80 | be a complete URL 81 | :param protocol: 'xmlrpc' and 'soap' are valid values 82 | """ 83 | 84 | catalog_category = api_class_property(Category) 85 | catalog_category_attribute = api_class_property(CategoryAttribute) 86 | catalog_product = api_class_property(Product) 87 | catalog_product_attribute = api_class_property(ProductAttribute) 88 | catalog_product_attribute_set = api_class_property(ProductAttributeSet) 89 | catalog_product_type = api_class_property(ProductTypes) 90 | catalog_product_attribute_media = api_class_property(ProductImages) 91 | catalog_product_attribute_tier_price = api_class_property(ProductTierPrice) 92 | catalog_product_link = api_class_property(ProductLinks) 93 | 94 | cataloginventory_stock_item = api_class_property(Inventory) 95 | 96 | ol_catalog_product_link = api_class_property(ProductConfigurable) 97 | -------------------------------------------------------------------------------- /magento/customer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.customer 4 | 5 | Customer API for magento 6 | 7 | :copyright: (c) 2010 by Sharoon Thomas. 8 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD. 9 | 10 | :license: AGPLv3, see LICENSE for more details 11 | ''' 12 | from magento.api import API 13 | 14 | 15 | class Customer(API): 16 | """ 17 | Customer API 18 | 19 | Example usage:: 20 | 21 | from magento import Customer as CustomerAPI 22 | 23 | with CustomerAPI(url, username, password) as customer_api: 24 | return customer_api.list() 25 | """ 26 | __slots__ = () 27 | 28 | def list(self, filters=None): 29 | """ 30 | Retreive list of customers 31 | 32 | :param filters: Dictionary of filters. 33 | 34 | Format: `{:{:}}` 35 | 36 | Example: `{'firstname':{'ilike':'sharoon'}}` 37 | :return: List of dictionaries of matching records 38 | """ 39 | return self.call('customer.list', filters and [filters] or [{}]) 40 | 41 | def create(self, data): 42 | """ 43 | Create a customer using the given data 44 | 45 | :param data: Dictionary of values 46 | :return: Integer ID of new record 47 | """ 48 | return int(self.call('customer.create', [data])) 49 | 50 | def info(self, id, attributes=None): 51 | """ 52 | Retrieve customer data 53 | 54 | :param id: ID of customer 55 | :param attributes: `List` of attributes needed 56 | """ 57 | if attributes: 58 | return self.call('customer.info', [id, attributes]) 59 | else: 60 | return self.call('customer.info', [id]) 61 | 62 | def update(self, id, data): 63 | """ 64 | Update a customer using the given data 65 | 66 | :param id: ID of the customer record to modify 67 | :param data: Dictionary of values 68 | :return: Boolean 69 | """ 70 | return self.call('customer.update', [id, data]) 71 | 72 | def delete(self, id): 73 | """ 74 | Delete a customer 75 | 76 | :param id: ID of customer to delete 77 | :return: Boolean 78 | """ 79 | return self.call('customer.delete', [id]) 80 | 81 | 82 | class CustomerGroup(API): 83 | """ 84 | Customer Group API to connect to magento 85 | """ 86 | __slots__ = () 87 | 88 | def list(self): 89 | """ 90 | Retreive list of customers 91 | 92 | :return: List of dictionaries of matching records 93 | """ 94 | return self.call('customer_group.list', []) 95 | 96 | 97 | class CustomerAddress(API): 98 | """ 99 | Customer Address API 100 | """ 101 | __slots__ = () 102 | 103 | def list(self, customer_id): 104 | """ 105 | Retreive list of customer Addresses 106 | 107 | :param customer_id: ID of customer whose address needs to be fetched 108 | :return: List of dictionaries of matching records 109 | """ 110 | return self.call('customer_address.list', [customer_id]) 111 | 112 | def create(self, customer_id, data): 113 | """ 114 | Create a customer using the given data 115 | 116 | :param customer_id: ID of customer, whose address is being added 117 | :param data: Dictionary of values (country, zip, city, etc...) 118 | :return: Integer ID of new record 119 | """ 120 | return int(self.call('customer_address.create', [customer_id, data])) 121 | 122 | def info(self, id): 123 | """ 124 | Retrieve customer data 125 | 126 | :param id: ID of customer 127 | """ 128 | return self.call('customer_address.info', [id]) 129 | 130 | def update(self, id, data): 131 | """ 132 | Update a customer address using the given data 133 | 134 | :param id: ID of the customer address record to modify 135 | :param data: Dictionary of values 136 | :return: Boolean 137 | """ 138 | return self.call('customer_address.update', [id, data]) 139 | 140 | def delete(self, id): 141 | """ 142 | Delete a customer address 143 | 144 | :param id: ID of address to delete 145 | :return: Boolean 146 | """ 147 | return self.call('customer_address.delete', [id]) 148 | -------------------------------------------------------------------------------- /magento/directory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.directory 4 | 5 | Directory Country API for magento 6 | 7 | :copyright: (c) 2010 by Sharoon Thomas. 8 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD 9 | :license: AGPLv3, see LICENSE for more details 10 | ''' 11 | from magento.api import API 12 | 13 | 14 | class Country(API): 15 | """ 16 | Country API to connect to magento 17 | """ 18 | __slots__ = () 19 | 20 | def list(self): 21 | """ 22 | Retreive list of Countries 23 | 24 | :return: List of dictionaries 25 | """ 26 | return self.call('directory_country.list', []) 27 | 28 | 29 | class Region(API): 30 | """ 31 | Region API to connect to magento 32 | """ 33 | __slots__ = () 34 | 35 | def list(self, country): 36 | """ 37 | Retrieve list of regions 38 | 39 | :param country: Country code in ISO2 or ISO3 40 | :return: List of Dictionaries 41 | """ 42 | return self.call('directory_region.list', [country]) 43 | -------------------------------------------------------------------------------- /magento/miscellaneous.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | miscellaneous 4 | 5 | This API allows to access additional magento information 6 | 7 | :copyright: (c) 2013 by Openlabs Technologies & Consulting (P) Limited 8 | :license: AGPLv3, see LICENSE for more details. 9 | """ 10 | from magento.api import API 11 | 12 | 13 | class Store(API): 14 | """ 15 | This API allows to retrieve information about store views 16 | """ 17 | 18 | __slots__ = () 19 | 20 | def info(self, store_id): 21 | """ 22 | Returns information for store view 23 | 24 | :param store_id: ID of store view 25 | :return: Dictionary containing information about store view 26 | """ 27 | return self.call('store.info', [store_id]) 28 | 29 | def list(self, filters=None): 30 | """ 31 | Returns list of store views 32 | 33 | :param filters: Dictionary of filters. 34 | 35 | Format : 36 | {:{:}} 37 | Example : 38 | {'store_id':{'=':'1'}} 39 | :return: List of Dictionaries 40 | """ 41 | return self.call('store.list', [filters]) 42 | 43 | 44 | class Magento(API): 45 | """ 46 | This API returns information about current magento installation 47 | """ 48 | 49 | __slots__ = () 50 | 51 | def info(self): 52 | """ 53 | Returns information about current magento 54 | """ 55 | return self.call('core_magento.info', []) 56 | -------------------------------------------------------------------------------- /magento/sales.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.sales 4 | 5 | Allows to export/import sales orders from/into Magento, 6 | to create invoices, shipments, credit memos 7 | 8 | :copyright: (c) 2010 by Sharoon Thomas. 9 | :copyright: (c) 2010-2013 by Openlabs Technologies & Consulting (P) LTD 10 | :license: AGPLv3, see LICENSE for more details 11 | ''' 12 | from .api import API 13 | 14 | 15 | class Order(API): 16 | """ 17 | Allows to import/export orders. 18 | """ 19 | __slots__ = () 20 | 21 | def list(self, filters=None): 22 | """ 23 | Retrieve order list by filters 24 | 25 | :param filters: Dictionary of filters. 26 | 27 | Format : 28 | `{:{:}}` 29 | Example : 30 | `{'firstname':{'ilike':'sharoon'}}` 31 | 32 | :return: `list` of `dict` 33 | """ 34 | return self.call('sales_order.list', [filters]) 35 | 36 | def search(self, filters=None, fields=None, limit=None, page=1): 37 | """ 38 | Retrieve order list by options using search api. Using this result can 39 | be paginated 40 | 41 | :param options: Dictionary of options. 42 | 43 | :param filters: `{:{:}}` 44 | :param fields: [, ...] 45 | :param limit: `page limit` 46 | :param page: `current page` 47 | 48 | :return: `list` of `dict` 49 | """ 50 | options = { 51 | 'imported': False, 52 | 'filters': filters or {}, 53 | 'fields': fields or [], 54 | 'limit': limit or 1000, 55 | 'page': page, 56 | } 57 | return self.call('sales_order.search', [options]) 58 | 59 | def info(self, order_increment_id): 60 | """ 61 | Retrieve order info 62 | 63 | :param order_increment_id: Order ID 64 | """ 65 | return self.call( 66 | 'sales_order.info', [order_increment_id] 67 | ) 68 | 69 | def addcomment(self, order_increment_id, 70 | status, comment=None, notify=False): 71 | """ 72 | Add comment to order or change its state 73 | 74 | :param order_increment_id: Order ID 75 | TODO: Identify possible values for status 76 | """ 77 | if comment is None: 78 | comment = "" 79 | return bool(self.call( 80 | 'sales_order.addComment', 81 | [order_increment_id, status, comment, notify] 82 | ) 83 | ) 84 | 85 | #: A proxy for :meth:`addcomment` 86 | addComment = addcomment 87 | 88 | def hold(self, order_increment_id): 89 | """ 90 | Hold order 91 | 92 | :param order_increment_id: Order ID 93 | """ 94 | return bool(self.call('sales_order.hold', [order_increment_id])) 95 | 96 | def unhold(self, order_increment_id): 97 | """ 98 | Unhold order 99 | 100 | :param order_increment_id: Order ID 101 | """ 102 | return bool(self.call('sales_order.unhold', [order_increment_id])) 103 | 104 | def cancel(self, order_increment_id): 105 | """ 106 | Cancel an order 107 | 108 | :param order_increment_id: Order ID 109 | """ 110 | return bool(self.call('sales_order.cancel', [order_increment_id])) 111 | 112 | 113 | class Shipment(API): 114 | """ 115 | Allows create/export order shipments. 116 | """ 117 | __slots__ = () 118 | 119 | def list(self, filters=None): 120 | """ 121 | Retrieve shipment list by filters 122 | 123 | :param filters: Dictionary of filters. 124 | 125 | Format : 126 | `{:{:}}` 127 | Example : 128 | `{'firstname':{'ilike':'sharoon'}}` 129 | 130 | :return: `list` of `dict` 131 | """ 132 | return self.call('sales_order_shipment.list', [filters]) 133 | 134 | def info(self, shipment_increment_id): 135 | """ 136 | Retrieve shipment info 137 | 138 | :param shipment_increment_id: Order ID 139 | """ 140 | return self.call('sales_order_shipment.info', [shipment_increment_id]) 141 | 142 | def create(self, order_increment_id, 143 | items_qty, comment=None, email=True, include_comment=False): 144 | """ 145 | Create new shipment for order 146 | 147 | :param order_increment_id: Order Increment ID 148 | :type order_increment_id: str 149 | :param items_qty: items qty to ship 150 | :type items_qty: associative array (order_item_id ⇒ qty) as dict 151 | :param comment: Shipment Comment 152 | :type comment: str 153 | :param email: send e-mail flag (optional) 154 | :type email: bool 155 | :param include_comment: include comment in e-mail flag (optional) 156 | :type include_comment: bool 157 | """ 158 | if comment is None: 159 | comment = '' 160 | return self.call( 161 | 'sales_order_shipment.create', [ 162 | order_increment_id, items_qty, comment, email, include_comment 163 | ] 164 | ) 165 | 166 | def addcomment(self, shipment_increment_id, 167 | comment, email=True, include_in_email=False): 168 | """ 169 | Add new comment to shipment 170 | 171 | :param shipment_increment_id: Shipment ID 172 | """ 173 | return bool( 174 | self.call( 175 | 'sales_order_shipment.addComment', 176 | [shipment_increment_id, comment, email, include_in_email] 177 | ) 178 | ) 179 | 180 | #: A proxy for :meth:`addcomment` 181 | addComment = addcomment 182 | 183 | def addtrack(self, shipment_increment_id, carrier, title, track_number): 184 | """ 185 | Add new tracking number 186 | 187 | :param shipment_increment_id: Shipment ID 188 | :param carrier: Carrier Code 189 | :param title: Tracking title 190 | :param track_number: Tracking Number 191 | """ 192 | return self.call( 193 | 'sales_order_shipment.addTrack', 194 | [shipment_increment_id, carrier, title, track_number] 195 | ) 196 | 197 | #: A proxy for :meth:`addtrack` 198 | addTrack = addtrack 199 | 200 | def removetrack(self, shipment_increment_id, track_id): 201 | """ 202 | Remove tracking number 203 | 204 | :param shipment_increment_id: SHipment ID 205 | :param track_id: Tracking number to remove 206 | """ 207 | return bool( 208 | self.call( 209 | 'sales_order_shipment.removeTrack', 210 | [shipment_increment_id, track_id] 211 | ) 212 | ) 213 | 214 | #: A proxy for :meth:`removetrack` 215 | removeTrack = removetrack 216 | 217 | def getcarriers(self, order_increment_id): 218 | """ 219 | Retrieve list of allowed carriers for order 220 | 221 | :param order_increment_id: Order ID 222 | """ 223 | return self.call( 224 | 'sales_order_shipment.getCarriers', [order_increment_id] 225 | ) 226 | 227 | #: A proxy for :meth:`getcarriers` 228 | getCarriers = getcarriers 229 | 230 | def sendinfo(self, shipment_increment_id, comment=''): 231 | """ 232 | Send email with shipment data to customer 233 | 234 | :param order_increment_id: Order ID 235 | """ 236 | return self.call( 237 | 'sales_order_shipment.sendInfo', [shipment_increment_id, comment] 238 | ) 239 | 240 | #: A proxy for :meth:`sendinfo` 241 | sendInfo = sendinfo 242 | 243 | 244 | class Invoice(API): 245 | """ 246 | Allows create/export order invoices 247 | """ 248 | __slots__ = () 249 | 250 | def list(self, filters=None): 251 | """ 252 | Retrieve invoice list by filters 253 | 254 | :param filters: Dictionary of filters. 255 | 256 | Format : 257 | `{:{:}}` 258 | Example : 259 | `{'firstname':{'ilike':'sharoon'}}` 260 | 261 | :return: `list` of `dict` 262 | """ 263 | return self.call('sales_order_invoice.list', [filters]) 264 | 265 | def info(self, invoice_increment_id): 266 | """ 267 | Retrieve invoice info 268 | 269 | :param invoice_increment_id: Invoice ID 270 | """ 271 | return self.call( 272 | 'sales_order_invoice.info', [invoice_increment_id] 273 | ) 274 | 275 | def create(self, order_increment_id, items_qty, 276 | comment=None, email=True, include_comment=False): 277 | """ 278 | Create new invoice for order 279 | 280 | :param order_increment_id: Order increment ID 281 | :type order_increment_id: str 282 | :param items_qty: Items quantity to invoice 283 | :type items_qty: dict 284 | :param comment: Invoice Comment 285 | :type comment: str 286 | :param email: send invoice on e-mail 287 | :type email: bool 288 | :param include_comment: Include comments in email 289 | :type include_comment: bool 290 | 291 | :rtype: str 292 | """ 293 | return self.call( 294 | 'sales_order_invoice.create', 295 | [order_increment_id, items_qty, comment, email, include_comment] 296 | ) 297 | 298 | def addcomment(self, invoice_increment_id, 299 | comment=None, email=False, include_comment=False): 300 | """ 301 | Add comment to invoice or change its state 302 | 303 | :param invoice_increment_id: Invoice ID 304 | """ 305 | if comment is None: 306 | comment = "" 307 | return bool( 308 | self.call( 309 | 'sales_order_invoice.addComment', 310 | [invoice_increment_id, comment, email, include_comment] 311 | ) 312 | ) 313 | 314 | #: Add a proxy for :meth:`addcomment` 315 | addComment = addcomment 316 | 317 | def capture(self, invoice_increment_id): 318 | """ 319 | Capture Invoice 320 | 321 | :attention: You should check the invoice to see if can be 322 | captured before attempting to capture an invoice, otherwise 323 | the API call with generate an error. 324 | 325 | Invoices have states as defined in the model 326 | Mage_Sales_Model_Order_Invoice: 327 | 328 | STATE_OPEN = 1 329 | STATE_PAID = 2 330 | STATE_CANCELED = 3 331 | 332 | Also note there is a method call in the model that checks this 333 | for you canCapture(), and it also verifies that the payment is 334 | able to be captured, so the invoice state might not be the only 335 | condition that’s required to allow it to be captured. 336 | 337 | :param invoice_increment_id: Invoice ID 338 | :rtype: bool 339 | """ 340 | return bool( 341 | self.call('sales_order_invoice.capture', [invoice_increment_id]) 342 | ) 343 | 344 | def void(self, invoice_increment_id): 345 | """ 346 | Void an invoice 347 | 348 | :param invoice_increment_id: Invoice ID 349 | :rtype: bool 350 | """ 351 | return bool( 352 | self.call('sales_order_invoice.void', [invoice_increment_id]) 353 | ) 354 | 355 | def cancel(self, invoice_increment_id): 356 | """ 357 | Cancel invoice 358 | 359 | :param invoice_increment_id: Invoice ID 360 | :rtype: bool 361 | """ 362 | return bool( 363 | self.call('sales_order_invoice.cancel', [invoice_increment_id]) 364 | ) 365 | -------------------------------------------------------------------------------- /magento/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | ''' 3 | magento.utils 4 | 5 | General purpose utility functions 6 | 7 | :copyright: (c) 2010 by Sharoon Thomas. 8 | :copyright: (c) 2010 by Openlabs Technologies & Consulting (P) LTD. 9 | :license: AGPLv3, see LICENSE for more details 10 | ''' 11 | 12 | 13 | def expand_url(url, protocol): 14 | """ 15 | Expands the given URL to a full URL by adding 16 | the magento soap/wsdl parts 17 | 18 | :param url: URL to be expanded 19 | :param service: 'xmlrpc' or 'soap' 20 | """ 21 | if protocol == 'soap': 22 | ws_part = 'api/?wsdl' 23 | else: 24 | ws_part = 'index.php/api/xmlrpc' 25 | return url.endswith('/') and url + ws_part or url + '/' + ws_part 26 | 27 | -------------------------------------------------------------------------------- /magento/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | version 4 | 5 | Just the version here to avoid cyclic dependency when 6 | installing the module 7 | 8 | :copyright: © 2013 by Openlabs Technologies & Consulting (P) Limited 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | VERSION = '1.0' 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | ''' 4 | Magento API 5 | 6 | :copyright: (c) 2010 by Sharoon Thomas. 7 | :copyright: (c) 2010-2013 by Openlabs Technologies & Consulting (P) LTD 8 | :license: AGPLv3, see LICENSE for more details 9 | 10 | ''' 11 | import os 12 | from setuptools import setup 13 | 14 | execfile(os.path.join('magento', 'version.py')) 15 | 16 | setup( 17 | name = 'magento', 18 | version=VERSION, 19 | url='https://github.com/openlabs/magento/', 20 | license='GNU Affero General Public License v3', 21 | author='Sharoon Thomas, Openlabs Technologies', 22 | author_email='info@openlabs.co.in', 23 | description='Magento Core API Client', 24 | long_description=open('README.rst').read(), 25 | packages=['magento'], 26 | zip_safe=False, 27 | platforms='any', 28 | install_requires=[ 29 | 'suds>=0.3.9', 30 | ], 31 | classifiers=[ 32 | 'Development Status :: 6 - Mature', 33 | 'Environment :: Web Environment', 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: GNU Affero General Public License v3', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python', 38 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 39 | 'Topic :: Software Development :: Libraries :: Python Modules' 40 | ], 41 | ) 42 | 43 | --------------------------------------------------------------------------------