├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── dependabot-automerge.yml │ └── python-publish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── Makefile ├── _static │ └── css │ │ └── custom.css ├── api │ ├── products.rst │ ├── tracking.rst │ └── transactions.rst ├── conf.py ├── credentials.rst ├── credentials │ ├── config.rst │ ├── howto.rst │ └── storing.rst ├── disclaimer.rst ├── example.py ├── index.rst ├── installation.rst ├── requirements.txt └── resources.rst ├── python_paypal_api ├── __init__.py ├── api │ ├── __init__.py │ ├── disputes.py │ ├── identity.py │ ├── invoices.py │ ├── orders.py │ ├── partner_referrals.py │ ├── products.py │ ├── tracking.py │ └── transactions.py ├── auth │ ├── __init__.py │ ├── access_token_client.py │ ├── access_token_response.py │ ├── credentials.py │ └── exceptions.py ├── base │ ├── __init__.py │ ├── api_response.py │ ├── base_client.py │ ├── client.py │ ├── config.py │ ├── credential_provider.py │ ├── enum.py │ ├── exceptions.py │ ├── helpers.py │ └── utils.py └── version.py ├── setup.py └── test ├── test_auth.py └── test_create_token_file.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: denisneuf 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ["https://PayPal.Me/denisneuf"] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 2 9 | directory: "/" # Location of package manifests 10 | target-branch: "main" 11 | schedule: 12 | interval: "daily" 13 | assignees: 14 | - "denisneuf" 15 | commit-message: 16 | prefix: "DEP" 17 | include: "scope" 18 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | name: auto-merge 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | auto-merge: 8 | runs-on: ubuntu-latest 9 | 10 | if: | 11 | (github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]') || 12 | (github.event_name != 'pull_request_target' && github.actor != 'dependabot[bot]') 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 16 | with: 17 | target: minor 18 | github-token: ${{ secrets.MY_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.9' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | [Oo]bj/ 5 | [Bb]in/ 6 | 7 | # Other files and folders 8 | .settings/ 9 | 10 | # Executables 11 | *.swf 12 | *.air 13 | *.ipa 14 | *.apk 15 | 16 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 17 | # should NOT be excluded as they contain compiler settings and other important 18 | # information for Eclipse / Flash Builder. 19 | 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Python Paypal Api is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Python Paypal Api to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open [Source/Culture/Tech] Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at Python Paypal Api events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. . 61 | 62 | 63 | 64 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 | 66 | ## 8. Addressing Grievances 67 | 68 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 69 | 70 | 71 | 72 | ## 9. Scope 73 | 74 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 75 | 76 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 77 | 78 | ## 10. Contact info 79 | 80 | 81 | 82 | ## 11. License and attribution 83 | 84 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 85 | 86 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 87 | 88 | _Revision 2.3. Posted 6 March 2017._ 89 | 90 | _Revision 2.2. Posted 4 February 2016._ 91 | 92 | _Revision 2.1. Posted 23 June 2014._ 93 | 94 | _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ... 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PYTHON-PAYPAL-API 2 | 3 | ![CodeQL](https://img.shields.io/github/v/release/denisneuf/python-paypal-api) 4 | [![Documentation Status](https://readthedocs.org/projects/python-paypal-api/badge/?version=latest)](https://python-paypal-api.readthedocs.io/en/latest/?badge=latest) 5 | 6 | ## Paypal's Rest API 7 | 8 | A python 3 wrapper to access Paypal Rest API with an easy-to-use interface. 9 | 10 | ### Install 11 | 12 | [![Badge](https://img.shields.io/pypi/v/python-paypal-api?style=for-the-badge)](https://pypi.org/project/python-paypal-api/) 13 | 14 | ``` 15 | pip install python-paypal-api 16 | ``` 17 | 18 | ### Donate 19 | 20 | If you find this project is useful consider donating or [sponsor](https://github.com/sponsors/denisneuf) it to keep on going on it, thank you. 21 | 22 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/donate?hosted_button_id=G3KB6M2G9YV9C) 23 | 24 | ![alt text](https://github.com/denisneuf/python-amazon-ad-api/blob/main/test/codigo-QR.png?raw=true) 25 | 26 | 27 | ### Overview 28 | 29 | You need obtain your own credentials with Paypal that may include a paypal personal or business account and access as developer. Please view the official [Paypal Developer](https://developer.paypal.com/home) 30 | 31 | You can also check how use the credentials on the [documentation](https://python-paypal-api.readthedocs.io/en/latest/credentials/howto.html) of this Python Paypal API. 32 | 33 | 34 | ### Environment Credentials 35 | ```python 36 | from python_paypal_api.api import Identity 37 | 38 | os.environ["client_id"] = "your-client-id" 39 | os.environ["client_secret"] = "your-client-secret" 40 | # os.environ["client_mode"] = "PRODUCTION" 41 | 42 | # Can omit client_mode if using SANDBOX 43 | 44 | result = Identity().get_userinfo() 45 | 46 | ``` 47 | 48 | 49 | ### Code Credentials 50 | You can use your credentials as follows passing it to the client as a dict. Please review the full [documentation](https://python-paypal-api.readthedocs.io/en/latest/credentials/howto.html) to see all posibilities to include your credentials. 51 | 52 | Python code 53 | 54 | ```python 55 | from python_paypal_api.api import Identity 56 | 57 | my_credentials = dict( 58 | client_id="your-client-id", 59 | client_secret="your-client-secret", 60 | client_mode="PRODUCTION" 61 | ) 62 | 63 | # Can omit client_mode to use SANDBOX 64 | 65 | result = Identity(credentials=my_credentials).get_userinfo() 66 | 67 | ``` 68 | 69 | ### YAML Credentials 70 | Use a config.yaml file with your credentials for more convenience and manage diferent accounts or profiles. You can store a Sandbox and Production (Live) credentials to comvenient switch from sandbox to live environment. 71 | Note: default credentials without client_mode will use SANDBOX paypal endpoint for testing 72 | 73 | Create a file config.yaml (From version 0.1.1 the file use the default name provided by confuse package and use template validation) 74 | 75 | Please review the full [documentation](https://python-paypal-api.readthedocs.io/en/latest/credentials/config.html) to see all posibilities to include in your configuration. 76 | 77 | ```javascript 78 | version: '1.0' 79 | 80 | configuration: 81 | 82 | production: 83 | client_id: 'your-client-id' 84 | client_secret: 'your-client-secret' 85 | client_mode: 'PRODUCTION' 86 | default: 87 | client_id: 'your-client-id-sandbox' 88 | client_secret: 'your-client-secret-sandbox' 89 | 90 | ``` 91 | 92 | Python code 93 | 94 | ```python 95 | from python_paypal_api.api import Identity 96 | 97 | # Leave empty will use the 'default' account 98 | result = Identity().get_userinfo() 99 | # will use germany account data 100 | result = Identity(credentials="production").get_userinfo() 101 | ``` 102 | 103 | 104 | 105 | ### Search path for config.yaml 106 | 107 | * macOS: ``~/.config/python-paypal-api`` and ``~/Library/Application Support/python-paypal-api`` 108 | * Other Unix: ``~/.config/python-paypal-api`` and ``/etc/python-paypal-api`` 109 | * Windows: ``%APPDATA%\python-paypal-api`` where the APPDATA environment variable falls back to ``%HOME%\AppData\Roaming`` if undefined 110 | 111 | 112 | [Confuse Help](https://confuse.readthedocs.io/en/latest/usage.html#search-paths) 113 | 114 | 115 | ### Managing obtained credentials 116 | 117 | By default the package will store it in cache to use the LRU Cache from cachetools but the cache will be available only during the script living environment, so once you get the token, any call will use the cached token but since the script terminates the cached key will be gone. 118 | 119 | There is a way to create a 600 permissions file in the configuration search path. This is because the token obtained it will ve valid for 32400 seconds and storing it will reduce the calls to the oauth paypal endpoint. 120 | The token also can be stored encrypted, for complex configurations read the [Python Paypal API Help](https://python-paypal-api.readthedocs.io/en/latest/credentials/storing.html). 121 | 122 | 123 | 124 | ```python 125 | from python_paypal_api.api import Identity 126 | from python_paypal_api.base import PaypalApiException 127 | import logging 128 | 129 | try: 130 | 131 | result = Identity(store_credentials=True).get_userinfo() 132 | logging.info(result) 133 | 134 | except PaypalApiException as error: 135 | logging.error(error) 136 | ``` 137 | 138 | 139 | 140 | ### Exceptions 141 | 142 | You can use a [try](https://docs.python.org/3.10/reference/compound_stmts.html#try) except statement when you call the API and catch exceptions if some problem ocurred: 143 | 144 | ```python 145 | from python_paypal_api.api import Identity 146 | from python_paypal_api.base import PaypalApiException 147 | import logging 148 | 149 | try: 150 | 151 | result = Identity().get_userinfo() 152 | logging.info(result) 153 | 154 | except PaypalApiException as error: 155 | logging.error(error) 156 | ``` 157 | 158 | ### Debug 159 | 160 | Use debug=True if you want see some logs like the header you submit to the api endpoint, the method and path used among the params and the data submitted if any, to trace possible errors. 161 | 162 | ```python 163 | from python_paypal_api.api import Identity, 164 | from python_paypal_api.base import PaypalApiException 165 | import logging 166 | 167 | try: 168 | 169 | result = Identity(debug=True).get_userinfo() 170 | logging.info(result) 171 | 172 | except PaypalApiException as error: 173 | logging.error(error) 174 | ``` 175 | 176 | ### Paypal Current Resources 177 | * [Catalog](https://python-paypal-api.readthedocs.io/en/latest/api/products.html) 178 | * Disputes 179 | * Identity 180 | * Invoices 181 | * Orders 182 | * Partner Referral 183 | * [Tracking](https://python-paypal-api.readthedocs.io/en/latest/api/tracking.html) 184 | * [Transactions](https://python-paypal-api.readthedocs.io/en/latest/api/transactions.html) 185 | 186 | 187 | ### API NOTICE 188 | 189 | This API is based on the [API Client](https://github.com/saleweaver/rapid_rest_client) created by [@saleweaver](https://github.com/saleweaver) but adapted to paypal auth requeriments and improved system for token call 190 | 191 | ### DISCLAIMER 192 | 193 | We are not affiliated with PayPal 194 | 195 | ### LICENSE 196 | 197 | ![License](https://img.shields.io/badge/license-apache-green) 198 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = autodoc-example 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Has to be explicit, otherwise we don't get "make" without targets right. 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | # You can add custom targets here. 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /*@import 'theme.css';*/ 2 | .strike { 3 | text-decoration: line-through; 4 | } 5 | 6 | .dax-def-type { 7 | color: #6c7378; 8 | } 9 | 10 | .dax-def-note { 11 | color: #f08d00 12 | } 13 | 14 | .dax-operation-get { 15 | color: #009cde; 16 | } 17 | 18 | .dax-operation-path { 19 | color: #555; 20 | font-family: bt_mono, Courier New, monospace; 21 | font-size: .9375rem; 22 | font-weight: 700; 23 | margin-left: .5rem; 24 | padding:.2rem .25rem 25 | } 26 | .dax-def-meta{ 27 | color: #6c7378; 28 | font-weight: 400; 29 | } -------------------------------------------------------------------------------- /docs/api/products.rst: -------------------------------------------------------------------------------- 1 | Catalog Products API 2 | ==================== 3 | 4 | .. role:: dax-def-type 5 | :class: dax-def-type 6 | .. role:: dax-def-note 7 | :class: dax-def-note 8 | .. role:: dax-operation-get 9 | :class: dax-operation-get 10 | .. role:: dax-operation-path 11 | :class: dax-operation-path 12 | .. role:: dax-def-meta 13 | :class: dax-def-meta 14 | 15 | 16 | **Catalog Products API** 17 | 18 | Merchants can use the Catalog Products API to create products, which are goods and services. 19 | 20 | .. autoclass:: python_paypal_api.api.Products 21 | 22 | .. autofunction:: python_paypal_api.api.Products.list_products 23 | 24 | .. autofunction:: python_paypal_api.api.Products.get_product 25 | 26 | .. autofunction:: python_paypal_api.api.Products.update_product 27 | 28 | .. autofunction:: python_paypal_api.api.Products.update_product_helper 29 | 30 | .. autofunction:: python_paypal_api.api.Products.post_product 31 | 32 | .. autofunction:: python_paypal_api.api.Products.post_product_helper -------------------------------------------------------------------------------- /docs/api/tracking.rst: -------------------------------------------------------------------------------- 1 | Add Tracking API 2 | ================ 3 | 4 | .. _Add Tracking API Overview: https://developer.paypal.com/docs/tracking/ 5 | .. _Add Tracking API Integration Guide: https://developer.paypal.com/docs/tracking/integrate/ 6 | .. _aCommerce: https://www.acommerce.asia/ 7 | .. _Australian Postal Corporation: https://auspost.com.au/ 8 | .. _DPEX Worldwide: https://dpex.com/ 9 | .. _Pošta Srbije: https://www.posta.rs/default-eng.asp 10 | .. _Ukrposhta - Ukraine's National Post: https://ukrposhta.ua/en/vidslidkuvati-forma-poshuku 11 | .. _Buylogic: https://www.aftership.com/couriers/buylogic 12 | .. _General Logistics Systems (GLS) France: https://gls-group.eu/ 13 | .. _Pos Malaysia: https://www.pos.com.my/ 14 | .. _Bulgarian Post: https://www.bgpost.bg/ 15 | .. _Delhivery India: https://www.delhivery.com/ 16 | .. _APC Overnight UK: https://apc-overnight.com/ 17 | .. _Nacex Spain: https://www.nacex.es/cambiarIdioma.do?idioma=EN 18 | .. _Yodel UK: https://www.yodel.co.uk/ 19 | .. _DHL Global Forwarding Poland: https://www.logistics.dhl/pl-en/home/our-divisions/global-forwarding.html 20 | .. _CTT Expresso Portugal: https://www.cttexpresso.pt/home/ 21 | .. _PostNL Netherlands: https://www.postnl.com 22 | .. _Posten Norge: https://www.posten.no/en/ 23 | .. _Nim Express: https://www.nimexpress.com/web/p/ 24 | .. _Nova Poshta: https://novaposhta.ua/en 25 | .. _Redur Spain: https://www.redur.es/ 26 | .. _Emirates Post Group: https://www.epg.gov.ae/_en/index.xhtml 27 | .. _Yanwen Express: https://www.yw56.com.cn/ 28 | .. _CJ Logistics in Thailand: https://www.cjlogistics.com/en/network/th-th 29 | .. _Sociedad Estatal Correos y Telégrafos: https://www.correos.es/ss/Satellite/site/pagina-inicio/info 30 | .. _Blue Dart Express DHL: https://www.bluedart.com/ 31 | .. _Mex Post Correos de Mexico: https://www.correosdemexico.com.mx/Paginas/Inicio.asp/ 32 | .. _SFC Logistics: https://www.sfcservice.com/ 33 | .. _CBL Logística: https://www.cbl-logistica.com 34 | .. _DHL eCommerce US: https://www.logistics.dhl/us-en/home/tracking/tracking-ecommerce.html 35 | .. _AXL Express & Logistics: https://couriertrackingrobo.com/track/company-details.php?id=axl 36 | .. _United States Postal Service (USPS): https://www.usps.com/ 37 | .. _OCA Argentia: https://www.oca.com.ar/ 38 | .. _Chunghwa Post: https://www.post.gov.tw/post/internet/index.jsp 39 | .. _United Parcel Service of America, Inc.: https://www.ups.com/us/en/global.page 40 | .. _Toll: https://www.tollgroup.com/ 41 | .. _BH POŠTA: https://www.posta.ba/en/home-v2/ 42 | .. _PostNord Sverige: https://www.postnord.se/ 43 | .. _Canada Post: https://www.canadapost.ca/cpc/en/home.page 44 | .. _AddressPay UK: https://addresspal.anpost.ie/ 45 | .. _Seur Portugal: https://www.seur.com/pt/ 46 | .. _Correo Argentino: https://www.correoargentino.com.ar/ 47 | .. _Estafeta Mexico: https://www.estafeta.com/ 48 | .. _SRE Korea: https://www.srekorea.co.kr/home/ 49 | .. _Cyprus Post: https://www.cypruspost.post/en/home 50 | .. _CourierPost New Zealand: https://www.courierpost.co.nz/track/track-and-trace/ 51 | .. _General Logistics Systems (GLS) Germany: https://gls-group.eu/ 52 | .. _General Logistics Systems (GLS) Netherlands: https://gls-group.eu/ 53 | .. _TNT Italy: https://www.tnt.it/ 54 | .. _Bert France: https://bert.fr/ 55 | .. _ADSone Cumulus: https://www.adsone.com.au/ 56 | .. _Sociedad Estatal Correos y Telégrafos: https://www.correos.es/ss/Satellite/site/pagina-inicio/info 57 | .. _Nigerian Postal Service: https://www.nipost.gov.ng// 58 | .. _Direct Link Sweden: https://www.directlink.com/ 59 | .. _Česká pošta: https://www.ceskaposta.cz/en/ 60 | .. _Fastway Couriers (South Africa): http://www.fastway.co.za/ 61 | .. _DB Schenker Sweden: https://www.dbschenker.com/se-sv 62 | .. _bpost: https://parcel.bpost.be/ 63 | .. _Relais Colis: https://www.relaiscolis.com/ 64 | .. _CorreosChile: https://www.correos.cl/ 65 | .. _SDA Express Courier: https://www.sda.it/wps/portal/sdait.home/ 66 | .. _General Logistics Systems (GLS) Italy: https://gls-group.eu/ 67 | .. _BRT Corriere Espresso Italy: https://www.brt.it/it/home 68 | .. _CollectPlus UK: https://www.collectplus.co.uk/ 69 | .. _Internationational Seur Portugal: https://www.seur.com/en/ 70 | .. _Parcel Monitor Spain: https://www.parcelmonitor.com/track-spain/ 71 | .. _Japan Post: https://www.post.japanpost.jp/index_en.html 72 | .. _Hrvatska Pošta: https://www.posta.hr/ 73 | .. _Magyar Posta: https://www.posta.hu/ 74 | .. _Enterprise Freight Systems (EFS): https://www.efstrans.com/ 75 | .. _Poșta Română: https://www.posta-romana.ro/en 76 | .. _New Zealand Post Limited (NZ): https://www.nzpost.co.nz/tools/tracking 77 | .. _UK Mail: https://www.ukmail.com/ 78 | .. _Correos de Costa Rica: https://www.correos.go.cr/ 79 | .. _DotZot India: https://dotzot.in/ 80 | .. _Courier Plus Nigeria: https://www.courierplus-ng.com/ 81 | .. _Rocket Parcel International: https://www.rocketparcel.com/ 82 | .. _The Courier Guy: https://www.thecourierguy.co.za/ 83 | .. _JP RAM Shipping: https://www.jpshipping.co.uk/services/personal_effects 84 | .. _Internet date and time format: https://tools.ietf.org/html/rfc3339#section-5.6 85 | .. _link_description: https://developer.paypal.com/docs/api/tracking/v1/#definition-link_description 86 | .. _HATEOAS links: https://developer.paypal.com/api/rest/responses/#hateoas-links 87 | .. _Add tracking information with tracking numbers: https://developer.paypal.com/docs/tracking/integrate/#add-tracking-information-with-tracking-numbers 88 | .. _Add tracking information without tracking numbers: https://developer.paypal.com/docs/tracking/integrate/#add-tracking-information-without-tracking-numbers 89 | .. _tracker: https://developer.paypal.com/docs/api/tracking/v1/#definition-tracker 90 | 91 | 92 | .. role:: dax-def-type 93 | :class: dax-def-type 94 | .. role:: dax-def-note 95 | :class: dax-def-note 96 | .. role:: dax-operation-get 97 | :class: dax-operation-get 98 | .. role:: dax-operation-path 99 | :class: dax-operation-path 100 | .. role:: dax-def-meta 101 | :class: dax-def-meta 102 | 103 | **Add Tracking API** 104 | 105 | Merchants can use the PayPal Add Tracking API to manage tracking information. Merchants can add tracking numbers and associated information to PayPal. After adding these details to PayPal, merchants can: 106 | 107 | * Update tracking details. 108 | * Show tracking details. 109 | * Cancel tracking numbers. 110 | 111 | For more information, see the `Add Tracking API Overview`_ and `Add Tracking API Integration Guide`_. 112 | 113 | .. autoclass:: python_paypal_api.api.Tracking 114 | 115 | .. autofunction:: python_paypal_api.api.Tracking.put_tracking 116 | 117 | ### Example python 118 | 119 | .. code-block:: python 120 | 121 | from python_paypal_api.api import Tracking 122 | from python_paypal_api.base import ( 123 | ShipmentStatus, 124 | Carrier, 125 | TrackingNumberType, 126 | PaypalApiException 127 | ) 128 | import logging 129 | 130 | def py_put_tracking(composed_id: str, dictionary: dict): 131 | 132 | logger.info("---------------------------------") 133 | logger.info("Tracking > put_tracking(%s)" % str(dictionary)) 134 | logger.info("---------------------------------") 135 | 136 | try: 137 | 138 | result = Tracking(debug=True).put_tracking( 139 | id=composed_id, 140 | body=dictionary 141 | ) 142 | logger.info(result) 143 | 144 | except PaypalApiException as error: 145 | logger.error(error) 146 | 147 | 148 | if __name__ == '__main__': 149 | logger = logging.getLogger("test") 150 | 151 | id_transaction = "9ST00334VA8626***" 152 | number_tracking = "443844607820" 153 | 154 | path = "{}-{}".format(id_transaction, number_tracking) 155 | 156 | update = \ 157 | { 158 | "transaction_id": id_transaction, 159 | "tracking_number": number_tracking, 160 | "tracking_number_type": TrackingNumberType.CARRIER_PROVIDED.value, 161 | "status": ShipmentStatus.ON_HOLD.value, 162 | "carrier": Carrier.FEDEX.value, 163 | "notify_buyer": True 164 | } 165 | 166 | py_put_tracking(path, update) 167 | 168 | ### Response 169 | 170 | A successful request returns the HTTP 204 OK status code with no JSON response body. 171 | 172 | .. code-block:: javascript 173 | 174 | 204 No Content 175 | 176 | .. autofunction:: python_paypal_api.api.Tracking.get_tracking 177 | 178 | ### Example python 179 | 180 | .. code-block:: python 181 | 182 | from python_paypal_api.api import Tracking 183 | from python_paypal_api.base import ( 184 | ShipmentStatus, 185 | Carrier, 186 | TrackingNumberType, 187 | PaypalApiException 188 | ) 189 | import logging 190 | 191 | def py_get_tracking(composed_id: str): 192 | 193 | logger.info("---------------------------------") 194 | logger.info("Tracking > get_tracking(%s)" % str(composed_id)) 195 | logger.info("---------------------------------") 196 | 197 | try: 198 | 199 | result = Tracking(debug=True).get_tracking( 200 | id=composed_id 201 | ) 202 | logger.info(result) 203 | 204 | except PaypalApiException as error: 205 | logger.error(error) 206 | 207 | if __name__ == '__main__': 208 | logger = logging.getLogger("test") 209 | 210 | id_transaction = "9ST00334VA8626***" 211 | number_tracking = "443844607820" 212 | 213 | path = "{}-{}".format(id_transaction, number_tracking) 214 | 215 | py_get_tracking(path) 216 | 217 | ### Response JSON 218 | 219 | A successful request returns the HTTP 200 OK status code and a JSON response body that shows tracking information. 220 | 221 | .. code-block:: javascript 222 | 223 | { 224 | "transaction_id": "8MC585209K746392H", 225 | "tracking_number": "443844607820", 226 | "status": "SHIPPED", 227 | "carrier": "FEDEX", 228 | "links": [ 229 | { 230 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/8MC585209K746392H-443844607820", 231 | "rel": "self" 232 | }, 233 | { 234 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/8MC585209K746392H-443844607820", 235 | "rel": "replace", 236 | "method": "PUT" 237 | }, 238 | { 239 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers-batch", 240 | "rel": "create", 241 | "method": "POST" 242 | } 243 | ] 244 | } 245 | 246 | 247 | .. autofunction:: python_paypal_api.api.Tracking.post_tracking 248 | 249 | ### Example python 250 | 251 | .. code-block:: python 252 | 253 | from python_paypal_api.api import Tracking 254 | from python_paypal_api.base import ( 255 | ShipmentStatus, 256 | Carrier, 257 | TrackingNumberType, 258 | PaypalApiException 259 | ) 260 | import logging 261 | 262 | def py_post_tracking(dictionary: dict): 263 | 264 | logging.info("---------------------------------") 265 | logging.info("Tracking > post_tracking(%s)" % str(dictionary)) 266 | logging.info("---------------------------------") 267 | 268 | try: 269 | 270 | result = Tracking(debug=True).post_tracking( 271 | body=dictionary 272 | ) 273 | logging.info(result) 274 | 275 | except PaypalApiException as error: 276 | logging.error(error) 277 | 278 | 279 | if __name__ == '__main__': 280 | logger = logging.getLogger("test") 281 | 282 | id_transaction = "9ST00334VA8626***" 283 | number_tracking = "443844607820" 284 | 285 | trackers = { 286 | "trackers": [ 287 | { 288 | "transaction_id": id_transaction, 289 | "tracking_number": number_tracking, 290 | "status": "SHIPPED", 291 | "carrier": "FEDEX" 292 | } 293 | ] 294 | } 295 | 296 | py_post_tracking(trackers) 297 | 298 | ### Response JSON 299 | 300 | A successful request returns the HTTP 200 OK status code and a JSON response body that shows tracking information. 301 | 302 | .. code-block:: javascript 303 | 304 | { 305 | "tracker_identifiers": 306 | [ 307 | { 308 | "transaction_id": "8MC585209K746392H", 309 | "tracking_number": "443844607820", 310 | "links": [ 311 | { 312 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/8MC585209K746392H-443844607820", 313 | "rel": "self", 314 | "method": "GET" 315 | }, 316 | { 317 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/8MC585209K746392H-443844607820", 318 | "rel": "replace", 319 | "method": "PUT" 320 | } 321 | ] 322 | }, 323 | { 324 | "transaction_id": "53Y56775AE587553X", 325 | "tracking_number": "443844607821", 326 | "links": 327 | [ 328 | { 329 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/53Y56775AE587553X-443844607821", 330 | "rel": "self", 331 | "method": "GET" 332 | }, 333 | { 334 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers/53Y56775AE587553X-443844607821", 335 | "rel": "replace", 336 | "method": "PUT" 337 | } 338 | ] 339 | } 340 | ], 341 | "errors": 342 | [ 343 | { 344 | "name": "RESOURCE_NOT_FOUND", 345 | "debug_id": "46735c7461f3d", 346 | "message": "The specified resource does not exist.", 347 | "details": 348 | [ 349 | { 350 | "field": "/trackers/0/transaction_id", 351 | "value": "8MC585309K746392H", 352 | "location": "body", 353 | "issue": "INVALID_TRANSACTION_ID" 354 | } 355 | ] 356 | } 357 | ], 358 | "links": 359 | [ 360 | { 361 | "href": "https://api-m.sandbox.paypal.com/v1/shipping/trackers-batch", 362 | "rel": "self", 363 | "method": "POST" 364 | } 365 | ] 366 | } 367 | 368 | -------------------------------------------------------------------------------- /docs/api/transactions.rst: -------------------------------------------------------------------------------- 1 | Transaction Search API 2 | ====================== 3 | 4 | .. role:: dax-def-type 5 | :class: dax-def-type 6 | .. role:: dax-def-note 7 | :class: dax-def-note 8 | .. role:: dax-operation-get 9 | :class: dax-operation-get 10 | .. role:: dax-operation-path 11 | :class: dax-operation-path 12 | .. role:: dax-def-meta 13 | :class: dax-def-meta 14 | 15 | 16 | 17 | 18 | .. _Internet date and time format: https://tools.ietf.org/html/rfc3339#section-5.6 19 | 20 | .. _three-character ISO-4217 currency code: https://developer.paypal.com/api/rest/reference/currency-codes/ 21 | 22 | .. _Transaction event codes: https://developer.paypal.com/docs/integration/direct/transaction-search/transaction-event-codes/ 23 | 24 | 25 | **Transaction Search API** 26 | 27 | Use the Transaction Search API to get the history of transactions for a PayPal account. To use the API on behalf of third parties, you must be part of the PayPal partner network. Reach out to your partner manager for the next steps. To enroll in the partner program, see `Partner with PayPal`_. For more information about the API, see the `Transaction Search API Integration Guide`_. 28 | 29 | 30 | .. _Transaction Search API Integration Guide: https://developer.paypal.com/docs/transaction-search/ 31 | 32 | 33 | .. note:: 34 | 35 | Note: To use the API on behalf of third parties, you must be part of the PayPal partner network. Reach out to your partner manager for the next steps. To enroll in the partner program, see `Partner with PayPal`_. 36 | 37 | .. _Partner with PayPal: https://www.paypal.com/my/webapps/mpp/partner-program/global-programs?_ga=1.72234320.217415639.1675992033 38 | 39 | .. autoclass:: python_paypal_api.api.Transactions 40 | 41 | .. autofunction:: python_paypal_api.api.Transactions.get_list_transactions 42 | 43 | ### Example python 44 | 45 | .. code-block:: python 46 | 47 | from python_paypal_api.api import Transactions 48 | from python_paypal_api.base import PaypalApiException 49 | import logging 50 | from datetime import datetime, timezone 51 | 52 | def py_get_list_transactions(**kwargs): 53 | 54 | logger.info("---------------------------------") 55 | logger.info("Transactions > py_get_list_transactions({})".format(kwargs)) 56 | logger.info("---------------------------------") 57 | 58 | try: 59 | 60 | result = Transactions(debug=True).get_list_transactions( 61 | **kwargs 62 | ) 63 | logger.info(result) 64 | 65 | except PaypalApiException as error: 66 | logger.error(error) 67 | 68 | 69 | if __name__ == '__main__': 70 | 71 | logger = logging.getLogger("test") 72 | date_start = datetime(2023, 2, 1, 0, 0, 0, 0, timezone.utc).isoformat() 73 | date_end = datetime(2023, 3, 1, 0, 0, 0, 0, timezone.utc).isoformat() 74 | 75 | py_get_list_transactions( 76 | page_size=1, 77 | start_date=date_start, 78 | end_date=date_end 79 | ) 80 | 81 | 82 | 83 | ### Response JSON 84 | 85 | A successful request returns the HTTP 200 OK status code and a JSON response body that lists transactions. 86 | 87 | .. code-block:: javascript 88 | 89 | { 90 | 'account_number': '2LBUCGLCSB***', 91 | 'end_date': '2023-03-01T00:00:00+0000', 92 | 'last_refreshed_datetime': '2023-03-12T01:59:59+0000', 93 | 'links': [ 94 | { 95 | 'href': 'https://api.sandbox.paypal.com/v1/reporting/transactions?start_date=2023-02-01T00%3A00%3A00%2B00%3A00&end_date=2023-03-01T00%3A00%3A00%2B00%3A00&page_size=1&page=19', 96 | 'method': 'GET', 97 | 'rel': 'last' 98 | }, 99 | { 100 | 'href': 'https://api.sandbox.paypal.com/v1/reporting/transactions?start_date=2023-02-01T00%3A00%3A00%2B00%3A00&end_date=2023-03-01T00%3A00%3A00%2B00%3A00&page_size=1&page=2', 101 | 'method': 'GET', 102 | 'rel': 'next' 103 | }, 104 | { 105 | 'href': 'https://api.sandbox.paypal.com/v1/reporting/transactions?start_date=2023-02-01T00%3A00%3A00%2B00%3A00&end_date=2023-03-01T00%3A00%3A00%2B00%3A00&page_size=1&page=1', 106 | 'method': 'GET', 107 | 'rel': 'self' 108 | } 109 | ], 110 | 'page': 1, 111 | 'start_date': '2023-02-01T00:00:00+0000', 112 | 'total_items': 19, 113 | 'total_pages': 19, 114 | 'transaction_details': [ 115 | { 116 | 'transaction_info': 117 | { 118 | 'available_balance': 119 | { 120 | 'currency_code': 'EUR', 121 | 'value': '165488.69' 122 | }, 123 | 'custom_field': 'S1121674440646-1563665460', 124 | 'ending_balance': 125 | { 126 | 'currency_code': 'EUR', 127 | 'value': '165488.69' 128 | }, 129 | 'paypal_account_id': 'UYZPMCTQDV***', 130 | 'paypal_reference_id': '4G8384171L5786***', 131 | 'paypal_reference_id_type': 'TXN', 132 | 'protection_eligibility': '02', 133 | 'transaction_amount': 134 | { 135 | 'currency_code': 'EUR', 136 | 'value': '-50.00' 137 | }, 138 | 'transaction_event_code': 'T1110', 139 | 'transaction_id': '18670957S08679***', 140 | 'transaction_initiation_date': '2023-02-12T11:52:33+0000', 141 | 'transaction_status': 'P', 142 | 'transaction_updated_date': '2023-02-12T11:52:33+0000' 143 | } 144 | } 145 | ] 146 | } 147 | 148 | .. autofunction:: python_paypal_api.api.Transactions.get_balances 149 | 150 | 151 | ### Example python 152 | 153 | .. code-block:: python 154 | 155 | from python_paypal_api.api import Transactions 156 | from python_paypal_api.base import PaypalApiException 157 | import logging 158 | from datetime import datetime, timezone 159 | 160 | def py_get_balances(**kwargs): 161 | 162 | logger.info("---------------------------------") 163 | logger.info("Transactions > py_get_balances({})".format(kwargs))) 164 | logger.info("---------------------------------") 165 | 166 | try: 167 | 168 | result = Transactions(debug=True).get_balances( 169 | **kwargs 170 | ) 171 | logger.info(result) 172 | 173 | except PaypalApiException as error: 174 | logger.error(error) 175 | 176 | 177 | if __name__ == '__main__': 178 | 179 | logger = logging.getLogger("test") 180 | date_start = datetime(2023, 2, 1, 0, 0, 0, 0, timezone.utc).isoformat() 181 | py_get_balances( 182 | as_of_time=date_start, 183 | currency_code="USD" 184 | ) 185 | 186 | 187 | 188 | 189 | ### Response JSON 190 | 191 | A successful request returns the HTTP 200 OK status code and a JSON response body that lists balances. 192 | 193 | .. code-block:: javascript 194 | 195 | { 196 | 'account_id': '2LBUCGLCSB***', 197 | 'as_of_time': '2023-03-12T01:59:59Z', 198 | 'balances': [ 199 | { 200 | 'available_balance': 201 | { 202 | 'currency_code': 'USD', 203 | 'value': '1304.05' 204 | }, 205 | 'currency': 'USD', 206 | 'total_balance': 207 | { 208 | 'currency_code': 'USD', 209 | 'value': '1304.05' 210 | }, 211 | 'withheld_balance': 212 | { 213 | 'currency_code': 'USD', 214 | 'value': '0.00' 215 | } 216 | } 217 | ], 218 | 'last_refresh_time': '2023-03-12T01:59:59Z' 219 | } 220 | 221 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import sys 8 | sys.path.insert(0, os.path.abspath('..')) 9 | 10 | # -- Project information ----------------------------------------------------- 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 12 | 13 | project = 'Python Paypal Api' 14 | copyright = '2023, Daniel Alvaro' 15 | author = 'Daniel Alvaro' 16 | release = '0.1.1' 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | 'sphinx.ext.autodoc', 'sphinx_rtd_theme' 23 | ] 24 | 25 | templates_path = ['_templates'] 26 | exclude_patterns = [] 27 | 28 | 29 | 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 33 | 34 | # html_theme = 'alabaster' 35 | html_theme = 'sphinx_rtd_theme' 36 | html_static_path = ['_static'] 37 | # html_style = 'css/custom.css' 38 | html_css_files = [ 39 | 'css/custom.css', 40 | ] 41 | 42 | html_theme_options = { 43 | "collapse_navigation": False 44 | } 45 | -------------------------------------------------------------------------------- /docs/credentials.rst: -------------------------------------------------------------------------------- 1 | Credentials 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | 8 | credentials/howto 9 | credentials/storing 10 | -------------------------------------------------------------------------------- /docs/credentials/config.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _From Config File: 4 | 5 | ==================== 6 | From Config File 7 | ==================== 8 | 9 | .. role:: dax-def-type 10 | :class: dax-def-type 11 | 12 | .. role:: dax-def-note 13 | :class: dax-def-note 14 | 15 | An example config file is provided in this repository, it supports multiple accounts. 16 | The confuse will search the system for a config file named `config.yaml`_ 17 | 18 | The config is parsed by `confused`_, see their docs for more in depth information. 19 | Search paths are: 20 | 21 | * macOS: ``~/.config/python-paypal-api`` and ``~/Library/Application Support/python-paypal-api`` 22 | * Other Unix: ``~/.config/python-paypal-api`` and ``/etc/python-paypal-api`` 23 | * Windows: ``%APPDATA%\python-paypal-api`` where the APPDATA environment variable falls back to ``%HOME%\AppData\Roaming`` if undefined 24 | 25 | If you're only using one account, place it under default. You can pass the account's name to the client to use any other account used in the `config.yaml`_ file. 26 | 27 | .. code-block:: yaml 28 | 29 | version: '1.0' 30 | 31 | configuration: 32 | 33 | production: 34 | client_id: 'your-client-id' 35 | client_secret: 'your-client-secret' 36 | client_mode: 'PRODUCTION' 37 | default: 38 | client_id: 'your-client-id-sandbox' 39 | client_secret: 'your-client-secret-sandbox' 40 | 41 | 42 | 43 | Cache Store 44 | =========== 45 | 46 | The python paypal api will use cachetools by default to store the token. 47 | 48 | 49 | Usage with default account 50 | -------------------------- 51 | 52 | .. code-block:: python 53 | 54 | Identity().get_userinfo() 55 | 56 | 57 | Usage with another account 58 | -------------------------- 59 | 60 | You can use every account's name from the config file for account 61 | 62 | .. code-block:: python 63 | 64 | Identity(credentials="production").get_userinfo() 65 | 66 | 67 | 68 | File Store 69 | ========== 70 | 71 | Next examples shows how to combine the ``store_credentials`` and ``safe`` kwargs to store the token in a file, encrypted or not: 72 | 73 | 74 | Usage saving the token 75 | ---------------------- 76 | 77 | .. code-block:: python 78 | 79 | Identity(store_credentials=True).get_userinfo() 80 | 81 | 82 | Usage saving encrypted token 83 | ---------------------------- 84 | 85 | .. code-block:: python 86 | 87 | Identity(store_credentials=True, safe=True).get_userinfo() 88 | 89 | 90 | 91 | Custom Configurations 92 | ===================== 93 | 94 | You can provide your own paths to the config.yaml instead of the default configuration search paths, even provide your own file name. 95 | It is possible too to determine your own key to encrypt the token, or provide a path and name to store the key safely in a folder you determine. 96 | Please be aware that the configuration files must follow the requirements to avoid different configurations in the same account. 97 | For example change the path or the key value itself must lead that the .token file could not be read. 98 | If you commit some error and the combination between key and token is lost, remove the token file (and the key if you want generate a new one) an run again to recreate the token. 99 | 100 | 101 | Custom Credentials 102 | ------------------ 103 | 104 | The custom ``credentials`` need to be passed as a list: 105 | 106 | 107 | **credentials** :dax-def-type:`list` 108 | 109 | 110 | 111 | list[0] = account :dax-def-type:`str` :dax-def-note:`required` 112 | 113 | The name of the account that will be used in the yaml configuration file. 114 | 115 | list[1] = path :dax-def-type:`str` :dax-def-note:`required` 116 | 117 | The path to the folder where the configuration yaml file while be stored. 118 | 119 | list[2] = name :dax-def-type:`str` :dax-def-note:`optional` 120 | 121 | Optional the name of the file where the configuration is stored. If no name is provided or the file is missing will try config.yaml as default. 122 | 123 | 124 | ### Example python with required parameters 125 | 126 | .. code-block:: python 127 | 128 | account = "default" 129 | 130 | path = "/Users/your-user/Desktop/python_paypal_api/credentials" 131 | 132 | # It will find a config.yaml file in the folder provided 133 | 134 | custom_credentials = [account, path] 135 | 136 | Identity(credentials=custom_credentials).get_userinfo() 137 | 138 | 139 | ### Example python with all parameters 140 | 141 | .. code-block:: python 142 | 143 | account = "default" 144 | 145 | path = "/Users/your-user/Desktop/python_paypal_api/credentials" 146 | 147 | name = "test.yaml" 148 | 149 | custom_credentials = [account, path, name] 150 | 151 | Identity(credentials=custom_credentials).get_userinfo() 152 | 153 | 154 | Custom Store Credentials 155 | ------------------------ 156 | 157 | The custom ``store_credentials`` need to be passed as a dict: 158 | 159 | **credentials** :dax-def-type:`dict` 160 | 161 | | **safe** :dax-def-type:`bool` :dax-def-note:`required` 162 | 163 | | ``True`` will store the token encrypted. 164 | 165 | | Requires: **token_path** and **key_path_name** or **key_value**. 166 | 167 | | ``False`` will store the token in json format. 168 | 169 | | Require: **token_path**. 170 | 171 | Exclude: **key_path_name** and **key_value**. 172 | 173 | | **token_path** :dax-def-type:`str` :dax-def-note:`required` 174 | 175 | | The path to the folder where the token will be stored, if the folder doesn't exist it will be created. 176 | 177 | | **key_path_name** :dax-def-type:`str` :dax-def-note:`optional` 178 | 179 | | The path to the folder where the key will be stored, if the folder doesn't exist it will be created. 180 | 181 | | Require: **safe**: ``True``. 182 | 183 | | **key_value** :dax-def-type:`str` :dax-def-note:`optional` 184 | 185 | | The value of the key that will be used to encrypt the token. Fernet key must be 32 url-safe base64-encoded bytes. 186 | 187 | 188 | 189 | ### Example python with parameters, safe False and custom path token 190 | 191 | .. code-block:: python 192 | 193 | custom_save_credentials = \ 194 | { 195 | "safe": False, 196 | "token_path": "/Users/your-user/Desktop/python_paypal_api/store_unsafe_token", 197 | } 198 | 199 | Identity(store_credentials=custom_save_credentials, debug=True).get_userinfo() 200 | 201 | ### Example python with parameters, safe True, custom path token and custom file path for key 202 | 203 | .. code-block:: python 204 | 205 | custom_save_credentials = \ 206 | { 207 | "safe": True, 208 | "token_path": "/Users/your-user/Desktop/python_paypal_api/store_safe_token", 209 | "key_path_name": "/Users/your-user/Desktop/python_paypal_api/store_key/sandbox.secret.key", 210 | } 211 | 212 | Identity(store_credentials=custom_save_credentials, debug=True).get_userinfo() 213 | 214 | 215 | ### How to generate a key 216 | 217 | .. code-block:: python 218 | 219 | from cryptography.fernet import Fernet 220 | 221 | key = Fernet.generate_key() 222 | 223 | print (key) # b'38Ooy2dq7hNyhGg3Z_26cirj5aa5M3wURLAeIb5RsNk=' 224 | 225 | ### Example python with parameters, safe True, custom path token and custom key 226 | 227 | .. code-block:: python 228 | 229 | custom_save_credentials = \ 230 | { 231 | "safe": True, 232 | "token_path": "/Users/your-user/Desktop/python_paypal_api/store_safe_token", 233 | "key_value": "38Ooy2dq7hNyhGg3Z_26cirj5aa5M3wURLAeIb5RsNk=" 234 | } 235 | 236 | Identity(store_credentials=custom_save_credentials, debug=True).get_userinfo() 237 | 238 | 239 | 240 | Combining Custom Credentials and Custom Store Credentials 241 | --------------------------------------------------------- 242 | 243 | You could customize the whole configuration, see an example: 244 | 245 | ### Example python 246 | 247 | .. code-block:: python 248 | 249 | from python_paypal_api.api import Identity 250 | from python_paypal_api.base import PaypalApiException 251 | import logging 252 | 253 | def py_test_credentials_config_account_full(account: list = None, config: dict = None): 254 | 255 | logging.info("---------------------------------------") 256 | logging.info("py_test_credentials_config_account_full") 257 | logging.info("---------------------------------------") 258 | 259 | try: 260 | 261 | result = Identity(credentials=account, store_credentials=config, debug=True).get_userinfo() 262 | return result 263 | 264 | except PaypalApiException as error: 265 | logging.error(error) 266 | 267 | if __name__ == '__main__': 268 | 269 | logger = logging.getLogger("test") 270 | 271 | custom_credentials = [ 272 | "production", 273 | "/Users/hanuman/Desktop/python_paypal_api/credentials", 274 | "users.yaml" 275 | ] 276 | 277 | custom_save_credentials = \ 278 | { 279 | "safe": True, 280 | "token_path": "/Users/your-user/Desktop/python_paypal_api/store_token", 281 | "key_path_name": "/Users/your-user/Desktop/python_paypal_api/store_key/production.secret.key", 282 | } 283 | 284 | res = py_test_credentials_config_account_full(custom_credentials, custom_save_credentials) 285 | logger.info(res) 286 | 287 | References 288 | ===================== 289 | 290 | .. target-notes:: 291 | 292 | .. _`config.yaml`: https://github.com/denisneuf/python-paypal-api/#credentials 293 | .. _`confused`: https://confuse.readthedocs.io/en/latest/usage.html#search-paths 294 | -------------------------------------------------------------------------------- /docs/credentials/howto.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | How to Authenticate 3 | =================== 4 | 5 | 6 | .. role:: dax-def-type 7 | :class: dax-def-type 8 | 9 | .. role:: dax-def-note 10 | :class: dax-def-note 11 | 12 | .. _requests: https://docs.python-requests.org/en/latest/index.html 13 | 14 | Please check as this example provides all ways to authenticate trough this API. 15 | 16 | Paypal Rest API, support Basic and Bearer authentication so lets go from simple to complex. 17 | For any test you need have at least a client_id and a client_secret to run it. 18 | 19 | | **client_id** :dax-def-type:`string` The ID obtained from Paypal 20 | | **client_secret** :dax-def-type:`string` The password obtained from Paypal 21 | | **client_mode** :dax-def-type:`enum` ``PRODUCTION`` or ``SANDBOX`` 22 | 23 | 24 | 25 | 26 | HTTPBasicAuth 27 | ============= 28 | 29 | The most simple way is to use the auth as a combination of client_id and client_secret, `requests`_ will create an HTTPBasicAuth that will allow you to get access to Paypal Rest API: 30 | 31 | 32 | Pass Credentials As Tuple 33 | ------------------------- 34 | 35 | 36 | **credentials** :dax-def-type:`tuple` 37 | 38 | .. code-block:: python 39 | 40 | credentials = ("your-client-id", "your-client-secret", "SANDBOX") 41 | 42 | 43 | ### Example python 44 | 45 | .. code-block:: python 46 | 47 | from python_paypal_api.api import Identity 48 | from python_paypal_api.base import PaypalApiException 49 | 50 | my_credentials = ("your-client-id", "your-client-secret", "SANDBOX") 51 | 52 | try: 53 | 54 | result = Identity(credentials=my_credentials).get_userinfo() 55 | payload = result.payload 56 | print(payload) 57 | 58 | except PaypalApiException as error: 59 | 60 | print(error) 61 | 62 | 63 | 64 | 65 | OAuth 2.0 Bearer 66 | ================ 67 | 68 | Bearer Tokens are the predominant type of access token used with OAuth 2.0. 69 | A Bearer Token is an opaque string, not intended to have any meaning to clients using it. 70 | This way will call the paypal oauth2 api and request for a token that could be used for all the api calls until get expired. 71 | There is 3 ways to achieve that: from environ, from code and from config. 72 | 73 | Pass Credentials From Environ 74 | ----------------------------- 75 | 76 | | **client_id** :dax-def-type:`os._Environ` >> :dax-def-type:`str` :dax-def-note:`required` 77 | | **client_secret** :dax-def-type:`os._Environ` >> :dax-def-type:`str` :dax-def-note:`required` 78 | | **client_mode** :dax-def-type:`os._Environ` >> :dax-def-type:`str` 79 | 80 | .. code-block:: python 81 | 82 | import os 83 | os.environ["client_id"] = "your-client-id" 84 | os.environ["client_secret"] = "your-client-secret" 85 | os.environ["client_mode"] = "your-client-mode" 86 | 87 | ### Example python 88 | 89 | .. code-block:: python 90 | 91 | 92 | import os 93 | from python_paypal_api.api import Identity 94 | from python_paypal_api.base import PaypalApiException 95 | 96 | 97 | os.environ["client_id"] = "your-client-id" 98 | os.environ["client_secret"] = "your-client-secret" 99 | os.environ["client_mode"] = "your-client-mode" 100 | 101 | try: 102 | result = Identity().get_userinfo() 103 | payload = result.payload 104 | print(payload) 105 | except PaypalApiException as error: 106 | print(error) 107 | 108 | 109 | 110 | 111 | 112 | Pass Credentials From Code 113 | -------------------------- 114 | 115 | **credentials** :dax-def-type:`dict` 116 | 117 | .. code-block:: python 118 | 119 | credentials = dict( 120 | client_id="your-client-id", 121 | client_secret="your-client-secret", 122 | client_mode="SANDBOX" 123 | ) 124 | 125 | 126 | ### Example python 127 | 128 | .. code-block:: python 129 | 130 | from python_paypal_api.api import Products 131 | from python_paypal_api.base import PaypalApiException 132 | 133 | my_credentials = dict( 134 | client_id="your-client-id", 135 | client_secret="your-client-secret", 136 | client_mode="SANDBOX" 137 | ) 138 | 139 | try: 140 | result = Products(credentials=my_credentials, debug=True).list_products() 141 | payload = result.payload 142 | print(payload) 143 | except PaypalApiException as error: 144 | print(error) 145 | 146 | 147 | 148 | Pass Credentials From Config 149 | ---------------------------- 150 | 151 | An example config file is provided in this repository, it supports multiple accounts. 152 | The confuse will search the system for a config file named ``config.yaml`` in the following search paths. 153 | 154 | * macOS: ``~/.config/python-paypal-api`` and ``~/Library/Application Support/python-paypal-api`` 155 | * Other Unix: ``~/.config/python-paypal-api`` and ``/etc/python-paypal-api`` 156 | * Windows: ``%APPDATA%\python-paypal-api`` where the APPDATA environment variable falls back to ``%HOME%\AppData\Roaming`` if undefined 157 | 158 | Content example of a ``config.yaml`` file. 159 | 160 | .. code-block:: yaml 161 | 162 | version: '1.0' 163 | 164 | configuration: 165 | 166 | production: 167 | client_id: 'your-client-id' 168 | client_secret: 'your-client-secret' 169 | client_mode: 'PRODUCTION' 170 | 171 | default: 172 | client_id: 'your-client-id-sandbox' 173 | client_secret: 'your-client-secret-sandbox' 174 | 175 | 176 | 177 | **credentials** :dax-def-type:`str` 178 | 179 | .. code-block:: python 180 | 181 | credentials = "production" 182 | 183 | ### Example python 184 | 185 | .. code-block:: python 186 | 187 | from python_paypal_api.api import Identity 188 | from python_paypal_api.base import PaypalApiException 189 | 190 | try: 191 | result = Identity(credentials="production", debug=True).get_userinfo( 192 | payload = result.payload 193 | print(payload) 194 | except PaypalApiException as error: 195 | print(error) 196 | 197 | View full possibilities :ref:`From Config File` 198 | 199 | -------------------------------------------------------------------------------- /docs/credentials/storing.rst: -------------------------------------------------------------------------------- 1 | .. _Storing Credentials: 2 | 3 | .. role:: dax-def-type 4 | :class: dax-def-type 5 | 6 | .. warning:: 7 | 8 | If you are using HTTPBasicAuth this do not applies as every call will be based on client_id and client_secret combination. No token is involved. 9 | 10 | 11 | Storing Credentials 12 | ~~~~~~~~~~~~~~~~~~~ 13 | 14 | If you use OAuth 2.0 Bearer to avoid calls to the oauth paypal authentication api to obtain the token for Bearer authentication, there is 2 ways to deal with that: 15 | 16 | 17 | ***** 18 | Cache 19 | ***** 20 | 21 | The python paypal api will use the `cachetools`_ package to store the token in a LRU cache. 22 | This provides you a way that the token is stored in cache however it will be alive since the python script is executing and includes all possible calls to the api. 23 | When the script is ended the cache is flushed and other script will not be able to obtain a cached token one and will call the api to obtain a current or new one. 24 | 25 | 26 | **** 27 | File 28 | **** 29 | 30 | If you provide a store_credentials bool in the client, True or False (default) 31 | The python paypal api will store the token in a file linked as an md5 of the client_id. 32 | All next calls will find the file first, check if the token is not expired and use it. 33 | If the token is expired will recreate a new file with the new token and the actual expiring time. 34 | For security reasons the token file could be encrypted or not, using safe bool in the client, True or False (default). 35 | If safe=True, will use `cryptography`_ package to generate a Fernet key and encrypt the content. 36 | The .key file and the .token file will be stored in the confuse config folder ``config = confuse.Configuration('python-paypal-api')`` 37 | View more details about `confuse`_ in the :ref:`From Config File` help. 38 | 39 | 40 | ------ 41 | Simple 42 | ------ 43 | 44 | Pass the keywords parameters to the client as bool: 45 | 46 | .. code-block:: python 47 | 48 | Identity(store_credentials=True).get_userinfo() # Mode Store Safe No 49 | 50 | 51 | .. code-block:: python 52 | 53 | Identity(store_credentials=True, safe=True).get_userinfo() # Mode Store Safe Yes 54 | 55 | 56 | 57 | ------- 58 | Complex 59 | ------- 60 | 61 | You could also pass the configuration as a dict. In that way you could overwrite the default storing folders for both token and key. 62 | Be aware that the folders need to exist and changing the configuration without moving the files will create a new token with a new key. 63 | Please check full examples to see the way you can use and choose it wisely. 64 | 65 | | **safe** :dax-def-type:`bool` Whether if the token will be encrypted or not. True or False 66 | | **token_path** :dax-def-type:`string` The absolut path to the folder wish to store the token 67 | | **key_path_name** :dax-def-type:`string` The absolut path to the file where you wish to store the key. 68 | | **key_value** :dax-def-type:`string` Fernet key must be 32 url-safe base64-encoded bytes. Use key_value will invalidate a key_path_name. 69 | 70 | 71 | 72 | This examples store an encrypted token in the folder ``store_encrypted`` and a key named ``sandbox_secret.key`` in the folder ``store_key``. 73 | 74 | 75 | .. code-block:: python 76 | 77 | config = \ 78 | { 79 | "safe": True, 80 | "token_path": "/Users/your-user/Desktop/store_encrypted", 81 | "key_path_name": "/Users/your-user/Desktop/store_key/sandbox_secret.key", 82 | } 83 | 84 | Identity(store_credentials=config, safe=True).get_userinfo() # Mode Store Safe Yes 85 | 86 | 87 | 88 | You could also use your own key and not store it, so in that way you will not use a ``key_path_name`` and you should use a ``key_value``. 89 | 90 | .. code-block:: python 91 | 92 | config = \ 93 | { 94 | "safe": True, 95 | "token_path": "/Users/your-user/Desktop/store_encrypted", 96 | "key_value": "4BekoTZNO5aK4HOtIwkGYbq0IegqE5Y6w0bUoqVJqzk=", 97 | } 98 | 99 | Identity(store_credentials=config, safe=True).get_userinfo() # Mode Store Safe Yes 100 | 101 | 102 | .. warning:: 103 | 104 | Take care about that the token filename is inmutable and if you use diferent configuration could result in overwriting the token with a new key if you change it. 105 | 106 | 107 | .. _`cachetools`: https://pypi.org/project/cachetools/ 108 | .. _`cryptography`: https://pypi.org/project/cryptography/ 109 | .. _`confuse`: https://pypi.org/project/confuse/ 110 | 111 | -------------------------------------------------------------------------------- /docs/disclaimer.rst: -------------------------------------------------------------------------------- 1 | Disclaimer 2 | ========== 3 | 4 | .. code-block:: bash 5 | 6 | MIT License 7 | 8 | Copyright (c) 2021 denisneuf 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | -------------------------------------------------------------------------------- /docs/example.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.api import Identity, Products 2 | from python_paypal_api.base import PaypalApiException 3 | import logging 4 | 5 | 6 | def py_list_products(**kwargs): 7 | 8 | logging.info("---------------------------------") 9 | logging.info("Catalog > list_products()") 10 | logging.info("---------------------------------") 11 | 12 | credentials = dict( 13 | client_id="your-client-id", 14 | client_secret="your-client-secret", 15 | client_mode="your-mode" # PRODUCTION OR SANDBOX(default) 16 | ) 17 | 18 | try: 19 | 20 | result = Products(credentials=credentials, store_credentials=False, debug=True).list_products( 21 | **kwargs 22 | ) 23 | document_dict = result.payload 24 | logging.info(result) 25 | 26 | except Exception as error: 27 | logging.info(error) 28 | 29 | 30 | def py_get_userinfo(): 31 | 32 | logging.info("---------------------------------") 33 | logging.info("Identity > py_get_userinfo") 34 | logging.info("---------------------------------") 35 | 36 | try: 37 | 38 | # result = Identity(account="production", store_credentials=True, debug=True).get_userinfo( 39 | result = Identity(debug=True).get_userinfo( 40 | ) 41 | logging.info(result) 42 | 43 | except PaypalApiException as error: 44 | logging.error(error) 45 | 46 | except Exception as error: 47 | logging.info(error) 48 | 49 | if __name__ == '__main__': 50 | 51 | logger = logging.getLogger("test") 52 | 53 | py_get_userinfo() 54 | 55 | py_list_products( 56 | total_required=True, 57 | page_size=1, 58 | page=2 59 | ) 60 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Python Paypal API documentation 2 | =============================== 3 | 4 | This project helps you using python 3.8 to use the *Python Paypal API*. 5 | 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | installation 11 | credentials 12 | resources 13 | disclaimer 14 | 15 | 16 | .. versionadded:: 0.1.1 17 | The *Products* is added replacing *Catalog* for best naming 18 | The *Config.yaml* is now the default for config credentials 19 | 20 | .. literalinclude:: example.py 21 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | 5 | 6 | 7 | 8 | 9 | You can install using pip 10 | 11 | 12 | 13 | 14 | .. code-block:: bash 15 | 16 | pip install python-paypal-api 17 | 18 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | cachetools 4 | confuse 5 | cryptography 6 | -------------------------------------------------------------------------------- /docs/resources.rst: -------------------------------------------------------------------------------- 1 | Current resources 2 | ================= 3 | 4 | Create and manage your site's payment functions using PayPal API collections. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | api/tracking 10 | api/transactions 11 | api/products -------------------------------------------------------------------------------- /python_paypal_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denisneuf/python-paypal-api/824e21e88522938abef73b6b79a66fb3b51a1069/python_paypal_api/__init__.py -------------------------------------------------------------------------------- /python_paypal_api/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .orders import Orders 2 | from .disputes import Disputes 3 | from .products import Products 4 | from .tracking import Tracking 5 | from .identity import Identity 6 | from .invoices import Invoices 7 | from .partner_referrals import PartnerReferrals 8 | from .transactions import Transactions 9 | 10 | __all__ = [ 11 | "Orders", 12 | "Disputes", 13 | "Products", 14 | "Identity", 15 | "Invoices", 16 | "PartnerReferrals", 17 | "Transactions" 18 | ] -------------------------------------------------------------------------------- /python_paypal_api/api/disputes.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | import logging 10 | 11 | class Disputes(Client): 12 | 13 | 14 | @PaypalEndpoint('/v1/customer/disputes', method='GET') 15 | def list_disputes(self, **kwargs) -> ApiResponse: 16 | return self._request(kwargs.pop('path'), params=kwargs) 17 | 18 | @PaypalEndpoint('/v1/customer/disputes/{}', method='GET') 19 | def get_dispute(self, disputeId, **kwargs) -> ApiResponse: 20 | contentType = "application/json" 21 | headers = {'Content-Type': contentType} 22 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, headers=headers) 23 | 24 | @PaypalEndpoint('/v1/customer/disputes/{}/accept-offer', method='POST') 25 | def accept_offer_dispute(self, disputeId, **kwargs) -> ApiResponse: 26 | return self._request(fPaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 27 | 28 | @PaypalEndpoint('/v1/customer/disputes/{}/make-offer', method='POST') 29 | def make_offer_dispute(self, disputeId, **kwargs) -> ApiResponse: 30 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 31 | 32 | @PaypalEndpoint('/v1/customer/disputes/{}/escalate', method='POST') 33 | def escalate_dispute(self, disputeId, **kwargs) -> ApiResponse: 34 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 35 | 36 | @PaypalEndpoint('/v1/customer/disputes/{}/send-message', method='POST') 37 | def send_message(self, disputeId, **kwargs) -> ApiResponse: 38 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 39 | 40 | @PaypalEndpoint('/v1/customer/disputes/{}/provide-evidence', method='POST') 41 | def provide_evidence(self, disputeId, file, content_type='application/pdf', **kwargs) -> ApiResponse: 42 | 43 | fields={ 44 | 'input': (None, Utils.convert_body(kwargs.pop('body'), False), 'application/json'), 45 | 'file1': ("sample2.pdf", open(file, 'rb'), 'application/pdf') # could be ignored the application but not the name .pdf 46 | } 47 | 48 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, files=fields) 49 | 50 | # Important: This method is for sandbox use only. 51 | @PaypalEndpoint('/v1/customer/disputes/{}/require-evidence', method='POST') 52 | def update_dispute(self, disputeId, **kwargs) -> ApiResponse: 53 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 54 | 55 | # Important: This method is for sandbox use only. 56 | @PaypalEndpoint('/v1/customer/disputes/{}/adjudicate', method='POST') 57 | def adjudicate(self, disputeId, **kwargs) -> ApiResponse: 58 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(disputeId), params=kwargs, data=kwargs.pop('body')) 59 | -------------------------------------------------------------------------------- /python_paypal_api/api/identity.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | class Identity(Client): 10 | 11 | @PaypalEndpoint('/v1/identity/oauth2/userinfo', method='GET') 12 | def get_userinfo(self, **kwargs) -> ApiResponse: 13 | headers = {} 14 | contentType = "application/json" # headers = {'Content-Type': contentType} 15 | Utils.contentType(headers, contentType) 16 | schema = "paypalv1.1" 17 | kwargs["schema"] = schema 18 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 19 | 20 | 21 | @PaypalEndpoint('/v1/identity/account-settings', method='POST') 22 | def post_set_account_properties(self, **kwargs) -> ApiResponse: 23 | headers = {} 24 | contentType = "application/json" # headers = {'Content-Type': contentType} 25 | Utils.contentType(headers, contentType) 26 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 27 | 28 | @PaypalEndpoint('/v1/identity/account-settings/deactivate', method='POST') 29 | def post_disable_account_properties(self, **kwargs) -> ApiResponse: 30 | headers = {} 31 | contentType = "application/json" # headers = {'Content-Type': contentType} 32 | Utils.contentType(headers, contentType) 33 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) -------------------------------------------------------------------------------- /python_paypal_api/api/invoices.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | 10 | class Invoices(Client): 11 | 12 | 13 | @PaypalEndpoint('/v2/invoicing/generate-next-invoice-number', method='POST') 14 | def post_generate_next_invoice_number(self, **kwargs) -> ApiResponse: 15 | headers = {} 16 | contentType = "application/json" 17 | Utils.contentType(headers, contentType) 18 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 19 | 20 | 21 | @PaypalEndpoint('/v2/invoicing/invoices', method='GET') 22 | def get_list_invoices(self, **kwargs) -> ApiResponse: 23 | headers = {} 24 | contentType = "application/json" 25 | Utils.contentType(headers, contentType) 26 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 27 | 28 | 29 | @PaypalEndpoint('/v2/invoicing/invoices', method='POST') 30 | def post_draft_invoice(self, **kwargs) -> ApiResponse: 31 | headers = {} 32 | contentType = "application/json" 33 | Utils.contentType(headers, contentType) 34 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 35 | 36 | 37 | @PaypalEndpoint('/v2/invoicing/invoices/{}', method='DELETE') 38 | def post_delete_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 39 | headers = {} 40 | contentType = "application/json" 41 | Utils.contentType(headers, contentType) 42 | Utils.payPalRequestId(headers) 43 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), params=kwargs, headers=headers) 44 | 45 | 46 | @PaypalEndpoint('/v2/invoicing/invoices/{}', method='PUT') 47 | def put_update_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 48 | headers = {} 49 | contentType = "application/json" 50 | Utils.contentType(headers, contentType) 51 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 52 | 53 | 54 | @PaypalEndpoint('/v2/invoicing/invoices/{}', method='GET') 55 | def get_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 56 | headers = {} 57 | contentType = "application/json" 58 | Utils.contentType(headers, contentType) 59 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), params=kwargs, headers=headers) 60 | 61 | 62 | @PaypalEndpoint('/v2/invoicing/invoices/{}/cancel', method='POST') 63 | def post_cancel_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 64 | headers = {} 65 | contentType = "application/json" 66 | Utils.contentType(headers, contentType) 67 | Utils.payPalRequestId(headers) 68 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 69 | 70 | 71 | @PaypalEndpoint('/v2/invoicing/invoices/{}/generate-qr-code', method='POST') 72 | def post_generate_qr_code(self, invoiceId:str, **kwargs) -> ApiResponse: 73 | headers = {} 74 | contentType = "application/json" 75 | Utils.contentType(headers, contentType) 76 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 77 | 78 | 79 | @PaypalEndpoint('/v2/invoicing/invoices/{}/payments', method='POST') 80 | def post_record_payment_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 81 | headers = {} 82 | contentType = "application/json" 83 | Utils.contentType(headers, contentType) 84 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 85 | 86 | 87 | @PaypalEndpoint('/v2/invoicing/invoices/{}/payments/{}', method='DELETE') 88 | def delete_external_payment_invoice(self, invoiceId:str, transactionId:str, **kwargs) -> ApiResponse: 89 | headers = {} 90 | contentType = "application/json" 91 | Utils.contentType(headers, contentType) 92 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId, transactionId), params=kwargs, headers=headers) 93 | 94 | 95 | @PaypalEndpoint('/v2/invoicing/invoices/{}/refunds', method='POST') 96 | def post_record_refund_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 97 | headers = {} 98 | contentType = "application/json" 99 | Utils.contentType(headers, contentType) 100 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 101 | 102 | 103 | @PaypalEndpoint('/v2/invoicing/invoices/{}/refunds/{}', method='DELETE') 104 | def delete_external_refund_invoice(self, invoiceId:str, refundId:str, **kwargs) -> ApiResponse: 105 | headers = {} 106 | contentType = "application/json" 107 | Utils.contentType(headers, contentType) 108 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId, refundId), params=kwargs, headers=headers) 109 | 110 | 111 | @PaypalEndpoint('/v2/invoicing/invoices/{}/remind', method='POST') 112 | def post_invoice_remind(self, invoiceId:str, **kwargs) -> ApiResponse: 113 | headers = {} 114 | contentType = "application/json" 115 | Utils.contentType(headers, contentType) 116 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 117 | 118 | 119 | @PaypalEndpoint('/v2/invoicing/invoices/{}/send', method='POST') 120 | def post_send_invoice(self, invoiceId:str, **kwargs) -> ApiResponse: 121 | headers = {} 122 | contentType = "application/json" 123 | Utils.contentType(headers, contentType) 124 | Utils.payPalRequestId(headers) 125 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(invoiceId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 126 | 127 | 128 | @PaypalEndpoint('/v2/invoicing/search-invoices', method='POST') 129 | def post_search_invoices(self, **kwargs) -> ApiResponse: 130 | headers = {} 131 | contentType = "application/json" 132 | Utils.contentType(headers, contentType) 133 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 134 | 135 | 136 | @PaypalEndpoint('/v2/invoicing/templates', method='GET') 137 | def get_list_templates(self, **kwargs) -> ApiResponse: 138 | headers = {} 139 | contentType = "application/json" 140 | Utils.contentType(headers, contentType) 141 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 142 | 143 | @PaypalEndpoint('/v2/invoicing/templates', method='POST') 144 | def post_create_template(self, **kwargs) -> ApiResponse: 145 | headers = {} 146 | contentType = "application/json" 147 | Utils.contentType(headers, contentType) 148 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 149 | 150 | 151 | @PaypalEndpoint('/v2/invoicing/templates/{}', method='DELETE') 152 | def delete_template(self, templateId:str, **kwargs) -> ApiResponse: 153 | headers = {} 154 | contentType = "application/json" 155 | Utils.contentType(headers, contentType) 156 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(templateId), params=kwargs, headers=headers) 157 | 158 | 159 | @PaypalEndpoint('/v2/invoicing/templates/{}', method='PUT') 160 | def put_update_template(self, templateId:str, **kwargs) -> ApiResponse: 161 | headers = {} 162 | contentType = "application/json" 163 | Utils.contentType(headers, contentType) 164 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(templateId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 165 | 166 | 167 | @PaypalEndpoint('/v2/invoicing/templates/{}', method='GET') 168 | def get_template(self, templateId:str, **kwargs) -> ApiResponse: 169 | headers = {} 170 | contentType = "application/json" 171 | Utils.contentType(headers, contentType) 172 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(templateId), params=kwargs, headers=headers) 173 | -------------------------------------------------------------------------------- /python_paypal_api/api/orders.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | import logging 10 | 11 | class Orders(Client): 12 | 13 | 14 | @PaypalEndpoint('/v2/checkout/orders', method='POST') 15 | def post_create_order(self, **kwargs) -> ApiResponse: 16 | headers = {} 17 | contentType = "application/json" # headers = {'Content-Type': contentType} 18 | prefer = "return=representation" # return=minimal return=representation 19 | Utils.contentType(headers, contentType) 20 | Utils.payPalRequestId(headers) 21 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 22 | 23 | 24 | @PaypalEndpoint('/v2/checkout/orders/{}', method='PATCH') 25 | def patch_update_order(self, orderId:str, **kwargs) -> ApiResponse: 26 | headers = {} 27 | contentType = "application/json" 28 | Utils.contentType(headers, contentType) 29 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(orderId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 30 | 31 | @PaypalEndpoint('/v2/checkout/orders/{}', method='GET') 32 | def get_order(self, orderId:str, **kwargs) -> ApiResponse: 33 | headers = {} 34 | contentType = "application/json" # headers = {'Content-Type': contentType} 35 | Utils.contentType(headers, contentType) 36 | # return self._request(fill_query_params(kwargs.pop('path'), orderId), params=kwargs, headers=headers) 37 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(orderId), params=kwargs, headers=headers) 38 | 39 | @PaypalEndpoint('/v2/checkout/orders/{}/capture', method='POST') 40 | def post_capture_for_order(self, orderId:str, **kwargs) -> ApiResponse: 41 | headers = {} 42 | contentType = "application/json" # headers = {'Content-Type': contentType} 43 | prefer = "return=representation" # return=minimal return=representation 44 | Utils.contentType(headers, contentType) 45 | Utils.payPalRequestId(headers) 46 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(orderId), params=kwargs, headers=headers) 47 | 48 | 49 | @PaypalEndpoint('/v2/checkout/orders/{}/authorize', method='POST') 50 | def post_authorize_for_order(self, orderId:str, **kwargs) -> ApiResponse: 51 | headers = {} 52 | contentType = "application/json" # headers = {'Content-Type': contentType} 53 | prefer = "return=representation" # return=minimal return=representation 54 | Utils.contentType(headers, contentType) 55 | Utils.payPalRequestId(headers) 56 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(orderId), params=kwargs, headers=headers) 57 | 58 | 59 | @PaypalEndpoint('/v2/checkout/orders/{}/confirm-payment-source', method='POST') 60 | def post_confirm_order(self, orderId:str, **kwargs) -> ApiResponse: 61 | headers = {} 62 | contentType = "application/json" # headers = {'Content-Type': contentType} 63 | prefer = "return=representation" # return=minimal return=representation 64 | Utils.contentType(headers, contentType) 65 | Utils.payPalRequestId(headers) 66 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(orderId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 67 | 68 | 69 | -------------------------------------------------------------------------------- /python_paypal_api/api/partner_referrals.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | 10 | class PartnerReferrals(Client): 11 | 12 | @PaypalEndpoint('/v2/customer/partner-referrals', method='POST') 13 | def post_create_partner_referrals(self, **kwargs) -> ApiResponse: 14 | headers = {} 15 | contentType = "application/json" 16 | Utils.contentType(headers, contentType) 17 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 18 | 19 | 20 | @PaypalEndpoint('/v2/customer/partner-referrals/{}', method='GET') 21 | def get_referral_data(self, partnerReferralId:str, **kwargs) -> ApiResponse: 22 | headers = {} 23 | contentType = "application/json" 24 | Utils.contentType(headers, contentType) 25 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(partnerReferralId), params=kwargs, headers=headers) -------------------------------------------------------------------------------- /python_paypal_api/api/products.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils, 7 | ProductType, 8 | CategoryType 9 | ) 10 | 11 | # import logging 12 | # import json 13 | 14 | 15 | PRODUCT_PROPERTIES = { 16 | 'name', 'type', 'id', 'category', 'home_url', 17 | 'image_url', 'description', 'update_time', 'create_time' 18 | } 19 | 20 | # PRODUCT_UPDATE = {'category', 'home_url', 'image_url', 'description'} 21 | PRODUCT_UPDATE = {'name', 'category', 'home_url', 'image_url', 'description'} 22 | 23 | 24 | class Products(Client): 25 | 26 | r""" 27 | Use ``/products`` resource to create and manage products. 28 | """ 29 | 30 | @PaypalEndpoint('/v1/catalogs/products', method='GET') 31 | def list_products(self, **kwargs) -> ApiResponse: 32 | r""" 33 | :dax-operation-get:`GET` :dax-operation-path:`/v1/catalogs/products` 34 | 35 | Lists products. 36 | 37 | \*\*\kwargs: 38 | 39 | | **page** :dax-def-type:`integer` 40 | 41 | | A non-zero integer which is the start index of the entire list of items that are returned in the response. So, the combination of ``page=1`` and ``page_size=20`` returns the first 20 items. The combination of ``page=2`` and ``page_size=20`` returns the next 20 items. 42 | 43 | | :dax-def-meta:`Minimum value:` ``1``. 44 | 45 | | :dax-def-meta:`Maximum value:` ``100000``. 46 | 47 | | **page_size** :dax-def-type:`integer` 48 | 49 | | The number of items to return in the response. 50 | 51 | | :dax-def-meta:`Minimum value:` ``1``. 52 | 53 | | :dax-def-meta:`Maximum value:` ``20``. 54 | 55 | | **total_required** :dax-def-type:`boolean` 56 | 57 | | Indicates whether to show the total items and total pages in the response. 58 | 59 | 60 | """ 61 | headers = {} 62 | contentType = "application/json" 63 | Utils.contentType(headers, contentType) 64 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 65 | 66 | @PaypalEndpoint('/v1/catalogs/products/{}', method='GET') 67 | def get_product(self, productId:str, **kwargs) -> ApiResponse: 68 | r""" 69 | :dax-operation-get:`GET` :dax-operation-path:`/v1/catalogs/products/{product_id}` 70 | """ 71 | headers = {} 72 | contentType = "application/json" 73 | Utils.contentType(headers, contentType) 74 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(productId), params=kwargs, headers=headers) 75 | 76 | @PaypalEndpoint('/v1/catalogs/products/{}', method='PATCH') 77 | def update_product(self, productId:str, **kwargs) -> ApiResponse: 78 | r""" 79 | :dax-operation-get:`PATCH` :dax-operation-path:`/v1/catalogs/products/{product_id}` 80 | """ 81 | headers = {} 82 | contentType = "application/json" 83 | Utils.contentType(headers, contentType) 84 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(productId), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 85 | 86 | @PaypalEndpoint('/v1/catalogs/products/{}', method='PATCH') 87 | def update_product_helper(self, productId:str, **kwargs) -> ApiResponse: 88 | r""" 89 | :dax-operation-get:`PATCH` :dax-operation-path:`/v1/catalogs/products/{product_id}` 90 | """ 91 | headers = {} 92 | contentType = "application/json" 93 | Utils.contentType(headers, contentType) 94 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(productId), data=Utils.convert_updates(kwargs.pop('body')), params=kwargs, headers=headers) 95 | 96 | @PaypalEndpoint('/v1/catalogs/products', method='POST') 97 | def post_product(self, **kwargs) -> ApiResponse: 98 | r""" 99 | :dax-operation-get:`POST` :dax-operation-path:`/v1/catalogs/products` 100 | """ 101 | headers = {} 102 | contentType = "application/json" 103 | Utils.contentType(headers, contentType) 104 | Utils.payPalRequestId(headers) 105 | prefer = "return=representation" # "return=minimal" 106 | Utils.prefer(headers, prefer) 107 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 108 | 109 | @PaypalEndpoint('/v1/catalogs/products', method='POST') 110 | def post_product_helper(self, body:(str or dict or list), **kwargs) -> ApiResponse: 111 | r""" 112 | :dax-operation-get:`POST` :dax-operation-path:`/v1/catalogs/products` 113 | """ 114 | headers = {} 115 | contentType = "application/json" 116 | Utils.contentType(headers, contentType) 117 | Utils.payPalRequestId(headers) 118 | prefer = "return=representation" # "return=minimal" 119 | Utils.prefer(headers, prefer) 120 | dictionary = body.copy() 121 | 122 | if isinstance(dictionary["type"], ProductType): 123 | dictionary["type"] = dictionary["type"].value 124 | else: 125 | raise TypeError("Only ProductType are allowed") 126 | 127 | if isinstance(dictionary["category"], CategoryType): 128 | dictionary["category"] = dictionary["category"].value 129 | else: 130 | raise TypeError("Only CategoryType are allowed") 131 | 132 | return self._request(kwargs.pop('path'), data=Utils.convert_body(dictionary, False), params=kwargs, headers=headers) 133 | 134 | -------------------------------------------------------------------------------- /python_paypal_api/api/tracking.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | import logging 10 | 11 | class Tracking(Client): 12 | r""" 13 | Use the /trackers resource to create and manage tracking information for PayPal transactions. 14 | """ 15 | 16 | @PaypalEndpoint('/v1/shipping/trackers/{}', method='PUT') 17 | def put_tracking(self, id: str, **kwargs) -> ApiResponse: 18 | r""" 19 | :dax-operation-get:`PUT` :dax-operation-path:`/v1/shipping/trackers/{id}` 20 | 21 | .. _Update or cancel tracking information: https://developer.paypal.com/docs/tracking/integrate/#update-or-cancel-tracking-information 22 | 23 | Updates or cancels the tracking information for a PayPal transaction, by ID. To cancel tracking information, call this method and set the status to CANCELLED. For more information, see `Update or cancel tracking information`_. 24 | 25 | \*\*\args: 26 | 27 | | **id** :dax-def-type:`string` :dax-def-note:`required` 28 | 29 | | The ID of the tracker in the ``transaction_id-tracking_number`` format. 30 | 31 | \*\*\kwargs: 32 | Request body 33 | 34 | | **status** :dax-def-type:`enum` :dax-def-note:`required` 35 | 36 | | The status of the item shipment. For allowed values, see Shipping Statuses. 37 | 38 | | The possible values are: 39 | 40 | - ``CANCELLED``. The shipment was cancelled and the tracking number no longer applies. 41 | - ``DELIVERED``. The item was already delivered when the tracking number was uploaded. 42 | - ``LOCAL_PICKUP``. Either the buyer physically picked up the item or the seller delivered the item in person without involving any couriers or postal companies. 43 | - ``ON_HOLD``. The item is on hold. Its shipment was temporarily stopped due to bad weather, a strike, customs, or another reason. 44 | - ``SHIPPED``. The item was shipped and is on the way. 45 | - ``SHIPMENT_CREATED``. The shipment was created. 46 | - ``DROPPED_OFF``. The shipment was dropped off. 47 | - ``IN_TRANSIT``. The shipment is in transit on its way to the buyer. 48 | - ``RETURNED``. The shipment was returned. 49 | - ``LABEL_PRINTED``. The label was printed for the shipment. 50 | - ``ERROR``. An error occurred with the shipment. 51 | - ``UNCONFIRMED``. The shipment is unconfirmed. 52 | - ``PICKUP_FAILED``. Pick-up failed for the shipment. 53 | - ``DELIVERY_DELAYED``. The delivery was delayed for the shipment. 54 | - ``DELIVERY_SCHEDULED``. The delivery was scheduled for the shipment. 55 | - ``DELIVERY_FAILED``. The delivery failed for the shipment. 56 | - ``INRETURN``. The shipment is being returned. 57 | - ``IN_PROCESS``. The shipment is in process. 58 | - ``NEW``. The shipment is new. 59 | - ``VOID``. If the shipment is cancelled for any reason, its state is void. 60 | - ``PROCESSED``. The shipment was processed. 61 | - ``NOT_SHIPPED``. The shipment was not shipped. 62 | 63 | | **transaction_id** :dax-def-type:`string` :dax-def-note:`required` 64 | 65 | | The PayPal transaction ID. 66 | 67 | | **carrier** :dax-def-type:`enum` :dax-def-note:`required` 68 | 69 | | The carrier for the shipment. Some carriers have a global version as well as local subsidiaries. The subsidiaries are repeated over many countries and might also have an entry in the global list. Choose the carrier for your country. If the carrier is not available for your country, choose the global version of the carrier. If your carrier name is not in the list, set carrier_other_name to OTHER. For allowed values, see Carriers. 70 | 71 | | The possible values are: 72 | 73 | - ``ACOMMERCE``. `aCommerce`_ 74 | - ``PHL_2GO``. 2GO Philippines 75 | - ``AU_DHL_EXPRESS``. DHL Express Australia. 76 | - ``BEL_DHL``. DHL Belgium. 77 | - ``DE_DHL_DEUTSHCE_POST_INTL_SHIPMENTS``. Deutsche Post DHL Post International Germany 78 | - ``IE_AN_POST_REGISTERED``. Post Registered Ireland 79 | - ``AU_AU_POST``. `Australian Postal Corporation`_ 80 | - ``SPEEDEXCOURIER``. Speedex Courier. 81 | - ``UK_ROYALMAIL_SPECIAL``. Royal Mail Special Delivery UK 82 | - ``FR_COLIS``. Colis France. 83 | - ``VNPOST_EMS``. Post EMS Vietnam. 84 | - ``NL_FEDEX``. Federal Express Netherlands 85 | - ``CN_EMS``. EMS China. 86 | - ``IT_POSTE_ITALIANE``. Poste Italiane. 87 | - ``HK_DHL_ECOMMERCE``. DHL eCommerce Hong Kong. 88 | - ``ARAMEX``. Aramex. 89 | - ``AU_OTHER``. Other - Australia. 90 | - ``TW_CHUNGHWA_POST``. Chunghwa POST Taiwan 91 | - ``DPEX``. `DPEX Worldwide`_ 92 | - ``POST_SERBIA``. `Pošta Srbije`_ 93 | - ``PL_POCZTEX``. Pocztex 94 | - ``CNEXPS``. CN Express China. 95 | - ``DIRECTLOG``. Directlog. 96 | - ``ES_CORREOS_DE_ESPANA``. Correos de Espana 97 | - ``BE_KIALA``. Kiala Point Belgium 98 | - ``ALPHAFAST``. Alphafast. 99 | - ``UKR_POSHTA``. `Ukrposhta - Ukraine's National Post`_ 100 | - ``CN_FEDEX``. Federal Express China 101 | - ``BUYLOGIC``. `Buylogic`_ 102 | - ``IT_DHL_ECOMMERCE``. DHL eCommerce Italy. 103 | - ``NINJAVAN_MY``. Ninjavan Malaysia. 104 | - ``JPN_YAMATO``. Yamato Japan. 105 | - ``POSTNORD_LOGISTICS``. Post Nord Logistics. 106 | - ``US_DHL_GLOBALFORWARDING``. DHL Global Forwarding US. 107 | - ``IT_SGT``. SGT Corriere Espresso Italy. 108 | - ``NINJAVAN_PHILIPPINES``. Ninja Van Philippines. 109 | - ``EKART``. Ekart. 110 | - ``IDN_WAHANA``. Wahana Indonesia. 111 | - ``FR_GLS``. `General Logistics Systems (GLS) France`_ 112 | - ``IDN_POS_INT``. Pos Indonesia International. 113 | - ``DE_HERMES``. Hermes Germany. 114 | - ``PRT_CHRONOPOST``. Chronopost Portugal. 115 | - ``MYS_MYS_POST``. `Pos Malaysia`_ 116 | - ``WEBINTERPRET``. WebInterpret. 117 | - ``BG_BULGARIAN_POST``. `Bulgarian Post`_ 118 | - ``NL_TPG``. TPG Post Netherlands 119 | - ``CA_CANPAR``. Canpar Courier Canada 120 | - ``MYS_AIRPAK``. Airpak Malaysia. 121 | - ``MEX_SENDA``. Senda Mexico. 122 | - ``LANDMARK_GLOBAL``. Landmark Global. 123 | - ``UK_NIGHTLINE``. Nightline UK. 124 | - ``JP_UPS``. United Parcel Service Japan 125 | - ``UK_DHL``. DHL UK. 126 | - ``SG_SG_POST``. Singapore Post. 127 | - ``PHL_AIRSPEED``. Airspeed Philippines. 128 | - ``DHL``. DHL Express. 129 | - ``KR_KOREA_POST``. Korea Post 130 | - ``JP_KURO_NEKO_YAMATO_UNYUU``. Kuro Neko Yamato Unyuu Japan 131 | - ``IE_AN_POST_SWIFTPOST``. Swift Post Ireland 132 | - ``CUCKOOEXPRESS``. Cuckoo Express. 133 | - ``FR_OTHER``. Other - France. 134 | - ``FASTRAK_TH``. Fastrak Thailand. 135 | - ``AU_DHL_ECOMMERCE``. DHL eCommerce Australia. 136 | - ``DE_UPS``. United Parcel Service Germany 137 | - ``ESHOPWORLD``. EShopWorld. 138 | - ``INTERNATIONAL_BRIDGE``. International Bridge. 139 | - ``FR_COLIPOSTE``. Coliposte France 140 | - ``AU_AUSTRIAN_POST``. Austrian Post. 141 | - ``IND_DELHIVERY``. `Delhivery India`_ 142 | - ``DE_TNT``. TNT Germany. 143 | - ``GLOBAL_DHL``. DHL Global. 144 | - ``US_DHL_PARCEL``. DHL Parcel US. 145 | - ``NL_UPS``. United Parcel Service Netherlands 146 | - ``GB_APC``. `APC Overnight UK`_ 147 | - ``IDN_TIKI``. Tiki Indonesia. 148 | - ``HERMES``. Hermes. 149 | - ``ESP_NACEX``. `Nacex Spain`_ 150 | - ``NL_TNT``. TNT Netherlands. 151 | - ``DE_FEDEX``. Federal Express Germany 152 | - ``OTHER``. Other. 153 | - ``BONDSCOURIERS``. Bonds Couriers. 154 | - ``IT_DHL_GLOBALFORWARDING``. DHL Global Forwarding Italy. 155 | - ``IDN_LION_PARCEL``. Lion Parcel Indonesia. 156 | - ``UK_YODEL``. `Yodel UK`_ 157 | - ``IT_DHL_EXPRESS``. DHL Express Italy. 158 | - ``PL_DHL_GLOBALFORWARDING``. `DHL Global Forwarding Poland`_ 159 | - ``DPD_POLAND``. DPD Poland. 160 | - ``AU_AUSTRALIA_POST_EXPRESS_POST_PLATINUM``. Australia Post Express Post Platinum 161 | - ``ES_TNT``. TNT Spain. 162 | - ``CN_DHL_EXPRESS``. DHL Express Canada. 163 | - ``DE_DPD``. DPD Germany. 164 | - ``DE_DPD_DELISTRACK``. DPD Delistrack Germany 165 | - ``CN_DHL_ECOMMERCE``. DHL eCommerce China. 166 | - ``JP_TNT``. TNT Japan. 167 | - ``PRT_CTT``. `CTT Expresso Portugal`_ 168 | - ``UK_INTERLINK_EXPRESS``. Interlink Express UK. 169 | - ``NLD_POSTNL``. `PostNL Netherlands`_ 170 | - ``CA_DHL_ECOMMERCE``. DHL eCommerce Canada. 171 | - ``SWIFTAIR``. Swift Air. 172 | - ``NOR_POSTEN``. `Posten Norge`_ 173 | - ``MEX_REDPACK``. Redpack Mexico. 174 | - ``PL_MASTERLINK``. Masterlink Poland 175 | - ``PL_TNT``. TNT Express Poland 176 | - ``NIM_EXPRESS``. `Nim Express`_ 177 | - ``PL_UPS``. United Parcel Service Poland 178 | - ``UKR_NOVA``. `Nova Poshta`_ 179 | - ``QUANTIUM``. Quantium. 180 | - ``SENDLE``. Sendle. 181 | - ``SG_PARCELPOST``. Parcel Post Singapore. 182 | - ``SG_NINJAVAN``. Ninjavan Singapore. 183 | - ``BQC_EXPRESS``. BQC Express. 184 | - ``RPD2MAN``. RPD2man Deliveries 185 | - ``THA_KERRY``. Kerry Thailand. 186 | - ``MEX_AEROFLASH``. Aeroflash Mexico. 187 | - ``SPREADEL``. Spreadel. 188 | - ``ESP_REDUR``. `Redur Spain`_ 189 | - ``JP_JAPANPOST``. Japan Post 190 | - ``ARE_EMIRATES_POST``. `Emirates Post Group`_ 191 | - ``CN_CHINA_POST_EMS``. China Post EMS Express Mail Service 192 | - ``UK_DHL_GLOBALFORWARDING``. DHL Global Forwarding UK. 193 | - ``CN_SF_EXPRESS``. SF Express China. 194 | - ``UK_FEDEX``. Federal Express UK 195 | - ``POL_POCZTA``. Poczta Poland. 196 | - ``YANWEN``. `Yanwen Express`_ 197 | - ``KOR_CJ``. CJ Korea. 198 | - ``DE_DEUTSCHE_POST_DHL_WITHIN_EUROPE_TRACKNET``. Deutsche Post DHL Tracknet Germany 199 | - ``IND_XPRESSBEES``. XpressBees India. 200 | - ``UK_TNT``. TNT UK. 201 | - ``CJ_KOREA_THAI``. `CJ Logistics in Thailand`_ 202 | - ``CN_OTHER``. Other - China. 203 | - ``IDN_POS``. Indonesia Post. 204 | - ``ABC_MAIL``. ABC Mail. 205 | - ``UK_UPS``. United Parcel Service UK 206 | - ``CHINA_POST``. China Post. 207 | - ``PL_DHL_EXPRESS``. DHL Express Poland. 208 | - ``ESP_SPANISH_SEUR``. Spanish Seur Spain 209 | - ``SG_ZALORA``. Zalora Singapore. 210 | - ``MATKAHUOLTO``. Matkahuoloto. 211 | - ``FR_LAPOSTE``. Laposte France. 212 | - ``KANGAROO_MY``. Kangaroo Express Malaysia. 213 | - ``ESP_CORREOS``. `Sociedad Estatal Correos y Telégrafos`_ 214 | - ``NL_KIALA``. KIALA Netherlands 215 | - ``IND_BLUEDART``. `Blue Dart Express DHL`_ 216 | - ``TUR_PTT``. PTT Turkey. 217 | - ``CA_CANNOT_PROVIDE_TRACKING``. Cannot provide tracking - Canada. 218 | - ``JPN_SAGAWA``. Sagawa Japan. 219 | - ``MYS_SKYNET``. Skynet Malaysia. 220 | - ``IT_FERCAM``. Fercam Italy. 221 | - ``UK_AIRBORNE_EXPRESS``. Airborne Express UK. 222 | - ``CA_OTHER``. Other - Canada. 223 | - ``DE_DEUTSHCE_POST_DHL_TRACK_TRACE_EXPRESS``. Deutsche Post DHL Track Trace Express Germany 224 | - ``CORREOS_DE_MEXICO``. `Mex Post Correos de Mexico`_ 225 | - ``FR_DHL_GLOBALFORWARDING``. DHL Global Forwarding France. 226 | - ``GLOBAL_SKYNET``. Skynet Global. 227 | - ``AU_DHL_GLOBALFORWARDING``. DHL Global Forwarding Australia. 228 | - ``DE_DHL_GLOBALFORWARDING``. DHL Global Forwarding Germany. 229 | - ``SFC_LOGISTICS``. `SFC Logistics`_ 230 | - ``US_GLOBEGISTICS``. Globeistics US. 231 | - ``CA_DHL_GLOBALFORWARDING``. DHL Global Forwarding Canada. 232 | - ``OMNIPARCEL``. Omni Parcel. 233 | - ``PHL_AIR21``. Air21 Philippines 234 | - ``CBL_LOGISTICA``. `CBL Logística`_ 235 | - ``FR_MONDIAL``. Mondial France. 236 | - ``DE_DHL_ECOMMERCE``. DHL eCommerce Germany. 237 | - ``ADICIONAL``. Adicional. 238 | - ``CH_SWISS_POST_PRIORITY``. Swiss Post Priority 239 | - ``NL_INTANGIBLE_DIGITAL_SERVICES``. Intangible Digital Services 240 | - ``DE_ASENDIA``. Asendia Germany. 241 | - ``NL_ABC_MAIL``. ABC Mail Netherlands 242 | - ``UK_DELTEC``. Deltec UK. 243 | - ``ONE_WORLD``. One World. 244 | - ``AIRBORNE_EXPRESS``. Airborne Express. 245 | - ``ES_OTHER``. Other - Spain. 246 | - ``US_DHL_ECOMMERCE``. `DHL eCommerce US`_ 247 | - ``US_ENSENDA``. Ensenda US. 248 | - ``CPACKET``. Cpacket. 249 | - ``AXL``. `AXL Express & Logistics`_ 250 | - ``IND_REDEXPRESS``. Red Express India. 251 | - ``NL_LOCAL_PICKUP``. Local PickUp Netherlands 252 | - ``UK_ROYALMAIL_AIRSURE``. Royal Mail AirSure UK 253 | - ``FR_TNT``. TNT France. 254 | - ``USPS``. `United States Postal Service (USPS)`_ 255 | - ``RINCOS``. Rincos. 256 | - ``B2CEUROPE``. B2C Europe 257 | - ``PHL_LBC``. LBC Philippines. 258 | - ``SG_TAQBIN``. TA-Q-BIN Parcel Singapore. 259 | - ``GR_ELTA``. Elta Greece. 260 | - ``WINIT``. WinIt. 261 | - ``NLD_DHL``. DHL Netherlands. 262 | - ``FR_GEODIS``. Geodis France. 263 | - ``DE_DHL_PACKET``. DHL Packet Germany. 264 | - ``ARG_OCA``. `OCA Argentia`_ 265 | - ``JP_DHL``. DHL Japan. 266 | - ``RUSSIAN_POST``. Russian Post. 267 | - ``TW_TAIWAN_POST``. `Chunghwa Post`_ 268 | - ``UPS``. `United Parcel Service of America, Inc.`_ 269 | - ``BE_BPOST``. Bpost Belgium 270 | - ``JP_SAGAWA_KYUU_BIN``. Sagawa Kyuu Bin Japan 271 | - ``NATIONWIDE_MY``. Nationwide Malaysia. 272 | - ``TNT``. TNT Portugal. 273 | - ``COURIERS_PLEASE``. Couriers Please. 274 | - ``DMM_NETWORK``. DMM Network. 275 | - ``TOLL``. `Toll`_ 276 | - ``NONE``. None 277 | - ``IDN_FIRST_LOGISTICS``. First Logistics Indonesia. 278 | - ``BH_POSTA``. `BH POŠTA`_ 279 | - ``SENDIT``. SendIt. 280 | - ``US_DHL_EXPRESS``. DHL Express US. 281 | - ``FEDEX``. FedEx. 282 | - ``SWE_POSTNORD``. `PostNord Sverige`_ 283 | - ``PHL_XEND_EXPRESS``. Xend Express Philippines. 284 | - ``POSTI``. Posti. 285 | - ``CA_CANADA_POST``. `Canada Post`_ 286 | - ``PL_FEXEX``. Fexex Poland 287 | - ``CN_EC``. EC China. 288 | - ``HK_TAQBIN``. TA-Q-BIN Parcel Hong Kong. 289 | - ``UK_AN_POST``. `AddressPay UK`_ 290 | - ``WISELOADS``. Wiseloads. 291 | - ``PRT_SEUR``. `Seur Portugal`_ 292 | - ``US_ONTRAC``. Ontrac US. 293 | - ``THA_THAILAND_POST``. Thailand Post. 294 | - ``DPE_EXPRESS``. DPE Express. 295 | - ``UK_DHL_EXPRESS``. DHL Express UK. 296 | - ``NL_DHL``. DHL Netherlands 297 | - ``HK_FLYT_EXPRESS``. Flyt Express Hong Kong 298 | - ``UK_HERMESWORLD``. Hermesworld UK. 299 | - ``IT_REGISTER_MAIL``. Registered Mail Italy. 300 | - ``ARG_CORREO``. `Correo Argentino`_ 301 | - ``CA_LOOMIS``. Loomis Express Canada 302 | - ``DTDC_AU``. DTDC Australia. 303 | - ``DPD``. DPD Global. 304 | - ``ASENDIA_HK``. Asendia Hong Kong. 305 | - ``UK_ROYALMAIL_RECORDED``. Royal Mail Recorded UK 306 | - ``PL_POCZTA_POLSKA``. Poczta Polska 307 | - ``EU_IMX``. IMX EU 308 | - ``IDN_PANDU``. Pandu Indonesia. 309 | - ``MEX_ESTAFETA``. `Estafeta Mexico`_ 310 | - ``SREKOREA``. `SRE Korea`_ 311 | - ``CYP_CYPRUS_POST``. `Cyprus Post`_ 312 | - ``NZ_COURIER_POST``. `CourierPost New Zealand`_ 313 | - ``CN_EMPS``. EMPS China. 314 | - ``AU_TNT``. TNT Australia. 315 | - ``UK_CANNOT_PROVIDE_TRACKING``. Cannot provide tracking - UK. 316 | - ``ES_DHL``. DHL Spain. 317 | - ``CONTINENTAL``. Continental. 318 | - ``IND_DTDC``. DTDC India. 319 | - ``DE_GLS``. `General Logistics Systems (GLS) Germany`_ 320 | - ``NLD_GLS``. `General Logistics Systems (GLS) Netherlands`_ 321 | - ``UK_DPD``. DPD UK. 322 | - ``IT_TNT``. `TNT Italy`_ 323 | - ``PL_DHL``. DHL Portugal. 324 | - ``JP_NITTSU_PELICAN_BIN``. Nittsu Pelican Bin Japan 325 | - ``THA_DYNAMIC_LOGISTICS``. Dynamic Logistics Thailand. 326 | - ``IT_POSTE_ITALIA``. Poste Italia 327 | - ``UK_ROYALMAIL_INTER_SIGNED``. Royal Mail International Signed UK 328 | - ``HERMES_IT``. Hermes Italy. 329 | - ``FR_BERT``. `Bert France`_ 330 | - ``IND_PROFESSIONAL_COURIERS``. Professional Couriers India. 331 | - ``POL_SIODEMKA``. Siodemka Poland. 332 | - ``IE_AN_POST_SDS_PRIORITY``. Post SDS Priority Ireland 333 | - ``ADSONE``. `ADSone Cumulus`_ 334 | - ``BRA_CORREIOS``. Correios Brazil. 335 | - ``UBI_LOGISTICS``. UBI Logistics. 336 | - ``ES_CORREOS``. `Sociedad Estatal Correos y Telégrafos`_ 337 | - ``NGA_NIPOST``. `Nigerian Postal Service`_ 338 | - ``AUT_AUSTRIAN_POST``. Austrian Post. 339 | - ``AU_FASTWAY``. Fastway Australia. 340 | - ``AUS_TOLL``. Toll Australia. 341 | - ``CA_CANPAR_COURIER``. Canpar Courier Canada. 342 | - ``SWE_DIRECTLINK``. `Direct Link Sweden`_ 343 | - ``CZE_CESKA``. `Česká pošta`_ 344 | - ``ROYAL_MAIL``. Royal Mail. 345 | - ``SG_SINGPOST``. SingPost Singapore 346 | - ``IT_OTHER``. Other - Italy. 347 | - ``ZA_FASTWAY``. `Fastway Couriers (South Africa)`_ 348 | - ``SEKOLOGISTICS``. Seko Logistics. 349 | - ``CN_UPS``. CN_UPS 350 | - ``HUNTER_EXPRESS``. Hunter Express. 351 | - ``DE_DHL_PARCEL``. DHL Parcel Germany. 352 | - ``NLD_TRANSMISSION``. Transmission Netherlands. 353 | - ``CN_TNT``. TNT China. 354 | - ``DE_DEUTSCHE``. Deutsche Post Germany. 355 | - ``AIRSURE``. Airsure. 356 | - ``UK_PARCELFORCE``. Parcelforce UK. 357 | - ``SWE_DB``. `DB Schenker Sweden`_ 358 | - ``CN_CHINA_POST``. China Post 359 | - ``PL_GLS``. General Logistics Systems Poland 360 | - ``EU_BPOST``. `bpost`_ 361 | - ``RELAIS_COLIS``. `Relais Colis`_ 362 | - ``UK_DHL_PARCEL``. DHL Parcel UK. 363 | - ``AUS_STARTRACK``. StarTrack Australia 364 | - ``AU_TOLL_IPEC``. Toll IPEC Australia 365 | - ``CORREOS_CHILE``. `CorreosChile`_ 366 | - ``CH_SWISS_POST_EXPRES``. Swiss Post Express 367 | - ``MYS_TAQBIN``. TA-Q-BIN Parcel Malaysia. 368 | - ``JET_SHIP``. Jetship. 369 | - ``HK_DHL_EXPRESS``. DHL Express Hong Kong. 370 | - ``IT_SDA``. `SDA Express Courier`_ 371 | - ``DE_DHL_DEUTSCHEPOST``. DHL Deutsche Post Germany. 372 | - ``HK_DHL_GLOBALFORWARDING``. DHL Global Forwarding Hong Kong. 373 | - ``PHL_RAF``. RAF Philippines. 374 | - ``IT_GLS``. `General Logistics Systems (GLS) Italy`_ 375 | - ``PANTOS``. Pantos. 376 | - ``KOR_ECARGO``. Ecargo Korea. 377 | - ``AT_AUSTRIAN_POST_EMS``. EMS Express Mail Service Austria 378 | - ``IT_BRT``. `BRT Corriere Espresso Italy`_ 379 | - ``CHE_SWISS_POST``. Swiss Post. 380 | - ``FASTWAY_NZ``. Fastway New Zealand. 381 | - ``IT_EBOOST_SDA``. IT_EBOOST_SDA 382 | - ``ASENDIA_UK``. Asendia UK. 383 | - ``RRDONNELLEY``. RR Donnelley. 384 | - ``US_RL``. RL US. 385 | - ``GR_GENIKI``. Geniki Greece. 386 | - ``DE_DHL_EXPRESS``. DHL Express Germany. 387 | - ``CA_GREYHOUND``. Greyhound Canada. 388 | - ``UK_COLLECTPLUS``. `CollectPlus UK`_ 389 | - ``NINJAVAN_THAI``. Ninjavan Thailand. 390 | - ``RABEN_GROUP``. Raben Group. 391 | - ``CA_DHL_EXPRESS``. DHL Express Canada. 392 | - ``GLOBAL_TNT``. TNT Global. 393 | - ``IN_INDIAPOST``. India Post 394 | - ``ITIS``. ITIS International 395 | - ``PHL_JAMEXPRESS``. JamExpress Philippines. 396 | - ``PRT_INT_SEUR``. `Internationational Seur Portugal`_ 397 | - ``ESP_ASM``. `Parcel Monitor Spain`_ 398 | - ``NINJAVAN_ID``. Ninjavan Indonesia. 399 | - ``JP_FEDEX``. Federal Express Japan 400 | - ``FR_CHRONOPOST``. Chronopost France. 401 | - ``FR_SUIVI``. Suivi FedEx France 402 | - ``FR_TELIWAY``. Teliway France. 403 | - ``JPN_JAPAN_POST``. `Japan Post`_ 404 | - ``HRV_HRVATSKA``. `Hrvatska Pošta`_ 405 | - ``AT_AUSTRIAN_POST_PAKET_PRIME``. Austrian Post Paket Prime 406 | - ``DE_OTHER``. Other - Germany. 407 | - ``HK_HONGKONG_POST``. Hong Kong Post. 408 | - ``GRC_ACS``. ACS Greece. 409 | - ``HUN_MAGYAR``. `Magyar Posta`_ 410 | - ``FR_DHL_PARCEL``. DHL Parcel France. 411 | - ``UK_OTHER``. Other - UK. 412 | - ``LWE_HK``. LWE Hong Kong. 413 | - ``EFS``. `Enterprise Freight Systems (EFS)`_ 414 | - ``PL_DHL_PARCEL``. DHL Parcel Poland. 415 | - ``PARCELFORCE``. Parcel Force. 416 | - ``AU_AUSTRALIA_POST_EMS``. Australia Post EMS 417 | - ``US_ASCENDIA``. Ascendia US. 418 | - ``ROU_POSTA``. `Poșta Română`_ 419 | - ``NZ_NZ_POST``. `New Zealand Post Limited (NZ)`_ 420 | - ``RPX``. RPX International. 421 | - ``POSTUR_IS``. Postur. 422 | - ``IE_AN_POST_SDS_EMS``. Post SDS EMS Express Mail Service Ireland 423 | - ``UK_UK_MAIL``. `UK Mail`_ 424 | - ``UK_FASTWAY``. Fastway UK. 425 | - ``CORREOS_DE_COSTA_RICA``. `Correos de Costa Rica`_ 426 | - ``MYS_CITYLINK``. Citylink Malaysia. 427 | - ``PUROLATOR``. Purolator. 428 | - ``IND_DOTZOT``. `DotZot India`_ 429 | - ``NG_COURIERPLUS``. `Courier Plus Nigeria`_ 430 | - ``HK_FOUR_PX_EXPRESS``. 4PX Express Hong Kong 431 | - ``ROCKETPARCEL``. `Rocket Parcel International`_ 432 | - ``CN_DHL_GLOBALFORWARDING``. DHL Global Forwarding China. 433 | - ``EPARCEL_KR``. EParcel Korea. 434 | - ``INPOST_PACZKOMATY``. InPost Paczkomaty. 435 | - ``KOR_KOREA_POST``. Korea Post. 436 | - ``CA_PUROLATOR``. Purolator Canada 437 | - ``APR_72``. APR 72. 438 | - ``FR_DHL_EXPRESS``. DHL Express France. 439 | - ``IDN_JNE``. JNE Indonesia. 440 | - ``AU_AUSTRALIA_POST_EPARCEL``. Australia Post Eparcel 441 | - ``GLOBAL_ESTES``. Estes Global. 442 | - ``LTU_LIETUVOS``. Lietuvos paštas Lithuania. 443 | - ``THECOURIERGUY``. `The Courier Guy`_ 444 | - ``BE_CHRONOPOST``. Chronopost Belgium. 445 | - ``VNM_VIETNAM_POST``. Vietnam Post. 446 | - ``AU_STAR_TRACK_EXPRESS``. StarTrack Express Australia 447 | - ``RAM``. `JP RAM Shipping`_ 448 | 449 | 450 | | **carrier_name_other** :dax-def-type:`string` 451 | 452 | | The name of the carrier for the shipment. Provide this value only if the carrier parameter is ``OTHER``. 453 | 454 | | **last_updated_time** :dax-def-type:`string` 455 | 456 | | The date and time when the tracking information was last updated, in `Internet date and time format`_. 457 | 458 | | :dax-def-meta:`Minimum length:` ``20``. 459 | 460 | | :dax-def-meta:`Maximum length:` ``64``. 461 | 462 | | :dax-def-meta:`Pattern:` ``^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$``. 463 | 464 | | **links** :dax-def-type:`array` (contains the `link_description`_ object) 465 | 466 | | An array of request-related `HATEOAS links`_. 467 | 468 | | **notify_buyer** :dax-def-type:`boolean` 469 | 470 | | If ``true``, sends an email notification to the buyer of the PayPal transaction. The email contains the tracking information that was uploaded through the API. 471 | 472 | | **postage_payment_id** :dax-def-type:`string` 473 | 474 | | The postage payment ID. 475 | 476 | | **quantity** :dax-def-type:`integer` 477 | 478 | | The quantity of items shipped. 479 | 480 | | **shipment_date** :dax-def-type:`string` 481 | 482 | | The date when the shipment occurred, in `Internet date and time format`_. 483 | 484 | | :dax-def-meta:`Minimum length:` ``20``. 485 | 486 | | :dax-def-meta:`Maximum length:` ``64``. 487 | 488 | | :dax-def-meta:`Pattern:` ``^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$``. 489 | 490 | 491 | | **tracking_number** :dax-def-type:`string` 492 | 493 | | The tracking number for the shipment. 494 | 495 | | **tracking_number_type** :dax-def-type:`enum` 496 | 497 | | The type of tracking number. 498 | 499 | | The possible values are: 500 | 501 | - ``CARRIER_PROVIDED``. A merchant-provided tracking number. 502 | - ``E2E_PARTNER_PROVIDED``. A marketplace-provided tracking number. 503 | 504 | | **tracking_number_validated** :dax-def-type:`boolean` 505 | 506 | | Indicates whether the carrier validated the tracking number. 507 | 508 | 509 | 510 | 511 | """ 512 | headers = {} 513 | contentType = "application/json" # headers = {'Content-Type': contentType} 514 | Utils.contentType(headers, contentType) 515 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(id), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 516 | 517 | @PaypalEndpoint('/v1/shipping/trackers/{}', method='GET') 518 | def get_tracking(self, id: str, **kwargs) -> ApiResponse: 519 | r""" 520 | :dax-operation-get:`GET` :dax-operation-path:`/v1/shipping/trackers/{id}` 521 | 522 | Shows tracking information, by tracker ID, for a PayPal transaction. 523 | 524 | \*\*\args: 525 | 526 | | **id** :dax-def-type:`string` :dax-def-note:`required` 527 | 528 | | The ID of the tracker in the ``transaction_id-tracking_number`` format. 529 | """ 530 | headers = {} 531 | contentType = "application/json" # headers = {'Content-Type': contentType} 532 | Utils.contentType(headers, contentType) 533 | return self._request(PaypalEndpointParams(kwargs.pop('path')).fill(id), params=kwargs, headers=headers) 534 | 535 | @PaypalEndpoint('/v1/shipping/trackers-batch', method='POST') 536 | def post_tracking(self, **kwargs) -> ApiResponse: 537 | r""" 538 | :dax-operation-get:`POST` :dax-operation-path:`/v1/shipping/trackers-batch` 539 | 540 | Adds tracking information, with or without tracking numbers, for multiple PayPal transactions. Accepts up to 20 tracking IDs. For more information, see `Add tracking information with tracking numbers`_ and `Add tracking information without tracking numbers`_. 541 | 542 | \*\*\kwargs: 543 | 544 | | **links** :dax-def-type:`array` (contains the `link_description`_ object) 545 | 546 | | An array of request-related `HATEOAS links`_. 547 | 548 | | **trackers** :dax-def-type:`array` (contains the `tracker`_ object) 549 | 550 | """ 551 | headers = {} 552 | contentType = "application/json" # headers = {'Content-Type': contentType} 553 | Utils.contentType(headers, contentType) 554 | return self._request(kwargs.pop('path'), data=Utils.convert_body(kwargs.pop('body'), False), params=kwargs, headers=headers) 555 | 556 | -------------------------------------------------------------------------------- /python_paypal_api/api/transactions.py: -------------------------------------------------------------------------------- 1 | from python_paypal_api.base import ( 2 | Client, 3 | PaypalEndpoint, 4 | PaypalEndpointParams, 5 | ApiResponse, 6 | Utils 7 | ) 8 | 9 | class Transactions(Client): 10 | r""" 11 | Use the /transactions resource to list transactions. 12 | """ 13 | 14 | @PaypalEndpoint('/v1/reporting/transactions', method='GET') 15 | def get_list_transactions(self, **kwargs) -> ApiResponse: 16 | ''' 17 | :dax-operation-get:`GET` :dax-operation-path:`/v1/reporting/transactions` 18 | 19 | Lists transactions. Specify one or more query parameters to filter the transaction that appear in the response. 20 | 21 | .. note:: 22 | * If you specify one or more optional query parameters, the ending_balance response field is empty. 23 | * It takes a maximum of three hours for executed transactions to appear in the list transactions call. 24 | * This call lists transaction for the previous three years. 25 | 26 | 27 | \*\*\kwargs: 28 | 29 | | **payment_instrument_type** :dax-def-type:`string` 30 | 31 | | Filters the transactions in the response by a payment instrument type. Value is either: 32 | 33 | - ``CREDITCARD``. Returns a direct credit card transaction with a corresponding value. 34 | 35 | - ``DEBITCARD``. Returns a debit card transaction with a corresponding value. 36 | 37 | | If you omit this parameter, the API does not apply this filter. 38 | 39 | | **end_date** :dax-def-type:`string` :dax-def-note:`required` 40 | 41 | | Filters the transactions in the response by an end date and time, in `Internet date and time format`_. Seconds are required. Fractional seconds are optional. The maximum supported range is 31 days. 42 | 43 | | :dax-def-meta:`Minimum length:` ``20``. 44 | 45 | | :dax-def-meta:`Maximum length:` ``64``. 46 | 47 | | :dax-def-meta:`Pattern:` ``^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$``. 48 | 49 | | **start_date** :dax-def-type:`string` :dax-def-note:`required` 50 | 51 | | Filters the transactions in the response by a start date and time, in `Internet date and time format`_. Seconds are required. Fractional seconds are optional. 52 | 53 | | :dax-def-meta:`Minimum length:` ``20``. 54 | 55 | | :dax-def-meta:`Maximum length:` ``64``. 56 | 57 | | :dax-def-meta:`Pattern:` ``^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$``. 58 | 59 | 60 | | **balance_affecting_records_only** :dax-def-type:`string` 61 | 62 | | Indicates whether the response includes only balance-impacting transactions or all transactions. 63 | 64 | | Value is either: 65 | 66 | - ``Y``. The default. The response includes only balance transactions. 67 | 68 | - ``N``. The response includes all transactions. 69 | 70 | 71 | | **fields** :dax-def-type:`string` 72 | 73 | | Indicates which fields appear in the response. Value is a single field or a comma-separated list of fields. The ``transaction_info`` value returns only the transaction details in the response. To include all fields in the response, specify ``fields=all``. Valid fields are: 74 | 75 | - ``transaction_info``. The transaction information. Includes the ID of the PayPal account of the payee, the PayPal-generated transaction ID, the PayPal-generated base ID, the PayPal reference ID type, the transaction event code, the date and time when the transaction was initiated and was last updated, the transaction amounts including the PayPal fee, any discounts, insurance, the transaction status, and other information about the transaction. 76 | 77 | - ``payer_info``. The payer information. Includes the PayPal customer account ID and the payer's email address, primary phone number, name, country code, address, and whether the payer is verified or unverified. 78 | 79 | - ``shipping_info``. The shipping information. Includes the recipient's name, the shipping method for this order, the shipping address for this order, and the secondary address associated with this order. 80 | 81 | - ``auction_info``. The auction information. Includes the name of the auction site, the auction site URL, the ID of the customer who makes the purchase in the auction, and the date and time when the auction closes. 82 | 83 | - ``cart_info``. The cart information. Includes an array of item details, whether the item amount or the shipping amount already includes tax, and the ID of the invoice for PayPal-generated invoices. 84 | 85 | - ``incentive_info``. An array of incentive detail objects. Each object includes the incentive, such as a special offer or coupon, the incentive amount, and the incentive program code that identifies a merchant loyalty or incentive program. 86 | 87 | - ``store_info``. The store information. Includes the ID of the merchant store and the terminal ID for the checkout stand in the merchant store. 88 | 89 | | **page** :dax-def-type:`integer` 90 | 91 | | The zero-relative start index of the entire list of items that are returned in the response. So, the combination of ``page=1`` and ``page_size=20`` returns the first 20 items. 92 | 93 | | :dax-def-meta:`Minimum value:` ``1``. 94 | 95 | | :dax-def-meta:`Maximum value:` ``2147483647``. 96 | 97 | | **page_size** :dax-def-type:`integer` 98 | 99 | | The number of items to return in the response. So, the combination of ``page=1`` and ``page_size=20`` returns the first 20 items. The combination of ``page=2`` and ``page_size=20`` returns the next 20 items. 100 | 101 | | :dax-def-meta:`Minimum value:` ``1``. 102 | 103 | | :dax-def-meta:`Maximum value:` ``500``. 104 | 105 | | **store_id** :dax-def-type:`string` 106 | 107 | | Filters the transactions in the response by a store ID. 108 | 109 | | **terminal_id** :dax-def-type:`string` 110 | 111 | | Filters the transactions in the response by a terminal ID. 112 | 113 | | **transaction_amount** :dax-def-type:`string` 114 | 115 | | Filters the transactions in the response by a gross transaction amount range. Specify the range as `` TO ``, where ```` is the lower limit of the gross PayPal transaction amount and ```` is the upper limit of the gross transaction amount. Specify the amounts in lower denominations. For example, to search for transactions from $5.00 to $10.05, specify ``[500 TO 1005]``. 116 | 117 | .. note:: 118 | 119 | The values must be URL encoded. 120 | 121 | | **transaction_currency** :dax-def-type:`string` 122 | 123 | | Filters the transactions in the response by a `three-character ISO-4217 currency code`_ for the PayPal transaction currency. 124 | 125 | | **transaction_id** :dax-def-type:`string` 126 | 127 | | Filters the transactions in the response by a PayPal transaction ID. A valid transaction ID is 17 characters long, except for an order ID, which is 19 characters long. 128 | 129 | .. note:: 130 | 131 | A transaction ID is not unique in the reporting system. The response can list two transactions with the same ID. One transaction can be balance affecting while the other is non-balance affecting. 132 | 133 | 134 | | :dax-def-meta:`Minimum length:` ``17``. 135 | 136 | | :dax-def-meta:`Maximum length:` ``19``. 137 | 138 | 139 | | **transaction_status** :dax-def-type:`string` 140 | 141 | | Filters the transactions in the response by a PayPal transaction status code. Value is: 142 | 143 | .. csv-table:: 144 | :header: "Status code", "Description" 145 | :widths: 5, 50 146 | 147 | "``D``", "PayPal or merchant rules denied the transaction." 148 | "``P``", "The transaction is pending. The transaction was created but waits for another payment process to complete, such as an ACH transaction, before the status changes to ``S``." 149 | "``S``", "The transaction successfully completed without a denial and after any pending statuses." 150 | "``V``", "A successful transaction was reversed and funds were refunded to the original sender." 151 | 152 | 153 | 154 | | **transaction_type** :dax-def-type:`string` 155 | 156 | | Filters the transactions in the response by a PayPal transaction event code. See `Transaction event codes`_. 157 | 158 | 159 | :return: ApiResponse 160 | ''' 161 | headers = {} 162 | contentType = "application/json" 163 | Utils.contentType(headers, contentType) 164 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) 165 | 166 | 167 | @PaypalEndpoint('/v1/reporting/balances', method='GET') 168 | def get_balances(self, **kwargs) -> ApiResponse: 169 | ''' 170 | :dax-operation-get:`GET` :dax-operation-path:`/v1/reporting/balances` 171 | 172 | List all balances. Specify date time to list balances for that time that appear in the response. 173 | 174 | .. note:: 175 | * It takes a maximum of three hours for balances to appear in the list balances call. 176 | * This call lists balances upto the previous three years. 177 | 178 | \*\*\kwargs: 179 | 180 | | **as_of_time** :dax-def-type:`string` 181 | 182 | | List balances in the response at the date time provided, will return the last refreshed balance in the system when not provided. 183 | 184 | | :dax-def-meta:`Minimum length:` ``20``. 185 | 186 | | :dax-def-meta:`Maximum length:` ``64``. 187 | 188 | | :dax-def-meta:`Pattern:` ``^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$``. 189 | 190 | | **currency_code** :dax-def-type:`string` 191 | 192 | | Filters the transactions in the response by a `three-character ISO-4217 currency code`_ for the PayPal transaction currency. 193 | 194 | | :dax-def-meta:`Minimum length:` ``3``. 195 | 196 | | :dax-def-meta:`Maximum length:` ``3``. 197 | 198 | 199 | ''' 200 | headers = {} 201 | contentType = "application/json" 202 | Utils.contentType(headers, contentType) 203 | return self._request(kwargs.pop('path'), params=kwargs, headers=headers) -------------------------------------------------------------------------------- /python_paypal_api/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .access_token_client import AccessTokenClient 2 | from .access_token_response import AccessTokenResponse 3 | from .credentials import Credentials 4 | from .exceptions import AuthorizationError 5 | 6 | __all__ = [ 7 | 'AccessTokenResponse', 8 | 'AccessTokenClient', 9 | 'Credentials', 10 | 'AuthorizationError' 11 | ] 12 | -------------------------------------------------------------------------------- /python_paypal_api/auth/access_token_client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests.auth import HTTPBasicAuth 3 | import hashlib 4 | import logging 5 | import confuse 6 | import os 7 | import logging 8 | import json 9 | from json.decoder import JSONDecodeError 10 | from datetime import datetime, timedelta 11 | from cachetools import TTLCache 12 | from cryptography.fernet import Fernet 13 | from cryptography.fernet import InvalidToken 14 | from python_paypal_api.base import BaseClient 15 | from python_paypal_api.base.enum import EndPoint 16 | from python_paypal_api.auth.credentials import Credentials 17 | from python_paypal_api.auth.access_token_response import AccessTokenResponse 18 | from python_paypal_api.auth.exceptions import AuthorizationError 19 | 20 | logging.basicConfig( 21 | level=logging.INFO, 22 | format="%(asctime)s:%(levelname)s:%(message)s" 23 | ) 24 | 25 | cache = TTLCache(maxsize=10, ttl=timedelta(seconds=32400), timer=datetime.now) 26 | 27 | class AccessTokenClient(BaseClient): 28 | 29 | grant_type = 'client_credentials' 30 | path = '/v1/oauth2/token' 31 | config = confuse.Configuration('python-paypal-api') 32 | flags = os.O_WRONLY | os.O_CREAT 33 | 34 | @classmethod 35 | def get_return_key(self, file_path: str = None): 36 | 37 | file = file_path or ".key" 38 | 39 | if file_path is not None: 40 | 41 | path_file = file_path 42 | 43 | else: 44 | 45 | path_file = self.config.config_dir() + "/" + file 46 | 47 | if os.path.exists(path_file): 48 | with open(path_file, 'rb') as filekey: 49 | key = filekey.read() 50 | else: 51 | 52 | head_tail = os.path.split(path_file) 53 | key_folder = confuse.Configuration(head_tail[0]) 54 | key = Fernet.generate_key() 55 | with os.fdopen(os.open(key_folder.config_dir() + "/" + head_tail[1], self.flags, 0o600), 'wb') as filekey: 56 | filekey.write(key) 57 | 58 | filekey.close() 59 | return key 60 | 61 | def __init__(self, 62 | credentials: str or dict = None, 63 | store_credentials: bool or dict = False, 64 | safe: bool = False, 65 | proxies: dict = None, 66 | verify: bool = True, 67 | timeout: int or float = None, 68 | debug: bool = False 69 | ): 70 | 71 | self.cred = Credentials(credentials) 72 | 73 | if isinstance(store_credentials, bool): 74 | self.store_credentials = store_credentials 75 | self.safe = safe 76 | if self.safe and self.store_credentials: 77 | self.key = self.get_return_key() 78 | 79 | elif isinstance(store_credentials, dict): 80 | self.store_credentials = True 81 | self.safe = store_credentials.get("safe") 82 | self.config = confuse.Configuration(store_credentials.get("token_path")) 83 | key_value = store_credentials.get("key_value") 84 | 85 | #TODO: make a real check dict configuration mapping 86 | # safe True or False, token path, key path or key value 87 | 88 | if key_value and self.safe: 89 | self.key = key_value 90 | elif key_value is None and self.safe: 91 | key_path_name = store_credentials.get("key_path_name") 92 | self.key = self.get_return_key(key_path_name) 93 | 94 | self.host = EndPoint[self.cred.client_mode].value if self.cred.client_mode is not None else EndPoint["SANDBOX"].value 95 | self.timeout = timeout 96 | self.proxies = proxies 97 | self.verify = verify 98 | self.debug = debug 99 | 100 | def delete_file_token(self): 101 | file = os.path.join(self.config.config_dir(), self._get_cache_key()) 102 | if (os.path.isfile(file)): 103 | os.remove(file) 104 | else: 105 | logging.info("file {} dont exist".format(file)) 106 | 107 | def _request(self, url, data, headers): 108 | 109 | response = requests.post( 110 | url, 111 | data=data, 112 | headers=headers, 113 | timeout=self.timeout, 114 | proxies=self.proxies, 115 | verify=self.verify, 116 | auth=HTTPBasicAuth(data["client_id"], data["client_secret"]) 117 | ) 118 | 119 | response_data = response.json() 120 | 121 | if response.status_code != 200: 122 | error_message = response_data.get('error_description') 123 | error_code = response_data.get('error') 124 | logging.info("access token client") 125 | raise AuthorizationError(error_code, error_message, response.status_code) 126 | 127 | return response_data 128 | 129 | def create_file_token(self, file:str): 130 | 131 | now_datetime = datetime.now() 132 | request_url = self.scheme + self.host + self.path 133 | 134 | try: 135 | 136 | access_token = self._request(request_url, self.data, self.headers) 137 | 138 | except AuthorizationError as error: 139 | logging.fatal(error) 140 | exit(0) 141 | 142 | future_datetime = now_datetime + timedelta(seconds=access_token["expires_in"]) 143 | access_token["expire_time"] = future_datetime.isoformat() 144 | 145 | if self.safe: 146 | 147 | fernet = Fernet(self.key) 148 | json_object = json.dumps(access_token) 149 | bytes_object = bytes(json_object, "utf-8") 150 | token = fernet.encrypt(bytes_object) 151 | 152 | with os.fdopen(os.open(file, self.flags, 0o600), 'wb') as fout: 153 | fout.write(token) 154 | 155 | else: 156 | 157 | json_object = json.dumps(access_token) 158 | with open(file, 'w') as filetoken: 159 | filetoken.write(json_object) 160 | 161 | return access_token 162 | 163 | 164 | def replace_content_unsafe(self, encrypted_token:str): 165 | 166 | file = os.path.join(self.config.config_dir(), self._get_cache_key()) 167 | fernet = Fernet(self.get_return_key()) 168 | decrypted_token = fernet.decrypt(encrypted_token) 169 | 170 | with os.fdopen(os.open(file, self.flags, 0o600), 'wb') as fout: 171 | fout.seek(0) 172 | fout.truncate() 173 | fout.write(decrypted_token) 174 | 175 | return json.loads(decrypted_token) 176 | 177 | def replace_content_safe(self, content:str): 178 | file = os.path.join(self.config.config_dir(), self._get_cache_key()) 179 | 180 | fernet = Fernet(self.key) 181 | bytes_object = bytes(content, "utf-8") 182 | token = fernet.encrypt(bytes_object) 183 | 184 | with os.fdopen(os.open(file, self.flags, 0o600), 'wb') as fout: 185 | fout.seek(0) 186 | fout.truncate() 187 | fout.write(token) 188 | 189 | return json.loads(content) 190 | 191 | def get_auth(self) -> AccessTokenResponse: 192 | 193 | if self.store_credentials: 194 | now_datetime = datetime.now() 195 | file = os.path.join(self.config.config_dir(), self._get_cache_key()) 196 | if self.safe: 197 | 198 | try : 199 | fernet = Fernet(self.key) 200 | except ValueError as error: 201 | 202 | logging.info(ValueError) 203 | logging.info(error) 204 | exit(0) 205 | 206 | try: 207 | 208 | openfile = open(file, 'r') 209 | str_encrypted_token = openfile.read() 210 | encrypted_token = bytes(str_encrypted_token, "utf-8") 211 | decrypted_token = fernet.decrypt(encrypted_token) 212 | logging.info(decrypted_token) 213 | access_token = json.loads(decrypted_token) 214 | openfile.close() 215 | 216 | except FileNotFoundError: 217 | 218 | access_token = self.create_file_token(file) 219 | if self.debug: 220 | logging.info("AUTH TOKEN >> FILE >> NOT FOUND >> CREATE") 221 | logging.info(access_token) 222 | future_datetime = now_datetime + timedelta(seconds=access_token["expires_in"]) 223 | 224 | except InvalidToken as error: 225 | 226 | logging.info(InvalidToken) 227 | access_token = self.replace_content_safe(str_encrypted_token) 228 | # exit(0) 229 | 230 | else: 231 | 232 | #TODO: Avoid open file 2 times and consider if move from encrypt / decrypt has sense 233 | 234 | try: 235 | 236 | openfile = open(file, 'r') 237 | access_token = json.load(openfile) 238 | openfile.close() 239 | 240 | except JSONDecodeError: 241 | 242 | logging.error(JSONDecodeError) 243 | logging.error(openfile) 244 | openfile = open(file, 'r') 245 | access_token = self.replace_content_unsafe(openfile.read()) 246 | openfile.close() 247 | # exit(0) 248 | 249 | except FileNotFoundError: 250 | 251 | access_token = self.create_file_token(file) 252 | if self.debug: 253 | logging.info("AUTH TOKEN >> FILE >> NOT FOUND >> CREATE") 254 | logging.info(access_token) 255 | 256 | future_datetime = datetime.fromisoformat(access_token["expire_time"]) 257 | 258 | if self.debug: 259 | logging.info("AUTH TOKEN >> FILE >> READ expires({})".format(access_token.get("expire_time"))) 260 | logging.info(access_token) 261 | 262 | if now_datetime > future_datetime: 263 | self.delete_file_token() 264 | access_token = self.create_file_token(file) 265 | 266 | else: 267 | pass 268 | 269 | else: 270 | 271 | cache_key = self._get_cache_key() 272 | try: 273 | access_token = cache[cache_key] 274 | if self.debug: 275 | logging.info("AUTH TOKEN >> CACHE >> READ") 276 | logging.info(access_token) 277 | except KeyError: 278 | request_url = self.scheme + self.host + self.path 279 | access_token = self._request(request_url, self.data, self.headers) 280 | 281 | if self.debug: 282 | logging.info("AUTH TOKEN >> CACHE >> KEY_ERROR >> CREATE") 283 | logging.info(access_token) 284 | cache[cache_key] = access_token 285 | 286 | return AccessTokenResponse(**access_token) 287 | 288 | def authorize_auth_code(self, auth_code): 289 | request_url = self.scheme + self.host + self.path 290 | res = self._request( 291 | request_url, 292 | data=self._auth_code_request_body(auth_code), 293 | headers=self.headers 294 | ) 295 | return res 296 | 297 | def _auth_code_request_body(self, auth_code): 298 | return { 299 | 'grant_type': 'client_credentials', 300 | 'client_id': self.cred.client_id, 301 | 'client_secret': self.cred.client_secret 302 | } 303 | 304 | @property 305 | def data(self): 306 | return { 307 | 'grant_type': self.grant_type, 308 | 'client_id': self.cred.client_id, 309 | 'client_secret': self.cred.client_secret 310 | } 311 | 312 | @property 313 | def headers(self): 314 | return { 315 | 'User-Agent': self.user_agent, 316 | 'content-type': self.content_type 317 | } 318 | 319 | def _get_cache_key(self, token_flavor=''): 320 | 321 | return '.' + hashlib.md5( 322 | (token_flavor + self.cred.client_id).encode('utf-8') 323 | ).hexdigest() -------------------------------------------------------------------------------- /python_paypal_api/auth/access_token_response.py: -------------------------------------------------------------------------------- 1 | class AccessTokenResponse: 2 | def __init__(self, **kwargs): 3 | self.scope = kwargs.get('scope') 4 | self.access_token = kwargs.get('access_token') 5 | self.token_type = kwargs.get('token_type') 6 | self.app_id = kwargs.get('app_id') 7 | self.expires_in = kwargs.get('expires_in') 8 | self.nonce = kwargs.get('nonce') 9 | 10 | -------------------------------------------------------------------------------- /python_paypal_api/auth/credentials.py: -------------------------------------------------------------------------------- 1 | class Credentials: 2 | def __init__(self, credentials): 3 | self.client_id = credentials.client_id 4 | self.client_secret = credentials.client_secret 5 | self.client_mode = credentials.client_mode -------------------------------------------------------------------------------- /python_paypal_api/auth/exceptions.py: -------------------------------------------------------------------------------- 1 | class AuthorizationError(Exception): 2 | """ 3 | Authorization Error 4 | 5 | Parameters: 6 | 7 | error_code: str Error code from paypal auth api 8 | error_msg: str Error sm 9 | status_code: integer Response status code from paypal auth api 10 | """ 11 | 12 | def __init__(self, error_code, error_msg, status_code): 13 | self.error_code = error_code 14 | self.message = error_msg 15 | self.status_code = status_code 16 | -------------------------------------------------------------------------------- /python_paypal_api/base/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_client import BaseClient 2 | from .client import Client 3 | from .helpers import PaypalEndpoint, PaypalEndpointParams 4 | from .exceptions import PaypalApiException, PaypalTypeException 5 | from .credential_provider import CredentialProvider, MissingCredentials 6 | from .api_response import ApiResponse 7 | from .utils import Utils 8 | from .enum import ProductType, CategoryType, EndPoint, ShipmentStatus, Carrier, TrackingNumberType 9 | 10 | __all__ = [ 11 | 'AccessTokenClient', 12 | 'ApiResponse', 13 | 'Client', 14 | 'BaseClient', 15 | 'PaypalEndpointParams', 16 | 'PaypalEndpoint', 17 | 'PaypalApiException', 18 | 'PaypalTypeException', 19 | 'CredentialProvider', 20 | 'MissingCredentials', 21 | 'Utils', 22 | 'ProductType', 23 | 'CategoryType', 24 | 'EndPoint', 25 | 'ShipmentStatus', 26 | 'Carrier', 27 | 'TrackingNumberType' 28 | ] 29 | -------------------------------------------------------------------------------- /python_paypal_api/base/api_response.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | 4 | class ApiResponse(): 5 | 6 | def __init__(self, payload=None, authType=None, currentAuth=None, storeCredentials=None, **kwargs): 7 | 8 | # self.payload = payload or kwargs # will return headers if empty response 9 | self.payload = payload 10 | self.headers = kwargs 11 | self.auth_type = authType 12 | self.current_token = currentAuth if authType == "Bearer" else None 13 | self.current_basic = currentAuth if authType == "Basic" else None 14 | self.info = storeCredentials 15 | 16 | def __str__(self): 17 | return pprint.pformat(self.__dict__) 18 | 19 | 20 | def current_token(self): 21 | return self.current_token 22 | 23 | def current_basic(self): 24 | return self.current_basic 25 | -------------------------------------------------------------------------------- /python_paypal_api/base/base_client.py: -------------------------------------------------------------------------------- 1 | import python_paypal_api.version as vd 2 | 3 | class BaseClient: 4 | scheme = 'https://' 5 | method = 'GET' 6 | content_type = 'application/x-www-form-urlencoded;charset=UTF-8' 7 | user_agent = 'python-paypal-api' 8 | version = vd.__version__ 9 | 10 | def __init__(self): 11 | 12 | try: 13 | version = vd.__version__ 14 | self.user_agent += f'-{version}' 15 | 16 | except: 17 | pass 18 | 19 | -------------------------------------------------------------------------------- /python_paypal_api/base/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from requests import request 4 | # from requests.auth import HTTPBasicAuth 5 | from requests.exceptions import HTTPError 6 | from python_paypal_api.auth.credentials import Credentials 7 | from python_paypal_api.auth import AccessTokenClient, AccessTokenResponse 8 | from python_paypal_api.base.credential_provider import CredentialProvider 9 | from python_paypal_api.base.api_response import ApiResponse 10 | from python_paypal_api.base.base_client import BaseClient 11 | from python_paypal_api.base.enum import EndPoint 12 | from python_paypal_api.base.exceptions import GetExceptionForCode 13 | import os 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | class Client(BaseClient): 18 | 19 | def __new__( 20 | cls, 21 | *args, 22 | **kwargs 23 | ): 24 | if not hasattr(cls, 'instance'): 25 | cls.instance = super(Client, cls).__new__(cls) 26 | return cls.instance 27 | 28 | 29 | def __init__( 30 | self, 31 | credentials: str or dict or tuple or list = "default", 32 | store_credentials: bool or dict = False, 33 | safe: bool = False, 34 | proxies: dict = None, 35 | verify: bool = True, 36 | timeout: int or float = None, 37 | debug: bool = False 38 | ): 39 | 40 | super().__init__() 41 | self.debug = debug 42 | self.store_credentials = store_credentials 43 | 44 | if isinstance(credentials, tuple): 45 | self.credentials = credentials 46 | try: 47 | mode = credentials[2] 48 | except IndexError as error: 49 | mode = None 50 | 51 | self.host = EndPoint[mode].value if mode is not None else EndPoint["SANDBOX"].value 52 | 53 | else: 54 | 55 | self.credentials = CredentialProvider( 56 | credentials, 57 | debug=self.debug 58 | ).credentials 59 | 60 | self._auth = AccessTokenClient( 61 | credentials=self.credentials, 62 | store_credentials=self.store_credentials, 63 | safe=safe, 64 | proxies=proxies, 65 | verify=verify, 66 | timeout=timeout, 67 | debug=self.debug 68 | ) 69 | 70 | self.host = EndPoint[self.credentials.client_mode].value if self.credentials.client_mode is not None else EndPoint["SANDBOX"].value 71 | 72 | self.endpoint = self.scheme + self.host 73 | self.timeout = timeout 74 | self.proxies = proxies 75 | self.verify = verify 76 | 77 | @property 78 | def headers(self): 79 | return { 80 | 'User-Agent': self.user_agent, 81 | 'Authorization': 'Bearer %s' % self.auth.access_token, 82 | } 83 | 84 | @property 85 | def basic(self): 86 | 87 | return { 88 | 'User-Agent': self.user_agent, 89 | } 90 | 91 | @property 92 | def get_store_credentials(self): 93 | 94 | if isinstance(self.credentials, tuple): 95 | client_id = self.credentials[0] 96 | else: 97 | client_id = self.credentials.client_id 98 | 99 | return { 100 | 'Store-Credentials': self.store_credentials, 101 | 'End-Point': self.endpoint, 102 | # 'Client-Id': self.credentials.client_id if self.credentials.client_id is not None else self.credentials[0] 103 | 'Client-Id': client_id 104 | } 105 | return self.store_credentials 106 | 107 | @property 108 | def auth(self) -> AccessTokenResponse: 109 | return self._auth.get_auth() 110 | 111 | def _request(self, 112 | path: str, 113 | data: str = None, 114 | files = None, 115 | params: dict = None, 116 | headers = None, 117 | ) -> ApiResponse: 118 | 119 | method = params.pop('method') 120 | 121 | if headers is not None and isinstance(self.credentials, tuple): 122 | base_header = self.basic.copy() 123 | base_header.update(headers) 124 | basic_header = base_header 125 | 126 | else: 127 | 128 | base_header = self.headers.copy() 129 | base_header.update(headers) 130 | token_headers = base_header 131 | 132 | request_data = data if method in ('POST', 'PUT', 'PATCH') else None 133 | 134 | res = request( 135 | method, 136 | self.endpoint + path, 137 | params=params, 138 | files=files, 139 | data=request_data, 140 | headers=basic_header if isinstance(self.credentials, tuple) else token_headers, 141 | timeout=self.timeout, 142 | proxies=self.proxies, 143 | verify=self.verify, 144 | auth=( 145 | self.credentials[0], 146 | self.credentials[1] 147 | ) if isinstance(self.credentials, tuple) else None 148 | ) 149 | 150 | if self.debug: 151 | logging.info(res.request.headers) 152 | 153 | if params: 154 | message = method + " " + res.request.url 155 | else: 156 | message = method + " " + self.endpoint + path 157 | 158 | logging.info(message) 159 | if data is not None: 160 | logging.info(data) 161 | if files is not None: 162 | logging.info(files) 163 | 164 | return self._check_response(res) 165 | 166 | 167 | def _check_response(self, res) -> ApiResponse: 168 | 169 | if self.debug: 170 | logging.info(vars(res)) 171 | 172 | content = vars(res).get('_content') 173 | headers = vars(res).get('headers') 174 | status_code = vars(res).get('status_code') 175 | 176 | if status_code == 204: 177 | data = None 178 | else: 179 | str_content = content.decode('utf8') 180 | data = json.loads(str_content) 181 | 182 | if status_code == 400: 183 | dictionary = {"name": data["name"], "status_code": vars(res).get('status_code'), "message": data["message"], "details": data["details"]} 184 | exception = GetExceptionForCode(status_code).get_class_exception() 185 | raise exception(dictionary, headers=headers) 186 | 187 | if status_code == 401: 188 | dictionary = {"error": data["error"], "error_description":data["error_description"], "status_code": status_code} 189 | exception = GetExceptionForCode(status_code).get_class_exception() 190 | raise exception(dictionary, headers=headers) 191 | 192 | if status_code == 422: 193 | # UNPROCESSABLE_ENTITY (The requested action could not be performed, semantically incorrect, or failed business validation.) 194 | dictionary = {"name": data["name"], "status_code": vars(res).get('status_code'), "details": data["details"]} 195 | exception = GetExceptionForCode(status_code).get_class_exception() 196 | raise exception(dictionary, headers=headers) 197 | 198 | if status_code == 404: 199 | # RESOURCE_NOT_FOUND (The specified resource does not exist.) 200 | dictionary = {"name": data["name"], "status_code": vars(res).get('status_code'), "details": data["details"]} 201 | exception = GetExceptionForCode(status_code).get_class_exception() 202 | raise exception(dictionary, headers=headers) 203 | 204 | if status_code == 403: 205 | # RESOURCE_NOT_FOUND (The specified resource does not exist.) 206 | dictionary = {"name": data["name"], "status_code": vars(res).get('status_code'), "details": data["details"]} 207 | exception = GetExceptionForCode(status_code).get_class_exception() 208 | raise exception(dictionary, headers=headers) 209 | 210 | authorization = res.request.headers.get("Authorization") 211 | 212 | result_authorization = authorization.split() 213 | 214 | return ApiResponse(data, 215 | result_authorization[0], 216 | result_authorization[1], 217 | self.get_store_credentials, 218 | headers=headers 219 | ) 220 | 221 | -------------------------------------------------------------------------------- /python_paypal_api/base/config.py: -------------------------------------------------------------------------------- 1 | class BaseConfig: 2 | def __init__(self, client_id, client_secret): 3 | self.client_id = client_id 4 | self.client_secret = client_secret 5 | 6 | def check_config(self): 7 | errors = [] 8 | for k, v in self.__dict__.items(): 9 | if not v and k != 'refresh_token': 10 | errors.append(k) 11 | return errors 12 | 13 | 14 | class Config(BaseConfig): 15 | def __init__(self, client_id, client_secret): 16 | super().__init__(client_id, client_secret) 17 | -------------------------------------------------------------------------------- /python_paypal_api/base/credential_provider.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import functools 3 | import json 4 | import os 5 | import pprint 6 | 7 | from typing import Dict, Iterable, Optional, Type 8 | 9 | import confuse 10 | import logging 11 | 12 | logger = logging.getLogger(__name__) 13 | required_credentials = [ 14 | 'client_id', 15 | 'client_secret' 16 | ] 17 | 18 | 19 | class MissingCredentials(Exception): 20 | """ 21 | Credentials are missing, see the error output to find possible causes 22 | """ 23 | pass 24 | 25 | 26 | class BaseCredentialProvider(abc.ABC): 27 | 28 | def __init__(self, credentials: dict or str, *args, **kwargs): 29 | self.credentials = credentials 30 | 31 | @abc.abstractmethod 32 | def load_credentials(self): 33 | pass 34 | 35 | 36 | def _get_env(self, key): 37 | return os.environ.get(f'{key}', 38 | os.environ.get(key)) 39 | 40 | def check_credentials(self): 41 | try: 42 | self.errors = [c for c in required_credentials if 43 | c not in self.credentials.keys() or not self.credentials[c]] 44 | except (AttributeError, TypeError): 45 | raise MissingCredentials(f'Credentials are missing: {", ".join(required_credentials)}') 46 | if not len(self.errors): 47 | return self.credentials 48 | raise MissingCredentials(f'Credentials are missing: {", ".join(self.errors)}') 49 | 50 | 51 | 52 | class FromEnvCredentialProvider(BaseCredentialProvider): 53 | 54 | def __init__(self, *args, **kwargs): 55 | pass 56 | 57 | def load_credentials(self): 58 | account_data = dict( 59 | client_id=self._get_env('client_id'), 60 | client_secret=self._get_env('client_secret'), 61 | client_mode=self._get_env('client_mode'), 62 | ) 63 | self.credentials = account_data 64 | self.check_credentials() 65 | return self.credentials 66 | 67 | class FromCodeCredentialProvider(BaseCredentialProvider): 68 | 69 | def __init__(self, credentials: dict, *args, **kwargs): 70 | super().__init__(credentials) 71 | 72 | def load_credentials(self): 73 | self.check_credentials() 74 | return self.credentials 75 | 76 | 77 | class FromConfigFileCredentialProvider(BaseCredentialProvider): 78 | 79 | folder = 'python-paypal-api' 80 | # file = 'credentials.yml' Moved to default confuse config.yaml 81 | file = None 82 | 83 | def __init__(self, credentials: str, *args, **kwargs): 84 | 85 | self.credentials = credentials 86 | 87 | if kwargs.get("config_folder") is not None: 88 | self.folder = kwargs.get("config_folder") 89 | 90 | if kwargs.get("config_filename") is not None: 91 | self.file = kwargs.get("config_filename") 92 | 93 | def load_credentials(self): 94 | 95 | config = confuse.Configuration(self.folder) 96 | 97 | if self.file is None: 98 | 99 | # TODO: make a debug if needed to show which configuration is being loaded 100 | # logging.info("Must load a default config.yaml in the folder: {}".format(self.folder)) 101 | 102 | if os.path.isfile(config.user_config_path()): 103 | logging.info(config.user_config_path()) 104 | else: 105 | logging.fatal("The configuration file {} is not in the folder {}".format(os.path.split(config.user_config_path())[1], config.config_dir())) 106 | exit(0) 107 | 108 | else: 109 | 110 | # logging.info("Must load a file named: {} in the folder {}".format(self.file, os.path.split(config.user_config_path())[0])) 111 | try: 112 | 113 | config.set_file(config.config_dir() + "/" + self.file) 114 | 115 | except confuse.exceptions.ConfigReadError as err: 116 | logging.error(confuse.exceptions.ConfigReadError) 117 | logging.error(err) 118 | # pass 119 | 120 | except Exception: 121 | logging.error(Exception) 122 | 123 | 124 | # valid template 125 | 126 | template = \ 127 | { 128 | 'configuration': confuse.MappingValues({ 129 | 'client_id': confuse.String(pattern='^[A-Za-z0-9-_]{80,80}$'), 130 | 'client_secret': confuse.String(pattern='^[A-Za-z0-9-_]{80,80}$'), 131 | }), 132 | } 133 | 134 | try: 135 | valid_config = config.get(template) 136 | except confuse.ConfigError as err: 137 | logging.error(confuse.ConfigError) 138 | logging.error(err) 139 | exit(0) 140 | except Exception: 141 | # print(Exception) 142 | logging.error(Exception) 143 | 144 | 145 | # TODO check if possible remove level configuration or rename to configurations 146 | 147 | try: 148 | account_data = config["configuration"][self.credentials].get() 149 | except (confuse.exceptions.NotFoundError, 150 | confuse.exceptions.ConfigReadError, 151 | confuse.exceptions.ConfigValueError, 152 | confuse.exceptions.ConfigTemplateError 153 | ) as error: 154 | logging.error(confuse.exceptions.NotFoundError) 155 | logging.error(error) 156 | exit(0) 157 | 158 | super().__init__(account_data) 159 | self.check_credentials() 160 | return (account_data) 161 | 162 | 163 | class CredentialProvider(): 164 | credentials = None 165 | debug = False 166 | 167 | def load_credentials(self): 168 | pass 169 | 170 | 171 | def __init__( 172 | self, 173 | credentials: str or dict, 174 | debug 175 | ): 176 | 177 | self.debug = debug 178 | 179 | client_id_env = os.environ.get('client_id') 180 | client_secret_env = os.environ.get('client_secret') 181 | 182 | if client_id_env is not None and client_secret_env is not None: 183 | 184 | try: 185 | self.credentials = FromEnvCredentialProvider().load_credentials() 186 | if self.debug: 187 | logging.info("CREDENTIALS MODE > ENVIRONMENT ({}, {})".format(self.credentials.get("client_id")[:10]+"*", 188 | self.credentials.get("client_mode"))) 189 | except MissingCredentials: 190 | logging.error("MissingCredentials") 191 | logging.error(MissingCredentials) 192 | 193 | 194 | elif isinstance(credentials, dict): 195 | try: 196 | self.credentials = FromCodeCredentialProvider(credentials).load_credentials() 197 | if self.debug: 198 | logging.info("CREDENTIALS MODE > CODE ({}, {})".format(self.credentials.get("client_id")[:10]+"*", 199 | self.credentials.get("client_mode"))) 200 | except MissingCredentials: 201 | logging.error("MissingCredentials") 202 | logging.error(MissingCredentials) 203 | 204 | 205 | elif isinstance(credentials, str): 206 | try: 207 | self.credentials = FromConfigFileCredentialProvider(credentials).load_credentials() 208 | if self.debug: 209 | logging.info("CREDENTIALS MODE > ACCOUNT ({}, {})".format(self.credentials.get("client_id")[:10]+"*", 210 | self.credentials.get("client_mode"))) 211 | except MissingCredentials: 212 | logging.error("MissingCredentials") 213 | logging.error(MissingCredentials) 214 | 215 | elif isinstance(credentials, list): 216 | 217 | # TODO: Find a proper validation 218 | account = credentials[0] 219 | _config_folder = credentials[1] 220 | # credentials[2] is optional 221 | try: 222 | _config_filename = credentials[2] 223 | except IndexError as error: 224 | # logging.info("The Configuration do not provide a file") 225 | _config_filename = None 226 | 227 | try: 228 | self.credentials = FromConfigFileCredentialProvider(account, config_folder=_config_folder, config_filename=_config_filename).load_credentials() 229 | if self.debug: 230 | logging.info("CREDENTIALS MODE > CUSTOM ({}, {})".format(self.credentials.get("client_id")[:10]+"*", 231 | self.credentials.get("client_mode"))) 232 | except MissingCredentials: 233 | logging.error("MissingCredentials") 234 | logging.error(MissingCredentials) 235 | 236 | 237 | else: 238 | raise MissingCredentials 239 | 240 | self.credentials = self.Config(**self.credentials) 241 | 242 | class Config: 243 | def __init__(self, **kwargs): 244 | self.client_id = kwargs.get('client_id') 245 | self.client_secret = kwargs.get('client_secret') 246 | self.client_mode = kwargs.get('client_mode') -------------------------------------------------------------------------------- /python_paypal_api/base/exceptions.py: -------------------------------------------------------------------------------- 1 | class PaypalApiException(Exception): 2 | code = 999 3 | 4 | def __init__(self, error, headers): 5 | 6 | # print(self.code) 7 | 8 | try: 9 | self.message = error.get('message') 10 | self.details = error.get('details') 11 | self.paypal_code = error.get('status_code') 12 | except IndexError: 13 | pass 14 | 15 | self.error = error 16 | self.headers = headers 17 | 18 | 19 | class PaypalTypeException(TypeError): 20 | def __init__(self, error): 21 | super(TypeError, self).__init__(error) 22 | 23 | class PaypalApiRequestException(PaypalApiException): 24 | code = 400 25 | 26 | def __init__(self, error, headers): 27 | 28 | try: 29 | self.name = error.get('name') 30 | self.message = error.get('message') 31 | self.details = error.get('details') 32 | self.paypal_code = error.get('status_code') 33 | except IndexError: 34 | pass 35 | 36 | self.error = error 37 | self.headers = headers 38 | 39 | 40 | class PaypalApiBadRequestException(PaypalApiRequestException): 41 | """ 42 | 400 Request has missing or invalid parameters and cannot be parsed. 43 | """ 44 | def __init__(self, error, headers=None): 45 | super(PaypalApiBadRequestException, self).__init__(error, headers) 46 | 47 | class PaypalApiUnprocessableEntityException(PaypalApiRequestException): 48 | """ 49 | 400 Request has missing or invalid parameters and cannot be parsed. 50 | """ 51 | def __init__(self, error, headers=None): 52 | super(PaypalApiUnprocessableEntityException, self).__init__(error, headers) 53 | 54 | 55 | class PaypalApiForbiddenException(PaypalApiException): 56 | """ 57 | 401 Indicates access to the resource is forbidden. Possible reasons include Access Denied, Unauthorized, Expired Token, or Invalid Signature. 58 | """ 59 | 60 | code = 401 61 | 62 | def __init__(self, error, headers=None): 63 | super(PaypalApiForbiddenException, self).__init__(error, headers) 64 | 65 | 66 | class PaypalApiResourceNotFound(PaypalApiException): 67 | """ 68 | 401 Indicates access to the resource is forbidden. Possible reasons include Access Denied, Unauthorized, Expired Token, or Invalid Signature. 69 | """ 70 | def __init__(self, error, headers=None): 71 | super(PaypalApiResourceNotFound, self).__init__(error, headers) 72 | 73 | 74 | class GetExceptionForCode(): 75 | 76 | def __init__(self, code: int): 77 | self.code = code 78 | # print("GetExceptionForCode"+str(code)) 79 | 80 | def get_class_exception(self): 81 | 82 | ''' 83 | print({ 84 | 400: PaypalApiBadRequestException, 85 | 422: PaypalApiUnprocessableEntityException, 86 | 401: PaypalApiForbiddenException, 87 | 403: PaypalApiForbiddenException, 88 | 404: PaypalApiResourceNotFound 89 | }.get(self.code, PaypalApiException)) 90 | 91 | ''' 92 | 93 | return { 94 | 400: PaypalApiBadRequestException, 95 | 422: PaypalApiUnprocessableEntityException, 96 | 401: PaypalApiForbiddenException, 97 | 403: PaypalApiForbiddenException, 98 | 404: PaypalApiResourceNotFound 99 | }.get(self.code, PaypalApiException) 100 | 101 | 102 | def get_exception_for_code(code: int): 103 | return { 104 | 400: PaypalApiBadRequestException, 105 | 422: PaypalApiUnprocessableEntityException, 106 | 401: PaypalApiForbiddenException, 107 | 403: PaypalApiForbiddenException, 108 | 404: PaypalApiResourceNotFound 109 | }.get(code, PaypalApiException) 110 | 111 | 112 | 113 | 114 | def get_exception_for_content(content: object): 115 | return { 116 | 'UNAUTHORIZED': PaypalApiForbiddenException 117 | }.get(content.get('code'), PaypalApiException) 118 | -------------------------------------------------------------------------------- /python_paypal_api/base/helpers.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from typing import TypeVar 3 | import functools 4 | import hashlib 5 | import base64 6 | from python_paypal_api.base.enum import EndPoint 7 | import requests 8 | import logging 9 | import confuse 10 | import os 11 | from python_paypal_api.auth.exceptions import AuthorizationError 12 | 13 | 14 | class PaypalEndpoint(object): 15 | 16 | def __init__(self, path, method="GET"): 17 | self.path = path 18 | self.method = method 19 | 20 | def __call__(self, fn, *args, **kwargs): 21 | @functools.wraps(fn) 22 | def decorated(*args, **kwargs): 23 | kwargs.update({"path": self.path, "method": self.method}) 24 | result = fn(*args, **kwargs) 25 | return result 26 | return decorated 27 | 28 | 29 | class PaypalEndpointParams(object): 30 | def __init__(self, query: str): 31 | self.query = query 32 | def fill(self, *args): 33 | return self.query.format(*args) 34 | 35 | 36 | class BaseTokenTerminate(): 37 | user_agent = 'python-paypal-api' 38 | content_type = 'application/x-www-form-urlencoded;charset=UTF-8' 39 | 40 | 41 | class AccessTokenTerminate(BaseTokenTerminate): 42 | grant_type = 'client_credentials' # refresh_token (refresh_token), authorization_code (code) 43 | token_type_hint = 'ACCESS_TOKEN' 44 | path = '/v1/oauth2/token/terminate' 45 | config = confuse.Configuration('python-paypal-api') 46 | 47 | @property 48 | def headers(self): 49 | return { 50 | 'User-Agent': self.user_agent, 51 | 'Authorization': 'Bearer %s' % self.token, 52 | 'Content-Type': self.content_type 53 | } 54 | 55 | def __init__(self, 56 | token: str = None, 57 | mode: dict = None, 58 | proxies: dict = None, 59 | verify: bool = True, 60 | timeout: int or float = None, 61 | debug: bool = False): 62 | self.token = token 63 | self.host = mode.get("End-Point") if mode.get("End-Point") is not None else EndPoint["SANDBOX"].value 64 | self.store_credentials = mode.get("Store-Credentials") 65 | self.client_id = mode.get("Client-Id") 66 | self.timeout = timeout 67 | self.proxies = proxies 68 | self.verify = verify 69 | self.debug = debug 70 | 71 | @property 72 | def values(self): 73 | return { 74 | 'token': self.token, 75 | 'token_type_hint': self.token_type_hint 76 | } 77 | 78 | 79 | def get_link_file(self, token_flavor=''): 80 | 81 | return '.' + hashlib.md5( 82 | (token_flavor + self.client_id).encode('utf-8') 83 | ).hexdigest() 84 | 85 | 86 | def delete_file_token(self): 87 | file = os.path.join(self.config.config_dir(), self.get_link_file()) 88 | if (os.path.isfile(file)): 89 | os.remove(file) 90 | else: 91 | logging.info("file {} dont exist".format(file)) 92 | 93 | def terminate(self): 94 | 95 | url = self.host + self.path 96 | 97 | response = requests.post( 98 | url, 99 | data=self.values, 100 | headers=self.headers, 101 | timeout=self.timeout, 102 | proxies=self.proxies, 103 | verify=self.verify, 104 | ) 105 | 106 | if response.status_code != 200: 107 | response_data = response.json() 108 | error_message = response_data.get('error_description') 109 | error_code = response_data.get('error') 110 | raise AuthorizationError(error_code, error_message, response.status_code) 111 | 112 | else: 113 | 114 | name_token = self.get_link_file() 115 | file_token = os.path.join(self.config.config_dir(), name_token) 116 | if (os.path.isfile(file_token)): 117 | os.remove(file_token) 118 | else: 119 | pass 120 | # logging.info("File do not exist: {}".format(file_token)) 121 | 122 | name_key = ".key" 123 | file_key = os.path.join(self.config.config_dir(), name_key) 124 | if (os.path.isfile(file_key)): 125 | os.remove(file_key) 126 | else: 127 | pass 128 | # logging.info("File do not exist: {}".format(file_key)) 129 | 130 | # print(len(self.token)) 131 | return self.token[:5] + "***" + self.token[92:] 132 | 133 | -------------------------------------------------------------------------------- /python_paypal_api/base/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | import json 4 | from json.decoder import JSONDecodeError 5 | from io import TextIOWrapper 6 | from python_paypal_api.base import PaypalTypeException 7 | import uuid 8 | 9 | class Utils: 10 | 11 | @staticmethod 12 | def payPalRequestId(headers: dict): 13 | headers["PayPal-Request-Id"] = str(uuid.uuid1()) 14 | return headers 15 | 16 | def payPalClientMetadataId(headers: dict): 17 | headers["PayPal-Client-Metadata-Id"] = "p5AngF3dfz5uYeQq4Tjws21Qx48Z0Nt9cCX3" 18 | 19 | @staticmethod 20 | def prefer(headers: dict, value: str): 21 | headers["Prefer"] = value 22 | return headers 23 | 24 | @staticmethod 25 | def contentType(headers: dict, value: str): 26 | headers["Content-Type"] = value 27 | return headers 28 | 29 | @staticmethod 30 | def clean_dictionary(dictionary: dict, valid_keys: set): 31 | """Cleans a dictionary removing null entries and invalid keys 32 | 33 | Arguments: 34 | dictionary {dict} -- The dictionary to be checked 35 | valid_keys {set} -- The keys that should not be deleted 36 | """ 37 | keys = list(dictionary.keys()) 38 | for k in keys: 39 | if dictionary.get(k) == None or not k in valid_keys: 40 | del dictionary[k] 41 | return dictionary 42 | 43 | 44 | @staticmethod 45 | def convert_updates(body): 46 | 47 | comp_keys = ['op', 'path', 'value'] 48 | 49 | if isinstance(body, list): 50 | 51 | if isinstance(body[0], tuple): 52 | body = [ {'op': x[0], 'path': x[1], 'value': x[2]} for x in body ] 53 | 54 | elif isinstance(body[0], dict): 55 | res = all(sorted(list(body[x].keys())) == sorted(comp_keys) for x in range(len(body))) 56 | 57 | elif isinstance(body, dict): 58 | body = [body] 59 | 60 | elif isinstance(body, str): 61 | obj = json.loads(body) 62 | 63 | if isinstance(obj, dict): 64 | body = [obj] 65 | 66 | elif isinstance(obj, list): 67 | return body 68 | else: 69 | error = "Only type list, dict or str are allowed on body" 70 | raise PaypalTypeException('TypeError: '+ error) 71 | 72 | return(json.dumps(body)) 73 | 74 | 75 | @staticmethod 76 | def convert_body(body, wrap: bool = True): 77 | 78 | if isinstance(body, str): 79 | 80 | if os.path.isfile(body): 81 | body = open(body, mode="r", encoding="utf-8") 82 | body = body.read() 83 | try: 84 | json.loads(body) 85 | except JSONDecodeError as error: 86 | raise AdvertisingTypeException(f"{type(error)}", error) 87 | else: 88 | try: 89 | body = json.loads(body) 90 | except ValueError as error: 91 | raise AdvertisingTypeException(f"{type(error)}", error) 92 | pass 93 | 94 | if isinstance(body, dict) and wrap: 95 | try: 96 | body = json.dumps([body]) 97 | except TypeError as error: 98 | raise AdvertisingTypeException(f"{type(error)}", error) 99 | 100 | if isinstance(body, dict) and wrap is False: 101 | try: 102 | body = json.dumps(body) 103 | except TypeError as error: 104 | raise AdvertisingTypeException(f"{type(error)}", error) 105 | 106 | if isinstance(body, list): 107 | try: 108 | body = json.dumps(body) 109 | except TypeError as error: 110 | raise AdvertisingTypeException(f"{type(error)}", error) 111 | 112 | if isinstance(body, TextIOWrapper): 113 | body = body.read() 114 | try: 115 | json.loads(body) 116 | except JSONDecodeError as error: 117 | raise AdvertisingTypeException(f"{type(error)}", error) 118 | 119 | return body 120 | 121 | def load_all_pages(throttle_by_seconds: float = 2, next_token_param='NextToken', 122 | use_rate_limit_header: bool = False, 123 | extras: dict = None): 124 | """ 125 | Load all pages if a next token is returned 126 | 127 | Args: 128 | throttle_by_seconds: float 129 | next_token_param: str | The param amazon expects to hold the next token 130 | use_rate_limit_header: if the function should try to use amazon's rate limit header 131 | extras: additional data to be sent with NextToken, e.g `dict(QueryType='NEXT_TOKEN')` for `FulfillmentInbound` 132 | Returns: 133 | Transforms the function in a generator, returning all pages 134 | """ 135 | if not extras: 136 | extras = {} 137 | 138 | def decorator(function): 139 | def wrapper(*args, **kwargs): 140 | res = function(*args, **kwargs) 141 | yield res 142 | if "nextCursor" in res.payload.get("payload"): 143 | kwargs.clear() 144 | kwargs.update({next_token_param: res.payload.get("payload")["nextCursor"], **extras}) 145 | sleep_time = throttle_by_seconds 146 | for x in wrapper(*args, **kwargs): 147 | yield x 148 | if sleep_time > 0: 149 | time.sleep(throttle_by_seconds) 150 | 151 | wrapper.__doc__ = function.__doc__ 152 | return wrapper 153 | 154 | return decorator 155 | 156 | 157 | def load_all_categories(throttle_by_seconds: float = 2, next_token_param='NextToken', 158 | use_rate_limit_header: bool = False, 159 | extras: dict = None): 160 | """ 161 | Load all pages if a next token is returned 162 | 163 | Args: 164 | throttle_by_seconds: float 165 | next_token_param: str | The param amazon expects to hold the next token 166 | use_rate_limit_header: if the function should try to use amazon's rate limit header 167 | extras: additional data to be sent with NextToken, e.g `dict(QueryType='NEXT_TOKEN')` for `FulfillmentInbound` 168 | Returns: 169 | Transforms the function in a generator, returning all pages 170 | """ 171 | if not extras: 172 | extras = {} 173 | 174 | def decorator(function): 175 | def wrapper(*args, **kwargs): 176 | res = function(*args, **kwargs) 177 | yield res 178 | if next_token_param in res.payload: 179 | # kwargs.clear() 180 | kwargs.update({next_token_param: res.payload[next_token_param], **extras}) 181 | sleep_time = throttle_by_seconds 182 | for x in wrapper(*args, **kwargs): 183 | yield x 184 | if sleep_time > 0: 185 | time.sleep(throttle_by_seconds) 186 | 187 | wrapper.__doc__ = function.__doc__ 188 | return wrapper 189 | 190 | return decorator -------------------------------------------------------------------------------- /python_paypal_api/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.2" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2023 Daniel Alvaro 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Author: 17 | Daniel Alvaro 18 | """ 19 | 20 | import os 21 | import sys 22 | 23 | import pathlib 24 | from setuptools import setup, find_packages 25 | 26 | # Current folder and README.md 27 | HERE = pathlib.Path(__file__).parent 28 | README = (HERE / "README.md").read_text() 29 | 30 | CURRENT_PYTHON_VERSION = sys.version_info[:2] 31 | REQUIRED_PYTHON_VERSION = (3,8) 32 | 33 | # Validating python version before the setup run 34 | if REQUIRED_PYTHON_VERSION > CURRENT_PYTHON_VERSION: 35 | sys.stderr.write(""" 36 | Unsupported python version found. 37 | 38 | Current Python: {}. 39 | Required Python: {}. 40 | 41 | """.format(CURRENT_PYTHON_VERSION, REQUIRED_PYTHON_VERSION)) 42 | sys.exit(64) 43 | 44 | setup( 45 | name='python-paypal-api', 46 | version='0.1.2', 47 | python_requires='>=3.8', 48 | author='denisneuf', 49 | author_email='denisneuf@hotmail.com', 50 | url='https://github.com/denisneuf/python_paypal_api', 51 | description='Paypal Python 3 API integration', 52 | long_description=README, 53 | long_description_content_type="text/markdown", 54 | packages = find_packages(exclude=['docs', 'tests']), 55 | install_requires = [ 56 | "requests>=2.27.1", 57 | "cachetools>=4.2", 58 | "confuse>=1.4", 59 | "cryptography>=39.0.2" 60 | ], 61 | license="Apache License 2.0", 62 | classifiers=[ 63 | 'License :: OSI Approved :: Apache Software License', 64 | "Programming Language :: Python :: 3", 65 | ] 66 | ) 67 | -------------------------------------------------------------------------------- /test/test_auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from python_paypal_api.base import ( 3 | Client, 4 | PaypalEndpoint, 5 | PaypalEndpointParams, 6 | PaypalApiException, 7 | ApiResponse, 8 | Utils, 9 | ProductType, 10 | CategoryType 11 | ) 12 | 13 | class TestClientMethods(unittest.TestCase): 14 | 15 | ''' 16 | def test_paypal_endpoint(self): 17 | assert PaypalEndpoint('foo') is not None 18 | 19 | @PaypalEndpoint('/api/call', method='POST') 20 | def my_endpoint(**kwargs): 21 | assert kwargs['path'] == '/api/call' 22 | assert kwargs['method'] == 'POST' 23 | 24 | my_endpoint() 25 | assert my_endpoint.__name__ == 'my_endpoint' 26 | 27 | ''' 28 | 29 | def test_paypal_endpoint(self): 30 | # assert PaypalEndpoint('foo') is not None 31 | self.assertTrue(PaypalEndpoint('foo')) 32 | 33 | @PaypalEndpoint('/api/call', method='POST') 34 | def my_endpoint(**kwargs): 35 | # assert kwargs['path'] == '/api/call' 36 | self.assertEqual(kwargs['path'], '/api/call') 37 | # assert kwargs['method'] == 'POST' 38 | self.assertEqual(kwargs['method'], 'POST') 39 | 40 | my_endpoint() 41 | assert my_endpoint.__name__ == 'my_endpoint' 42 | 43 | def test_paypal_exception(self): 44 | 45 | error = {"message": "hey", "details": "Joe", "paypal_code": 999} 46 | 47 | headers = {"bar": "foo"} 48 | 49 | s = 'hello world' 50 | 51 | e = PaypalApiException(error, headers) 52 | self.assertTrue(e.error) 53 | self.assertTrue(e.headers) 54 | self.assertTrue(e.error.get("paypal_code")) 55 | with self.assertRaises(TypeError): 56 | s.split(2) 57 | # assert e.paypal_code == 999 58 | # assert e.message == 'Foo' 59 | 60 | def test_client_timeout(self): 61 | client = Client(timeout=1) 62 | assert client.timeout == 1 63 | client = Client() 64 | assert client.timeout is None 65 | 66 | 67 | def test_client_store_credentials(self): 68 | client = Client(store_credentials=True) 69 | assert client.store_credentials == True 70 | client = Client() 71 | assert client.store_credentials is False 72 | 73 | # @unittest.skip("demonstrating skipping") 74 | @unittest.expectedFailure 75 | def test_client_account(self): 76 | client = Client(account="pp") 77 | self.assertEqual(client.account, 'pp') 78 | 79 | def test_fill_query_params(self): 80 | self.assertTrue(PaypalEndpointParams('{}/{}').fill("boo", "bar")) 81 | 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | 86 | 87 | -------------------------------------------------------------------------------- /test/test_create_token_file.py: -------------------------------------------------------------------------------- 1 | import stat 2 | import os 3 | import unittest 4 | import confuse 5 | 6 | 7 | def do_something(path, mode): 8 | flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL 9 | return os.fdopen(os.open(path, flags, 0o600), mode) 10 | 11 | 12 | class TestFileCreation(unittest.TestCase): 13 | 14 | config = confuse.Configuration('python-paypal-api') 15 | 16 | def test_correct_permissions(self): 17 | """ 18 | Make sure that a file can be created with specific permissions 19 | """ 20 | test_file = "demo.txt" 21 | 22 | file = os.path.join(self.config.config_dir(), test_file) 23 | with do_something(file, "w") as fout: 24 | fout.write("blah blah blah") 25 | 26 | finfo = os.stat(file) 27 | assert(finfo.st_mode & stat.S_IRUSR) 28 | assert(finfo.st_mode & stat.S_IWUSR) 29 | assert(not finfo.st_mode & stat.S_IXUSR) 30 | assert(not finfo.st_mode & stat.S_IRGRP) 31 | assert(not finfo.st_mode & stat.S_IWGRP) 32 | assert(not finfo.st_mode & stat.S_IXGRP) 33 | assert(not finfo.st_mode & stat.S_IROTH) 34 | assert(not finfo.st_mode & stat.S_IWOTH) 35 | assert(not finfo.st_mode & stat.S_IXOTH) 36 | 37 | os.remove(file) 38 | 39 | def test_file_exists(self): 40 | """ 41 | If the file already exists an OSError should be raised. 42 | """ 43 | test_file = "demo2.txt" 44 | file = os.path.join(self.config.config_dir(), test_file) 45 | 46 | # simulate file already placed at location by attacker 47 | with open(file, "w") as fout: 48 | fout.write("nasty attacker stuff") 49 | 50 | # ensure that we can't open the file 51 | with self.assertRaises(OSError): 52 | with do_something(file, "w") as fout: 53 | fout.write("this should never happen..") 54 | 55 | os.remove(file) --------------------------------------------------------------------------------