├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── dependabot.yml ├── .gitignore ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── akamai ├── __init__.py └── edgegrid │ ├── __init__.py │ ├── edgegrid.py │ ├── edgerc.py │ └── test │ ├── __init__.py │ ├── conftest.py │ ├── edgerc_that_doesnt_parse │ ├── sample_edgerc │ ├── sample_file.txt │ ├── test_edgegrid.py │ ├── testcases.json │ └── testdata.json ├── ci ├── Dockerfile └── test_with_docker.sh ├── dev-requirements.txt ├── examples ├── README.md ├── create-credentials.py ├── delete-credentials.py ├── get-credentials.py └── update-credentials.py ├── requirements.txt ├── setup.py └── tox.toml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug, to confirm 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 | **Please complete the following information:** 27 | - OS: [e.g. Linux, Mac OS] 28 | - Version [e.g. 1.2.1] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[ENHANCEMENT]" 5 | labels: enhancement, to confirm 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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://docs.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 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/* 2 | *~ 3 | *.egg-info 4 | *.pyc 5 | dist/* 6 | build/* 7 | .coverage 8 | htmlcov/ 9 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 2.0.1 (2025-04-24) 6 | ++++++++++++++++++ 7 | * Bug fixes 8 | - `GH#79 `_: removed unused pyOpenSSL library 9 | - Added ``README.md`` to package metadata 10 | - Updated various dependencies to remove vulnerabilities 11 | 12 | 13 | 2.0.0 (2024-11-13) 14 | ++++++++++++++++++ 15 | 16 | * Breaking changes 17 | - discontinue support for Python 2.7, the minimum supported version is now Python 3.9 18 | - the ``__init__`` function of ``EdgeGridAuth`` and ``EdgeGridAuthHeaders`` now accepts ``headers_to_sign`` and ``max_body`` as keyword-only arguments 19 | - change signatures of these methods in the ``EdgeGridAuthHeaders`` class: ``make_auth_header``, ``sign_request`` and ``make_data_to_sign`` 20 | 21 | * Improvements 22 | - update several dependencies in ``setup.py`` 23 | - start generating files ``requirements.txt`` and ``dev-requirements.txt`` using ``pip-compile``, 24 | so that they contain the full set of project dependencies 25 | 26 | * Bug fixes 27 | - fix handling file objects in the request body for ``EdgeGridAuth`` 28 | 29 | 1.3.1 (2022-09-22) 30 | ++++++++++++++++++ 31 | 32 | * Bug fixes 33 | - `GH#51 `_: include path params in signed path 34 | 35 | 1.3.0 (2022-08-29) 36 | ++++++++++++++++++ 37 | 38 | * Improvements 39 | - decouple from `requests` library 40 | - add support for MultipartEncoder 41 | 42 | * Bug fixes 43 | - remove unnecessary shebangs and permissions 44 | 45 | 1.2.1 (2021-10-11) 46 | ++++++++++++++++++ 47 | 48 | * Bug fixes 49 | - `GH#36 `_, `GH#44 `_ and `GH#53 `_ issues: split `MANIFEST.in` in several lines to properly include in Python package all necessary resource files 50 | 51 | 1.2.0 (2021-08-10) 52 | ++++++++++++++++++ 53 | 54 | * Bug fixes 55 | - `GH#48 `_ and `GH#50 `_ issues: recognize the `~` tilde character as home directory alias 56 | - `GH#36 `_, `GH#44 `_ and `GH#53 `_ issues: add missing test resource files to PyPI package 57 | - `GH#41 `_: require PyOpenSSL >= v19.0.0 to avoid old OS packages 58 | 59 | * Improvements 60 | - better Python 2 and Python 3 documentation and related setup.py tags 61 | 62 | 1.1.0 (2017-09-11) 63 | ++++++++++++++++++ 64 | 65 | - better python3 support 66 | 67 | 68 | 1.0.9 (2015-07-29) 69 | ++++++++++++++++++ 70 | 71 | - update default max_body to be 128k 72 | - read both max_body and max-body style properties from edgerc files 73 | 74 | 1.0.8 (2015-06-23) 75 | ++++++++++++++++++ 76 | 77 | - update requests dependency version 78 | 79 | 80 | 1.0.7 (2015-06-11) 81 | ++++++++++++++++++ 82 | 83 | - use pyopenssl to improve security as per https://urllib3.readthedocs.org/en/latest/security.html#pyopenssl 84 | 85 | 1.0.6 (2015-02-28) 86 | ++++++++++++++++++ 87 | 88 | - support passing in EdgeRc to from_edgerc static method 89 | - fix problem with following redirects 90 | 91 | 1.0.5 (2014-11-10) 92 | ++++++++++++++++++ 93 | 94 | - support 'Host' header more transparently 95 | - remove testurl since new 'Host' support handles the same case 96 | - support edgerc file 97 | 98 | 1.0.4 (2014-11-05) 99 | ++++++++++++++++++ 100 | 101 | - support python3 102 | 103 | 1.0.3 (2014-10-16) 104 | ++++++++++++++++++ 105 | 106 | - update link to developer site 107 | 108 | 1.0.2 (2014-08-29) 109 | ++++++++++++++++++ 110 | 111 | - add testurl parameter for overriding method and host for testing 112 | 113 | 1.0.1 (2014-05-14) 114 | ++++++++++++++++++ 115 | 116 | - Change POST behavior to truncate and max_body to 128kb(GRID-236) 117 | 118 | 1.0 (2014-04-04) 119 | ++++++++++++++++ 120 | - First version 121 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2025 Akamai Technologies, Inc. All rights reserved. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use these files except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include requirements.txt 4 | include akamai/edgegrid/test/testdata.json 5 | include akamai/edgegrid/test/sample_edgerc 6 | include akamai/edgegrid/test/edgerc_that_doesnt_parse 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python3 2 | 3 | .PHONY: help 4 | help: 5 | @echo " install install all dev and production dependencies (virtualenv is created as venv)" 6 | @echo " clean remove unwanted stuff" 7 | @echo " test run tests" 8 | 9 | .PHONY: install 10 | install: 11 | $(PYTHON) -m venv venv; . venv/bin/activate; python -m pip install -r dev-requirements.txt 12 | 13 | .PHONY: clean 14 | clean: 15 | rm -fr test 16 | rm -fr venv 17 | 18 | .PHONY: test 19 | test: 20 | @. venv/bin/activate; pytest --junitxml $(CURDIR)/test/tests.xml --cov-report xml:$(CURDIR)/test/coverage/cobertura-coverage.xml --cov=akamai 21 | 22 | .PHONY: test-docker 23 | test-docker: 24 | sh ci/test_with_docker.sh 25 | 26 | .PHONY: lint 27 | lint: 28 | @. venv/bin/activate; pylint ./akamai 29 | 30 | .PHONY: all 31 | all: clean install test lint 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EdgeGrid for Python 2 | 3 | This library implements an Authentication handler for [HTTP requests](https://requests.readthedocs.io/en/latest/) using the [Akamai EdgeGrid Authentication](https://techdocs.akamai.com/developer/docs/authenticate-with-edgegrid) scheme for Python. 4 | 5 | ## Install 6 | 7 | To use the library, you need to have Python 3.9 or later installed on your system. You can download it from [https://www.python.org/downloads/](https://www.python.org/downloads/). 8 | 9 | > __NOTE:__ Python 2 is no longer supported by the [Python Software Foundation](https://www.python.org/doc/sunset-python-2/). You won't be able to use the library with Python 2. 10 | 11 | Then, install the `edgegrid-python` authentication handler from sources by running this command from the project root directory: 12 | 13 | ``` 14 | pip install . 15 | ``` 16 | 17 | Alternatively, you can install it from PyPI (Python Package Index) by running: 18 | 19 | ``` 20 | pip install edgegrid-python 21 | ``` 22 | 23 | ## Authentication 24 | 25 | We provide authentication credentials through an API client. Requests to the API are signed with a timestamp and are executed immediately. 26 | 27 | 1. [Create authentication credentials](https://techdocs.akamai.com/developer/docs/set-up-authentication-credentials). 28 | 29 | 2. Place your credentials in an EdgeGrid resource file, `.edgerc`, under a heading of `[default]` at your local home directory. 30 | 31 | ``` 32 | [default] 33 | client_secret = C113nt53KR3TN6N90yVuAgICxIRwsObLi0E67/N8eRN= 34 | host = akab-h05tnam3wl42son7nktnlnnx-kbob3i3v.luna.akamaiapis.net 35 | access_token = akab-acc35t0k3nodujqunph3w7hzp7-gtm6ij 36 | client_token = akab-c113ntt0k3n4qtari252bfxxbsl-yvsdj 37 | ``` 38 | 39 | 3. Use your local `.edgerc` by providing the path to your resource file and credentials' section header. 40 | 41 | ```python 42 | import requests 43 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 44 | 45 | edgerc = EdgeRc('~/.edgerc') 46 | section = 'default' 47 | baseurl = 'https://%s' % edgerc.get(section, 'host') 48 | 49 | session = requests.Session() 50 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 51 | ``` 52 | Or hard code them as variables. 53 | 54 | ```python 55 | import requests 56 | from akamai.edgegrid import EdgeGridAuth 57 | 58 | session = requests.Session() 59 | session.auth = EdgeGridAuth( 60 | client_token='akab-c113ntt0k3n4qtari252bfxxbsl-yvsdj', 61 | client_secret='C113nt53KR3TN6N90yVuAgICxIRwsObLi0E67/N8eRN=', 62 | access_token='akab-acc35t0k3nodujqunph3w7hzp7-gtm6ij' 63 | ) 64 | ``` 65 | 66 | ## Use 67 | 68 | To use the library, provide the path to your `.edgerc`, your credentials section header, and the appropriate endpoint information. 69 | 70 | ```python 71 | import requests 72 | import json 73 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 74 | from urllib.parse import urljoin 75 | 76 | edgerc = EdgeRc('~/.edgerc') 77 | section = 'default' 78 | baseurl = 'https://%s' % edgerc.get(section, 'host') 79 | 80 | session = requests.Session() 81 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 82 | 83 | path = '/identity-management/v3/user-profile' 84 | headers = { 85 | "Accept": "application/json" 86 | } 87 | querystring = { 88 | "actions": True, 89 | "authGrants": True, 90 | "notifications": True 91 | } 92 | 93 | result = session.get(urljoin(baseurl, path), headers=headers, params=querystring) 94 | print(result.status_code) 95 | print(json.dumps(result.json(), indent=2)) 96 | ``` 97 | 98 | ### Query string parameters 99 | 100 | When entering query parameters use the `querystring` property. Set up the parameters as name-value pairs in an object. 101 | 102 | ```python 103 | edgerc = EdgeRc('~/.edgerc') 104 | section = 'default' 105 | baseurl = 'https://%s' % edgerc.get(section, 'host') 106 | 107 | session = requests.Session() 108 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 109 | 110 | path = '/identity-management/v3/user-profile' 111 | querystring = { 112 | "actions": True, 113 | "authGrants": True, 114 | "notifications": True 115 | } 116 | 117 | result = session.get(urljoin(baseurl, path), params=querystring) 118 | ``` 119 | 120 | ### Headers 121 | 122 | Enter request headers in the `headers` property as name-value pairs in an object. 123 | 124 | > __NOTE:__ You don't need to include the `Content-Type` and `Content-Length` headers. The authentication layer adds these values. 125 | 126 | ```python 127 | edgerc = EdgeRc('~/.edgerc') 128 | section = 'default' 129 | baseurl = 'https://%s' % edgerc.get(section, 'host') 130 | 131 | session = requests.Session() 132 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 133 | 134 | path = '/identity-management/v3/user-profile' 135 | headers = { 136 | "Accept": "application/json" 137 | } 138 | 139 | result = session.get(urljoin(baseurl, path), headers=headers) 140 | ``` 141 | 142 | ### Body data 143 | 144 | Provide the request body as an object in the `payload` property. 145 | 146 | ```python 147 | edgerc = EdgeRc('~/.edgerc') 148 | section = 'default' 149 | baseurl = 'https://%s' % edgerc.get(section, 'host') 150 | 151 | session = requests.Session() 152 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 153 | 154 | path = '/identity-management/v3/user-profile/basic-info' 155 | payload = { 156 | "contactType": "Billing", 157 | "country": "USA", 158 | "firstName": "John", 159 | "lastName": "Smith", 160 | "preferredLanguage": "English", 161 | "sessionTimeOut": 30, 162 | "timeZone": "GMT", 163 | "phone": "3456788765" 164 | } 165 | 166 | result = session.put(urljoin(baseurl, path), json=payload) 167 | ``` 168 | 169 | As the `data` parameter for the `session` methods, EdgeGrid for Python 170 | currently supports the `bytes` and `requests_toolbelt.MultipartEncoder` 171 | types or a file-like object. 172 | 173 | ### Debug 174 | 175 | Enable debugging to get additional information about a request. 176 | 177 | To log requests, use the built-in request logging. Add this before making a request: 178 | 179 | ```python 180 | import logging 181 | from http.client import HTTPConnection 182 | HTTPConnection.debuglevel = 1 183 | logging.basicConfig() 184 | logging.getLogger().setLevel(logging.DEBUG) 185 | urllib_log = logging.getLogger("urllib3") 186 | urllib_log.setLevel(logging.DEBUG) 187 | urllib_log.propagate = True 188 | ``` 189 | 190 | This will print everything apart from the HTTP response body. See the [Requests library for Python](https://requests.readthedocs.io/en/latest/api/#api-changes) for the original recipe. 191 | 192 | To log specific parts like URL, status code, headers, or body, add this: 193 | 194 | ```python 195 | import requests 196 | import logging 197 | import json 198 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 199 | from urllib.parse import urljoin 200 | 201 | logger = logging.getLogger('requests_logger') 202 | logging.basicConfig(level=logging.DEBUG) 203 | 204 | edgerc = EdgeRc('~/.edgerc') 205 | section = 'default' 206 | baseurl = 'https://%s' % edgerc.get(section, 'host') 207 | 208 | session = requests.Session() 209 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 210 | 211 | path = '/identity-management/v3/user-profile' 212 | 213 | result = session.get(urljoin(baseurl, path)) 214 | logger.debug(f'URL: {result.url}') 215 | logger.debug(f'Status Code: {result.status_code}') 216 | logger.debug(f'Headers: {result.headers}') 217 | logger.debug(f'Body: {result.json()}') 218 | ``` 219 | 220 | ## Virtual environment 221 | 222 | A [virtual environment](https://docs.python.org/3/library/venv.html) is a tool to keep dependencies required by different projects in separate places. The `venv` module is included in Python 3 by default. 223 | 224 | Set up a virtual environment: 225 | 226 | 1. Initialize your environment in a new directory. 227 | 228 | ``` 229 | // Unix/macOS 230 | python3 -m venv ~/Desktop/myenv 231 | 232 | // Windows 233 | py -m venv ~/Desktop/myenv 234 | ``` 235 | 236 | This creates a `venv` in the specified directory as well as copies pip into it. 237 | 238 | 2. Activate your environment. 239 | 240 | ``` 241 | // Unix/macOS 242 | source ~/Desktop/myenv/bin/activate 243 | 244 | // Windows 245 | ~/Desktop/myenv/Scripts/activate 246 | ``` 247 | 248 | Your prompt will change to show you're working in a virtual environment, for example: 249 | 250 | ``` 251 | (myenv) jsmith@abc-de12fg $ 252 | ``` 253 | 254 | 3. To recreate the environment, install the required dependencies within your project. 255 | 256 | ``` 257 | pip install -r dev-requirements.txt 258 | ``` 259 | 260 | 4. Run the tests. 261 | 262 | ``` 263 | // Unix/macOS 264 | pytest -v 265 | 266 | // Windows 267 | py -m pytest -v 268 | ``` 269 | 270 | 5. To deactivate your environment, run the `deactivate` command. 271 | 272 | ## Reporting issues 273 | 274 | To report an issue or make a suggestion, create a new [GitHub issue](https://github.com/akamai/AkamaiOPEN-edgegrid-python/issues). 275 | 276 | ## License 277 | 278 | Copyright 2025 Akamai Technologies, Inc. All rights reserved. 279 | 280 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 281 | 282 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 283 | -------------------------------------------------------------------------------- /akamai/__init__.py: -------------------------------------------------------------------------------- 1 | """Library provides an authentication handler for requests""" 2 | __import__('pkg_resources').declare_namespace(__name__) 3 | -------------------------------------------------------------------------------- /akamai/edgegrid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | akamai.edgegrid 3 | ~~~~~~~~~~~~~~~ 4 | 5 | This library provides an authentication handler for Requests that implements the 6 | Akamai {OPEN} EdgeGrid client authentication protocol as 7 | specified by https://developer.akamai.com/introduction/Client_Auth.html. 8 | For more information visit https://developer.akamai.com. 9 | 10 | usage: 11 | 12 | >>> import requests 13 | >>> from akamai.edgegrid import EdgeGridAuth 14 | >>> from urlparse import urljoin 15 | 16 | >>> baseurl = 'https://akaa-WWWWWWWWWWWW.luna.akamaiapis.net/' 17 | >>> s = requests.Session() 18 | >>> s.auth = EdgeGridAuth( 19 | client_token='akab-XXXXXXXXXXXXXXXXXXXXXXX', 20 | client_secret='YYYYYYYYYYYYYYYYYYYYYYYYYY', 21 | access_token='akab-ZZZZZZZZZZZZZZZZZZZZZZZZZZZ' 22 | ) 23 | 24 | ... now you have a requests session object that can be used to make {OPEN} requests 25 | 26 | >>> result = s.get(urljoin(baseurl, '/diagnostic-tools/v1/locations')) 27 | >>> result.status_code 28 | 200 29 | >>> result.json()['locations'][0] 30 | Hongkong, Hong Kong 31 | """ 32 | 33 | from .edgegrid import EdgeGridAuth 34 | from .edgerc import EdgeRc 35 | 36 | __all__ = ['EdgeGridAuth', 'EdgeRc'] 37 | 38 | __title__ = 'edgegrid-python' 39 | __version__ = '2.0.1' 40 | __license__ = 'Apache 2.0' 41 | __copyright__ = 'Copyright 2025 Akamai Technologies' 42 | -------------------------------------------------------------------------------- /akamai/edgegrid/edgegrid.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-arguments,missing-function-docstring 2 | """EdgeGrid requests Auth handler""" 3 | 4 | import logging 5 | import uuid 6 | import hashlib 7 | import hmac 8 | import base64 9 | import re 10 | import os 11 | from time import gmtime, strftime 12 | from urllib.parse import urlparse 13 | 14 | from requests.auth import AuthBase 15 | 16 | from .edgerc import EdgeRc 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | __all__ = ['EdgeGridAuth'] 21 | 22 | 23 | def eg_timestamp(): 24 | """Generates EdgeGrid compatible timestamp""" 25 | return strftime('%Y%m%dT%H:%M:%S+0000', gmtime()) 26 | 27 | 28 | def new_nonce(): 29 | return uuid.uuid4() 30 | 31 | 32 | def base64_hmac_sha256(data, key): 33 | return base64.b64encode( 34 | hmac.new( 35 | key.encode('utf8'), 36 | data.encode('utf8'), 37 | hashlib.sha256).digest() 38 | ).decode('utf8') 39 | 40 | 41 | def base64_sha256(data): 42 | digest = hashlib.sha256(data).digest() 43 | return base64.b64encode(digest).decode('utf8') 44 | 45 | 46 | def read_stream_and_rewind(f, max_read): 47 | """Reads up to read_max bytes from a python file object (like _io.BufferedReader) 48 | or a MultipartEncoder object, then rewinds the stream. 49 | 50 | The read() method of these objects is decorated by httpie with a side-effect code which 51 | prints the body content to stdout when the 'B' option is specified for --print. However, 52 | it does not trigger in the pre-request phase when this plugin is executed. Still, after reading 53 | we must set the stream position to the beginning to not impact subsequent reads outside 54 | the plugin. (We don't assume we can be passed a partially read stream to the plugin.) 55 | 56 | Raises TypeError if read() or seek() is not supported by f or f._buffer. May potentially raise 57 | OSError for any failed I/O operation, in particular io.UnsupportedOperation if the stream is 58 | not seekable (e.g. is a pipe which we don't expect here as httpie reads pipe contents and 59 | sets body as bytes). 60 | """ 61 | try: 62 | res = f.read(max_read) 63 | except AttributeError as exc: 64 | raise TypeError(f'akamai.edgegrid: unexpected body type: {type(f).__name__}') from exc 65 | 66 | try: 67 | f.seek(0) 68 | except AttributeError: 69 | # a MultipartEncoder 70 | try: 71 | # During read(), MultipartEncoder lazily loads its upload parts into self._buffer 72 | # depending on the requested number of bytes. Then a regular read() on self._buffer 73 | # is performed. Therefore, rewinding self._buffer effectively rewinds the whole 74 | # MultipartEncoder content. 75 | # pylint: disable=protected-access 76 | f._buffer.seek(0) 77 | except AttributeError as exc: 78 | raise TypeError(f'akamai.edgegrid: unexpected body type: {type(f).__name__}') from exc 79 | return res 80 | 81 | 82 | def read_body_content(body, max_body): 83 | """The body argument may be one of the following: 84 | 1. bytes object 85 | 2. str object 86 | 3. _io.BufferedReader object for body input from file 87 | 4. requests_toolbelt.MultipartEncoder object for multipart form requests 88 | 5. httpie.uploads.ChunkedUploadStream object for chunked transfer encoding 89 | (when --chunked, currently not supported) 90 | May raise TypeError for unexpected input type or OSError for I/O operations. 91 | """ 92 | if isinstance(body, bytes): 93 | return body[:max_body] 94 | if isinstance(body, str): 95 | return body.encode('utf8')[:max_body] 96 | return read_stream_and_rewind(body, max_body) 97 | 98 | 99 | def determine_body_len(body): 100 | """May raise exception if body appears to be a file (is not a str, bytes or MultipartEncoder) 101 | but either: 102 | - has no fileno method (TypeError) 103 | - raises OSError while trying to calculate the length using the file descriptor""" 104 | if isinstance(body, bytes): 105 | return len(body) 106 | if isinstance(body, str): 107 | return len(body.encode('utf8')) 108 | 109 | try: 110 | # a MultipartEncoder? 111 | return body.len 112 | except AttributeError: 113 | # a file object? 114 | try: 115 | return os.stat(body.fileno()).st_size 116 | except AttributeError as exc: 117 | raise TypeError( 118 | f'akamai.edgegrid: unexpected body type: {type(body).__name__}') from exc 119 | 120 | 121 | class EdgeGridAuth(AuthBase): 122 | """A Requests authentication handler that provides Akamai {OPEN} EdgeGrid support. 123 | 124 | Basic Usage:: 125 | >>> import requests 126 | >>> from akamai.edgegrid import EdgeGridAuth 127 | >>> s = requests.Session() 128 | >>> s.auth = EdgeGridAuth( 129 | client_token='cccccccccccccccccc', 130 | client_secret='sssssssssssssssss', 131 | access_token='aaaaaaaaaaaaaaaaa' 132 | ) 133 | 134 | """ 135 | 136 | def __init__(self, client_token, client_secret, access_token, 137 | *, headers_to_sign=(), max_body=131072): 138 | """Initialize authentication using the given parameters from the Akamai OPEN APIs 139 | Interface: 140 | 141 | :param client_token: Client token provided by "Credentials" ui 142 | :param client_secret: Client secret provided by "Credentials" ui 143 | :param access_token: Access token provided by "Authorizations" ui 144 | :param headers_to_sign: An ordered list header names that will be included in 145 | the signature. This will be provided by specific APIs. (default []) 146 | :param max_body: Maximum content body size for POST requests. This will be provided by 147 | specific APIs. (default 131072) 148 | 149 | """ 150 | # pylint: disable=invalid-name 151 | self.ah = EdgeGridAuthHeaders( 152 | client_token, 153 | client_secret, 154 | access_token, 155 | headers_to_sign=headers_to_sign, 156 | max_body=max_body 157 | ) 158 | 159 | @staticmethod 160 | def from_edgerc(rcinput, section='default'): 161 | """ 162 | Returns an EdgeGridAuth object from the configuration from the given section 163 | of the given edgerc file. 164 | 165 | :param rcinput: EdgeRc instance or path to the edgerc file 166 | :param section: the section to use (this is the [bracketed] part of the edgerc, 167 | default is 'default') 168 | 169 | """ 170 | if isinstance(rcinput, EdgeRc): 171 | edgerc = rcinput 172 | else: 173 | edgerc = EdgeRc(rcinput) 174 | 175 | return EdgeGridAuth( 176 | client_token=edgerc.get(section, 'client_token'), 177 | client_secret=edgerc.get(section, 'client_secret'), 178 | access_token=edgerc.get(section, 'access_token'), 179 | headers_to_sign=edgerc.getlist(section, 'headers_to_sign'), 180 | max_body=edgerc.getint(section, 'max_body') 181 | ) 182 | 183 | def handle_redirect(self, res, **_): 184 | if res.is_redirect: 185 | redirect_location = res.headers['location'] 186 | 187 | logger.debug("signing the redirected url: %s", redirect_location) 188 | request_to_sign = res.request.copy() 189 | request_to_sign.url = redirect_location 190 | 191 | res.request.headers['Authorization'] = self.ah.make_auth_header( 192 | request_to_sign, eg_timestamp(), new_nonce()) 193 | 194 | def __call__(self, r): 195 | timestamp = eg_timestamp() 196 | nonce = new_nonce() 197 | 198 | r.headers['Authorization'] = self.ah.make_auth_header(r, timestamp, nonce) 199 | r.register_hook('response', self.handle_redirect) 200 | return r 201 | 202 | 203 | class EdgeGridAuthHeaders: 204 | """ 205 | A class for preparing requests authentication headers needed for 206 | Akamai {OPEN} EdgeGrid support. 207 | """ 208 | def __init__(self, client_token, client_secret, access_token, 209 | *, headers_to_sign=(), max_body=131072): 210 | self.client_token = client_token 211 | self.client_secret = client_secret 212 | self.access_token = access_token 213 | self.headers_to_sign = [h.lower() for h in headers_to_sign] 214 | self.max_body = max_body 215 | 216 | def make_signing_key(self, timestamp): 217 | signing_key = base64_hmac_sha256(timestamp, self.client_secret) 218 | logger.debug('signing key: %s', signing_key) 219 | return signing_key 220 | 221 | def canonicalize_headers(self, headers): 222 | spaces_re = re.compile('\\s+') 223 | 224 | # note: r.headers is a case-insensitive dict and self.headers_to_sign 225 | # should already be in lowercase at this point 226 | # pylint: disable=consider-using-f-string 227 | return '\t'.join([ 228 | "%s:%s" % (h, spaces_re.sub(' ', headers[h].strip())) 229 | for h in self.headers_to_sign if h in headers 230 | ]) 231 | 232 | def make_content_hash(self, body, method): 233 | logger.debug("body is '%s'", body) 234 | content_hash = "" 235 | if method == 'POST': 236 | buf = read_body_content(body, self.max_body) 237 | if buf: 238 | logger.debug("signing content: %s", buf) 239 | content_hash = base64_sha256(buf) 240 | try: 241 | body_len = determine_body_len(body) 242 | if body_len > self.max_body: 243 | logger.debug( 244 | "data length %d is larger than maximum %d " 245 | "and will be truncated for computing the hash", 246 | body_len, self.max_body) 247 | except (TypeError, OSError) as e: 248 | # body length is needed only for debugging: just log a possible exception 249 | logger.warning("cannot determine length of request body=%s: %s", body, e) 250 | logger.debug("content hash is '%s'", content_hash) 251 | return content_hash 252 | 253 | @staticmethod 254 | def get_header_versions(header=None): 255 | if header is None: 256 | header = {} 257 | 258 | version_header = '' 259 | akamai_cli = os.getenv('AKAMAI_CLI') 260 | akamai_cli_version = os.getenv('AKAMAI_CLI_VERSION') 261 | if akamai_cli and akamai_cli_version: 262 | version_header += " AkamaiCLI/" + akamai_cli_version 263 | 264 | akamai_cli_command = os.getenv('AKAMAI_CLI_COMMAND') 265 | akamai_cli_command_version = os.getenv('AKAMAI_CLI_COMMAND_VERSION') 266 | if akamai_cli_command and akamai_cli_command_version: 267 | version_header += " AkamaiCLI-" + akamai_cli_command + \ 268 | "/" + akamai_cli_command_version 269 | 270 | if version_header != '': 271 | if 'User-Agent' not in header: 272 | header['User-Agent'] = version_header.strip() 273 | else: 274 | header['User-Agent'] += version_header 275 | 276 | return header 277 | 278 | def make_data_to_sign(self, request, auth_header): 279 | parsed_url = urlparse(request.url) 280 | 281 | if request.headers.get('Host', False): 282 | netloc = request.headers['Host'] 283 | else: 284 | netloc = parsed_url.netloc 285 | 286 | self.get_header_versions(request.headers) 287 | 288 | data_to_sign = '\t'.join([ 289 | request.method, 290 | parsed_url.scheme, 291 | netloc, 292 | # Note: relative URL constraints are handled by requests when it sets up 'r' 293 | parsed_url.path + (';' + parsed_url.params if parsed_url.params else "") + 294 | ('?' + parsed_url.query if parsed_url.query else ""), 295 | self.canonicalize_headers(request.headers), 296 | self.make_content_hash(request.body or '', request.method), 297 | auth_header 298 | ]) 299 | logger.debug('data to sign: %s', '\\t'.join(data_to_sign.split('\t'))) 300 | return data_to_sign 301 | 302 | def sign_request(self, request, timestamp, auth_header): 303 | return base64_hmac_sha256( 304 | self.make_data_to_sign(request, auth_header), 305 | self.make_signing_key(timestamp) 306 | ) 307 | 308 | def make_auth_header(self, request, timestamp, nonce): 309 | kvps = [ 310 | ('client_token', self.client_token), 311 | ('access_token', self.access_token), 312 | ('timestamp', timestamp), 313 | ('nonce', nonce), 314 | ] 315 | auth_header = "EG1-HMAC-SHA256 " + \ 316 | ';'.join([f"{k}={v}" for k, v in kvps]) + ';' 317 | logger.debug('unsigned authorization header: %s', auth_header) 318 | 319 | signed_auth_header = auth_header + \ 320 | 'signature=' + self.sign_request(request, timestamp, auth_header) 321 | 322 | logger.debug('signed authorization header: %s', signed_auth_header) 323 | return signed_auth_header 324 | -------------------------------------------------------------------------------- /akamai/edgegrid/edgerc.py: -------------------------------------------------------------------------------- 1 | """Support for .edgerc file format""" 2 | 3 | import logging 4 | from configparser import ConfigParser 5 | from os.path import expanduser 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class EdgeRc(ConfigParser): 11 | """Class for managing .edgerc files""" 12 | def __init__(self, filename): 13 | ConfigParser.__init__(self, 14 | {'client_token': '', 15 | 'client_secret': '', 16 | 'host': '', 17 | 'access_token': '', 18 | 'max_body': '131072', 19 | 'headers_to_sign': 'None'}) 20 | logger.debug("loading edgerc from %s", filename) 21 | 22 | self.read(expanduser(filename)) 23 | 24 | logger.debug("successfully loaded edgerc") 25 | 26 | def optionxform(self, optionstr): 27 | """support both max_body and max-body style keys""" 28 | return optionstr.replace('-', '_') 29 | 30 | def getlist(self, section, option): 31 | """ 32 | returns the named option as a list, splitting the original value by ',' 33 | """ 34 | value = self.get(section, option) 35 | if value: 36 | return value.split(',') 37 | return None 38 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akamai/AkamaiOPEN-edgegrid-python/ced12e127aa8a54c6287d614f932bb18c8d772fb/akamai/edgegrid/test/__init__.py -------------------------------------------------------------------------------- /akamai/edgegrid/test/conftest.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring 2 | """Unit tests helpers""" 3 | 4 | import json 5 | import os 6 | import pytest 7 | 8 | test_dir = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | def cases(): 12 | with open(f'{test_dir}/testcases.json', encoding="utf-8") as data: 13 | data = json.load(data) 14 | return data 15 | 16 | 17 | @pytest.fixture(scope="package") 18 | def testdata(): 19 | with open(f'{test_dir}/testdata.json', encoding="utf-8") as data: 20 | data = json.load(data) 21 | return data 22 | 23 | 24 | def names(tests): 25 | result = [] 26 | for test in tests: 27 | result.append(test["testName"]) 28 | return result 29 | 30 | 31 | @pytest.fixture 32 | def multipart_fields(): 33 | with open(f'{test_dir}/sample_file.txt', "rb") as f: 34 | result = { 35 | "foo": "bar", 36 | "baz": ("sample_file.txt", f), 37 | } 38 | yield result 39 | 40 | 41 | @pytest.fixture 42 | def sample_file(): 43 | with open(f'{test_dir}/sample_file.txt', "rb") as f: 44 | yield f 45 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/edgerc_that_doesnt_parse: -------------------------------------------------------------------------------- 1 | [default] 2 | client_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 3 | access_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 4 | max_body = xx131072 5 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/sample_edgerc: -------------------------------------------------------------------------------- 1 | [default] 2 | host = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/ 3 | client_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 4 | client_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= 5 | access_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 6 | max_body = 131072 7 | [broken] 8 | host = "https://xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/" 9 | client_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 10 | client_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= 11 | access_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 12 | [headers] 13 | host = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/ 14 | client_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 15 | client_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= 16 | access_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 17 | max_body = 131072 18 | headers_to_sign=X-MyThing1,X-MyThing2 19 | [dashes] 20 | host = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/ 21 | client-token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 22 | client-secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= 23 | access-token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx 24 | max-body = 131072 25 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/sample_file.txt: -------------------------------------------------------------------------------- 1 | this is a sample file. -------------------------------------------------------------------------------- /akamai/edgegrid/test/test_edgegrid.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-function-docstring 2 | """unit tests for edgegrid. It runs tests from testcases.json""" 3 | 4 | import io 5 | import logging 6 | import os 7 | import re 8 | import unittest.mock 9 | from urllib.parse import urljoin 10 | 11 | import requests 12 | import requests_toolbelt 13 | import pytest 14 | 15 | import akamai.edgegrid.edgegrid as eg 16 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 17 | from akamai.edgegrid.test.conftest import cases, names, test_dir 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | EXPECTED_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=' 22 | 23 | 24 | def test_edge_grid_auth_headers(testdata): 25 | auth_headers = eg.EdgeGridAuthHeaders( 26 | client_token=testdata['client_token'], 27 | client_secret=testdata['client_secret'], 28 | access_token=testdata['access_token'], 29 | headers_to_sign=testdata['headers_to_sign'], 30 | max_body=testdata['max_body'] 31 | ) 32 | 33 | sign_key = auth_headers.make_signing_key(testdata['timestamp']) 34 | assert sign_key == testdata["sign_key_test"] 35 | 36 | content_hash = auth_headers.make_content_hash( 37 | body="test_body", 38 | method="POST" 39 | ) 40 | assert content_hash == testdata["content_hash_test"] 41 | 42 | header = auth_headers.get_header_versions() 43 | assert header == {} 44 | 45 | 46 | def test_make_content_hash_logs_warning_for_unknown_body_len(testdata, caplog): 47 | auth_headers = eg.EdgeGridAuthHeaders( 48 | client_token=testdata['client_token'], 49 | client_secret=testdata['client_secret'], 50 | access_token=testdata['access_token'], 51 | headers_to_sign=testdata['headers_to_sign'], 52 | max_body=testdata['max_body'] 53 | ) 54 | 55 | def throwing_determine_body_len(_): 56 | raise OSError('boom') 57 | 58 | with unittest.mock.patch('akamai.edgegrid.edgegrid.determine_body_len', 59 | throwing_determine_body_len): 60 | content_hash = auth_headers.make_content_hash(body="test_body", method="POST") 61 | assert content_hash == testdata["content_hash_test"] 62 | assert re.match(r'WARNING.+cannot determine length of request body=.+:\s+boom', 63 | caplog.text) 64 | 65 | 66 | @pytest.mark.parametrize("testcase", cases(), ids=names(cases())) 67 | def test_edge_grid(testdata, testcase): 68 | auth = EdgeGridAuth( 69 | client_token=testdata['client_token'], 70 | client_secret=testdata['client_secret'], 71 | access_token=testdata['access_token'], 72 | headers_to_sign=testdata['headers_to_sign'], 73 | max_body=testdata['max_body'] 74 | ) 75 | 76 | headers = {} 77 | if 'headers' in testcase['request']: 78 | for request_headers in testcase['request']['headers']: 79 | for key, val in request_headers.items(): 80 | headers[key] = val 81 | 82 | req = requests.Request( 83 | method=testcase['request']['method'], 84 | url=urljoin( 85 | testdata['base_url'], 86 | testcase['request']['path']), 87 | headers=headers, 88 | data=testcase['request'].get('data') 89 | ) 90 | 91 | # The auth plugin is called on a PreparedRequest object, not a Request (see 92 | # PreparedRequest.prepare()). That's why we want req to be a PreparedRequest 93 | # in order to test make_auth_header with proper data. 94 | if testcase.get('failsWithMessage') is None: 95 | req = req.prepare() 96 | data_to_sign = auth.ah.make_data_to_sign(req, "") 97 | auth_header = auth.ah.make_auth_header(req, testdata['timestamp'], testdata['nonce']) 98 | assert auth_header == testcase['expectedAuthorization'] 99 | assert data_to_sign == testcase['expectedDataToSign'] 100 | else: 101 | with pytest.raises(Exception) as exc_info: 102 | req.prepare() 103 | assert str(exc_info.value) == testcase['failsWithMessage'] 104 | 105 | 106 | def test_nonce(): 107 | count = 100 108 | nonces = set() 109 | while count > 0: 110 | nonce = eg.new_nonce() 111 | assert nonce not in nonces 112 | count -= 1 113 | 114 | 115 | def test_timestamp(): 116 | valid_timestamp = re.compile(r""" 117 | ^ 118 | \d{4} # year 119 | [0-1][0-9] # month 120 | [0-3][0-9] # day 121 | T 122 | [0-2][0-9] # hour 123 | : 124 | [0-5][0-9] # minute 125 | : 126 | [0-5][0-9] # second 127 | \+0000 # timezone 128 | $ 129 | """, re.VERBOSE) 130 | assert re.match(valid_timestamp, eg.eg_timestamp()) 131 | 132 | 133 | def test_defaults(): 134 | auth = EdgeGridAuth( 135 | client_token='xxx', client_secret='xxx', access_token='xxx' 136 | ) 137 | assert auth.ah.max_body == 131072 138 | assert auth.ah.headers_to_sign == [] 139 | 140 | 141 | def test_edgerc_default(): 142 | auth = EdgeGridAuth.from_edgerc(os.path.join(test_dir, 'sample_edgerc')) 143 | assert auth.ah.client_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx' 144 | assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET 145 | assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx' 146 | assert auth.ah.max_body == 131072 147 | assert auth.ah.headers_to_sign == ['none'] 148 | 149 | 150 | def test_edgerc_broken(): 151 | auth = EdgeGridAuth.from_edgerc( 152 | os.path.join(test_dir, 'sample_edgerc'), 'broken') 153 | assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET 154 | assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx' 155 | assert auth.ah.max_body == 128 * 1024 156 | assert auth.ah.headers_to_sign == ['none'] 157 | 158 | 159 | def test_edgerc_unparseable(): 160 | with pytest.raises(BaseException): 161 | EdgeGridAuth.from_edgerc( 162 | os.path.join(test_dir, 'edgerc_that_doesnt_parse')) 163 | 164 | 165 | def test_edgerc_headers(): 166 | auth = EdgeGridAuth.from_edgerc( 167 | os.path.join(test_dir, 'sample_edgerc'), 'headers') 168 | assert auth.ah.headers_to_sign == ['x-mything1', 'x-mything2'] 169 | 170 | 171 | def test_get_header_versions(): 172 | auth = EdgeGridAuth.from_edgerc( 173 | os.path.join(test_dir, 'sample_edgerc'), 'headers') 174 | header = auth.ah.get_header_versions() 175 | assert 'user-agent' not in header 176 | 177 | header = auth.ah.get_header_versions({'User-Agent': 'testvalue'}) 178 | assert 'User-Agent' in header 179 | 180 | # setting environment variables with hardcoded `1.0.0` value, just for this test. 181 | # These variables are cleared at the end of this test. 182 | os.environ["AKAMAI_CLI"] = '1.0.0' 183 | os.environ["AKAMAI_CLI_VERSION"] = '1.0.0' 184 | 185 | header = auth.ah.get_header_versions() 186 | assert 'User-Agent' in header 187 | assert header['User-Agent'] == 'AkamaiCLI/1.0.0' 188 | 189 | header = auth.ah.get_header_versions({'User-Agent': 'test-agent'}) 190 | assert 'User-Agent' in header 191 | assert header['User-Agent'] == 'test-agent AkamaiCLI/1.0.0' 192 | 193 | os.environ["AKAMAI_CLI_COMMAND"] = '1.0.0' 194 | os.environ["AKAMAI_CLI_COMMAND_VERSION"] = '1.0.0' 195 | 196 | header = auth.ah.get_header_versions() 197 | assert 'User-Agent' in header 198 | assert header['User-Agent'] == 'AkamaiCLI/1.0.0 AkamaiCLI-1.0.0/1.0.0' 199 | 200 | header = auth.ah.get_header_versions({'User-Agent': 'testvalue'}) 201 | assert 'User-Agent' in header 202 | assert header['User-Agent'] == 'testvalue AkamaiCLI/1.0.0 AkamaiCLI-1.0.0/1.0.0' 203 | 204 | del os.environ['AKAMAI_CLI'] 205 | del os.environ['AKAMAI_CLI_VERSION'] 206 | del os.environ['AKAMAI_CLI_COMMAND'] 207 | del os.environ['AKAMAI_CLI_COMMAND_VERSION'] 208 | 209 | assert 'AKAMAI_CLI' not in os.environ 210 | assert 'AKAMAI_CLI_VERSION' not in os.environ 211 | assert 'AKAMAI_CLI_COMMAND' not in os.environ 212 | assert 'AKAMAI_CLI_COMMAND_VERSION' not in os.environ 213 | 214 | 215 | def test_edgerc_from_object(): 216 | auth = EdgeGridAuth.from_edgerc( 217 | EdgeRc(os.path.join(test_dir, 'sample_edgerc'))) 218 | assert auth.ah.client_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx' 219 | assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET 220 | assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx' 221 | assert auth.ah.max_body == 131072 222 | assert auth.ah.headers_to_sign == ['none'] 223 | 224 | 225 | def test_edgerc_dashes(): 226 | auth = EdgeGridAuth.from_edgerc( 227 | os.path.join(test_dir, 'sample_edgerc'), 'dashes') 228 | assert auth.ah.max_body == 128 * 1024 229 | 230 | 231 | class TestReadBodyContent: 232 | """Test read_body_content""" 233 | def test_reading_from_str(self): 234 | assert eg.read_body_content('foobar', 10) == b'foobar' 235 | 236 | def test_reading_from_str_truncated(self): 237 | assert eg.read_body_content('foobar', 3) == b'foo' 238 | 239 | def test_reading_from_bytes(self): 240 | assert eg.read_body_content(b'foobar', 10) == b'foobar' 241 | 242 | def test_reading_from_bytes_truncated(self): 243 | assert eg.read_body_content(b'foobar', 3) == b'foo' 244 | 245 | def test_basic_reading_from_file_object(self, sample_file): 246 | assert eg.read_body_content(sample_file, 32) == b'this is a sample file.' 247 | 248 | def test_basic_reading_from_multipart_encoder(self): 249 | def encoder(): 250 | return requests_toolbelt.MultipartEncoder({'foo': 'bar'}, boundary='baz') 251 | 252 | assert eg.read_body_content(encoder(), 1024) == encoder().to_string() 253 | 254 | 255 | class TestReadStreamAndRewind: 256 | """Test read_stream_and_rewind""" 257 | def test_with_file_object(self, sample_file): 258 | assert eg.read_stream_and_rewind(sample_file, 32) == b'this is a sample file.' 259 | assert sample_file.read() == b'this is a sample file.' 260 | 261 | def test_with_file_object_truncated(self, sample_file): 262 | assert eg.read_stream_and_rewind(sample_file, 4) == b'this' 263 | assert sample_file.read() == b'this is a sample file.' 264 | 265 | def test_with_multipart_encoder(self, multipart_fields): 266 | encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "multipart_boundary") 267 | 268 | buf = eg.read_stream_and_rewind(encoder, 1024) 269 | assert buf == encoder.to_string() 270 | assert buf.startswith(b'--multipart_boundary') 271 | assert len(buf) == encoder.len 272 | 273 | def test_with_multipart_encoder_truncated(self, multipart_fields): 274 | encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "multipart_boundary") 275 | 276 | assert eg.read_stream_and_rewind(encoder, 20) == b'--multipart_boundary' 277 | buf = eg.read_stream_and_rewind(encoder, 1024) 278 | assert buf == encoder.to_string() 279 | assert buf.startswith(b'--multipart_boundary') 280 | assert len(buf) == encoder.len 281 | 282 | def test_raises_when_input_has_no_read_method(self): 283 | with pytest.raises(TypeError) as excinfo: 284 | eg.read_stream_and_rewind('foo', 10) 285 | assert excinfo.match('akamai.edgegrid: unexpected body type: str') 286 | assert str(excinfo.value.__cause__) == "'str' object has no attribute 'read'" 287 | 288 | def test_raises_when_input_has_no_seek_method(self): 289 | # pylint: disable=missing-class-docstring,too-few-public-methods 290 | class DummyReader: 291 | def read(self, _): 292 | return b'Hello' 293 | 294 | with pytest.raises(TypeError) as excinfo: 295 | eg.read_stream_and_rewind(DummyReader(), 10) 296 | assert excinfo.match('akamai.edgegrid: unexpected body type: DummyReader') 297 | assert str(excinfo.value.__cause__) == "'DummyReader' object has no attribute '_buffer'" 298 | 299 | def test_raises_when_stream_not_seekable(self): 300 | r, w = os.pipe() 301 | os.write(w, b'Hello, pipe!') 302 | os.close(w) 303 | with pytest.raises(io.UnsupportedOperation) as excinfo: 304 | with open(r, 'rb') as pipe: 305 | eg.read_stream_and_rewind(pipe, 10) 306 | assert excinfo.match('not seekable') 307 | 308 | 309 | class TestDetermineBodyLen: 310 | """Test determine_body_len""" 311 | def test_with_str(self): 312 | assert eg.determine_body_len('foobarbaz') == 9 313 | 314 | def test_with_bytes(self): 315 | assert eg.determine_body_len(b'foobarbaz') == 9 316 | 317 | def test_with_file(self, sample_file): 318 | assert eg.determine_body_len(sample_file) == len('this is a sample file.') 319 | 320 | def test_with_multipart_encoder(self, multipart_fields): 321 | encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "boundary") 322 | assert eg.determine_body_len(encoder) == encoder.len 323 | 324 | def test_raises_on_unknown_body_type(self): 325 | with pytest.raises(TypeError) as excinfo: 326 | eg.determine_body_len({'foo': 'bar'}) 327 | assert excinfo.match('akamai.edgegrid: unexpected body type: dict') 328 | assert str(excinfo.value.__cause__) == "'dict' object has no attribute 'fileno'" 329 | 330 | def test_raises_when_filelike_body_does_not_support_file_descriptors(self): 331 | with pytest.raises(io.UnsupportedOperation) as excinfo: 332 | eg.determine_body_len(io.StringIO("Hello, string")) 333 | assert excinfo.match('fileno') 334 | 335 | 336 | def test_json(testdata): 337 | auth = EdgeGridAuth( 338 | client_token=testdata['client_token'], 339 | client_secret=testdata['client_secret'], 340 | access_token=testdata['access_token'], 341 | ) 342 | 343 | params = { 344 | 'extended': 'true', 345 | } 346 | 347 | data = { 348 | 'key': 'value', 349 | } 350 | 351 | request = requests.Request( 352 | method='POST', 353 | url=urljoin(testdata['base_url'], '/testapi/v1/t3'), 354 | params=params, 355 | json=data, 356 | ) 357 | 358 | req = request.prepare() 359 | auth_header = auth.ah.make_auth_header(req, testdata['timestamp'], testdata['nonce']) 360 | 361 | assert auth_header == testdata['jsontest_hash'] 362 | 363 | 364 | def test_multipart_encoder(testdata, multipart_fields): 365 | auth = EdgeGridAuth( 366 | client_token=testdata["client_token"], 367 | client_secret=testdata["client_secret"], 368 | access_token=testdata["access_token"], 369 | ) 370 | 371 | params = { 372 | "extended": "true", 373 | } 374 | 375 | data = requests_toolbelt.MultipartEncoder( 376 | fields=multipart_fields, 377 | boundary="multipart_boundary", 378 | ) 379 | 380 | request = requests.Request( 381 | method="POST", 382 | url=urljoin(testdata["base_url"], "/testapi/v1/t3"), 383 | params=params, 384 | data=data, 385 | ) 386 | 387 | req = request.prepare() 388 | auth_header = auth.ah.make_auth_header(req, testdata["timestamp"], testdata["nonce"]) 389 | 390 | assert auth_header == testdata["multipart_hash_test"] 391 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/testcases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "testName": "simple GET", 4 | "request": { 5 | "method": "GET", 6 | "path": "/", 7 | "headers": [ 8 | { 9 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 10 | } 11 | ] 12 | }, 13 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=tL+y4hxyHxgWVD30X3pWnGKHcPzmrIF+LThiAOhMxYU=", 14 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/\t\t\t" 15 | }, 16 | { 17 | "testName": "GET with querystring", 18 | "request": { 19 | "method": "GET", 20 | "path": "/testapi/v1/t1?p1=1&p2=2", 21 | "headers": [ 22 | { 23 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 24 | } 25 | ] 26 | }, 27 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hKDH1UlnQySSHjvIcZpDMbQHihTQ0XyVAKZaApabdeA=", 28 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t1?p1=1&p2=2\t\t\t" 29 | }, 30 | { 31 | "testName": "POST inside limit", 32 | "request": { 33 | "method": "POST", 34 | "path": "/testapi/v1/t3", 35 | "data": "datadatadatadatadatadatadatadata", 36 | "headers": [ 37 | { 38 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 39 | } 40 | ] 41 | }, 42 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=hXm4iCxtpN22m4cbZb4lVLW5rhX8Ca82vCFqXzSTPe4=", 43 | "expectedDataToSign": "POST\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t3\t\tfDimoYqXOLntG3If/Z0K2aS9I19Pkv9P5OMCoL8lY0w=\t" 44 | }, 45 | { 46 | "testName": "POST too large", 47 | "request": { 48 | "method": "POST", 49 | "path": "/testapi/v1/t3", 50 | "data": "ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", 51 | "headers": [ 52 | { 53 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 54 | } 55 | ] 56 | }, 57 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=6Q6PiTipLae6n4GsSIDTCJ54bEbHUBp+4MUXrbQCBoY=", 58 | "expectedDataToSign": "POST\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t3\t\tiysZKJ78BqF0NvDrpv9Hc3pJBWC5f5apR4qUK/Qfo5k=\t" 59 | }, 60 | { 61 | "testName": "POST length equals max_body", 62 | "request": { 63 | "method": "POST", 64 | "path": "/testapi/v1/t3", 65 | "data": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", 66 | "headers": [ 67 | { 68 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 69 | } 70 | ] 71 | }, 72 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=6Q6PiTipLae6n4GsSIDTCJ54bEbHUBp+4MUXrbQCBoY=", 73 | "expectedDataToSign": "POST\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t3\t\tiysZKJ78BqF0NvDrpv9Hc3pJBWC5f5apR4qUK/Qfo5k=\t" 74 | }, 75 | { 76 | "testName": "POST empty body", 77 | "request": { 78 | "method": "POST", 79 | "path": "/testapi/v1/t6", 80 | "data": "", 81 | "headers": [ 82 | { 83 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 84 | } 85 | ] 86 | }, 87 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=1gEDxeQGD5GovIkJJGcBaKnZ+VaPtrc4qBUHixjsPCQ=", 88 | "expectedDataToSign": "POST\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t6\t\t\t" 89 | }, 90 | { 91 | "testName": "Simple header signing with GET", 92 | "request": { 93 | "method": "GET", 94 | "path": "/testapi/v1/t4", 95 | "headers": [ 96 | { 97 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 98 | }, 99 | { 100 | "X-Test1": "test-simple-header" 101 | } 102 | ] 103 | }, 104 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=8F9AybcRw+PLxnvT+H0JRkjROrrUgsxJTnRXMzqvcwY=", 105 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t4\tx-test1:test-simple-header\t\t" 106 | }, 107 | { 108 | "testName": "Header containing spaces", 109 | "request": { 110 | "method": "GET", 111 | "path": "/testapi/v1/t4", 112 | "headers": [ 113 | { 114 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 115 | }, 116 | { 117 | "X-Test1": "\" test-header-with-spaces \"" 118 | } 119 | ] 120 | }, 121 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=ucq2AbjCNtobHfCTuS38fdkl5UDdWHZhQX46fYR8CqI=", 122 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t4\tx-test1:\" test-header-with-spaces \"\t\t" 123 | }, 124 | { 125 | "testName": "Header with leading and interior spaces", 126 | "request": { 127 | "method": "GET", 128 | "path": "/testapi/v1/t4", 129 | "headers": [ 130 | { 131 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 132 | }, 133 | { 134 | "X-Test1": " first-thing second-thing" 135 | } 136 | ] 137 | }, 138 | "failsWithMessage": "Invalid leading whitespace, reserved character(s), or return character(s) in header value: ' first-thing second-thing'", 139 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=WtnneL539UadAAOJwnsXvPqT4Kt6z7HMgBEwAFpt3+c=" 140 | }, 141 | { 142 | "testName": "Headers out of order", 143 | "request": { 144 | "method": "GET", 145 | "path": "/testapi/v1/t4", 146 | "headers": [ 147 | { 148 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 149 | }, 150 | { 151 | "X-Test2": "t2" 152 | }, 153 | { 154 | "X-Test1": "t1" 155 | }, 156 | { 157 | "X-Test3": "t3" 158 | } 159 | ] 160 | }, 161 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=Wus73Nx8jOYM+kkBFF2q8D1EATRIMr0WLWwpLBgkBqY=", 162 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t4\tx-test1:t1\tx-test2:t2\tx-test3:t3\t\t" 163 | }, 164 | { 165 | "testName": "Extra header", 166 | "request": { 167 | "method": "GET", 168 | "path": "/testapi/v1/t5", 169 | "headers": [ 170 | { 171 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 172 | }, 173 | { 174 | "X-Test2": "t2" 175 | }, 176 | { 177 | "X-Test1": "t1" 178 | }, 179 | { 180 | "X-Test3": "t3" 181 | }, 182 | { 183 | "X-Extra": "this won't be included" 184 | } 185 | ] 186 | }, 187 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=Knd/jc0A5Ghhizjayr0AUUvl2MZjBpS3FDSzvtq4Ixc=", 188 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t5\tx-test1:t1\tx-test2:t2\tx-test3:t3\t\t" 189 | }, 190 | { 191 | "testName": "PUT test", 192 | "request": { 193 | "method": "PUT", 194 | "path": "/testapi/v1/t6", 195 | "data": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" 196 | }, 197 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=GNBWEYSEWOLtu+7dD52da2C39aX/Jchpon3K/AmBqBU=", 198 | "expectedDataToSign": "PUT\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t6\t\t\t" 199 | }, 200 | { 201 | "testName": "PATCH test", 202 | "request": { 203 | "method": "PATCH", 204 | "path": "/testapi/v1/t6", 205 | "data": "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP" 206 | }, 207 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=JIl05ImY1AOnMtmw+9LKgaFA8mnzsEKabbnHmI8LsQ4=", 208 | "expectedDataToSign": "PATCH\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/t6\t\t\t" 209 | }, 210 | { 211 | "testName": "GET with query params", 212 | "request": { 213 | "method": "GET", 214 | "path": "/testapi/v1/configs/111?from=12345&limit=200000", 215 | "headers": [ 216 | { 217 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 218 | } 219 | ] 220 | }, 221 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=XM+hsuSs6nuy/5eDRty1IjtVCAdr8xPFRAZ/b8RXDm8=", 222 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/configs/111?from=12345&limit=200000\t\t\t" 223 | }, 224 | { 225 | "_comment": "signature must be different here than in 'GET with query params' test", 226 | "testName": "GET with query params and separator in path", 227 | "request": { 228 | "method": "GET", 229 | "path": "/testapi/v1/configs/111;222;333?from=12345&limit=200000", 230 | "headers": [ 231 | { 232 | "Host": "akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net" 233 | } 234 | ] 235 | }, 236 | "expectedAuthorization": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=pmQF7Is2+O4r/mMojPR4yeF58BrempNNoBX5/DT0Fxs=", 237 | "expectedDataToSign": "GET\thttps\takaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net\t/testapi/v1/configs/111;222;333?from=12345&limit=200000\t\t\t" 238 | } 239 | ] 240 | -------------------------------------------------------------------------------- /akamai/edgegrid/test/testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "https://akaa-baseurl-xxxxxxxxxxx-xxxxxxxxxxxxx.luna.akamaiapis.net/", 3 | "access_token": "akab-access-token-xxx-xxxxxxxxxxxxxxxx", 4 | "client_token": "akab-client-token-xxx-xxxxxxxxxxxxxxxx", 5 | "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=", 6 | "max_body": 2048, 7 | "headers_to_sign": [ 8 | "X-Test1", 9 | "X-Test2", 10 | "X-Test3" 11 | ], 12 | "nonce": "nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 13 | "timestamp": "20140321T19:34:21+0000", 14 | "jsontest_hash": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=nONuDe50qGrPius4Rg4D2jfi/2zDZWAYRUG6RudJLNM=", 15 | "sign_key_test": "znsRMDBRqTXGJ7Ojip3/h2FGPu3LuoMYWgv9PKEnE/o=", 16 | "content_hash_test": "REPGqEEubBHzJMhwqDZtbt515/ntEvAMNriNR53zcdY=", 17 | "multipart_hash_test": "EG1-HMAC-SHA256 client_token=akab-client-token-xxx-xxxxxxxxxxxxxxxx;access_token=akab-access-token-xxx-xxxxxxxxxxxxxxxx;timestamp=20140321T19:34:21+0000;nonce=nonce-xx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;signature=8b5xnV0YyhRCreV0x5UEftF+EHIr2I7ebyJVNjMb7FM=" 18 | } 19 | -------------------------------------------------------------------------------- /ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine3.19 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | 4 | # https://github.com/pyenv/pyenv/wiki#suggested-build-environment 5 | RUN apk add --no-cache git bash build-base libffi-dev openssl-dev bzip2-dev zlib-dev xz-dev readline-dev sqlite-dev tk-dev 6 | RUN apk add --no-cache curl 7 | RUN curl https://pyenv.run | bash 8 | ENV PYENV_ROOT="/root/.pyenv" 9 | ENV PATH="$PYENV_ROOT/bin:$PATH" 10 | RUN pyenv install 3.9 3.10 3.11 3.12 3.13 11 | RUN echo 'eval "$(pyenv init -)"' >> ~/.profile 12 | 13 | RUN pip install --no-cache-dir tox 14 | -------------------------------------------------------------------------------- /ci/test_with_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | REPO_ROOT=$(git rev-parse --show-toplevel) 4 | IMAGE="edgegrid-python-unittest:$(uuidgen | tr '[:upper:]' '[:lower:]')" 5 | 6 | echo "Building docker image: $IMAGE in $REPO_ROOT/ci" 7 | docker build -t "$IMAGE" "$REPO_ROOT/ci" 8 | 9 | echo "Running unit tests inside the container..." 10 | set -x 11 | # umask 0000: provide permissive access rights to files created as root on the mounted volume. 12 | CMD=". ~/.profile && umask 0000 && pyenv local 3.9 3.10 3.11 3.12 3.13 && tox" 13 | docker run --rm -v $REPO_ROOT:/testdir -w /testdir "$IMAGE" sh -c "$CMD" 14 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile --extra=dev --output-file=dev-requirements.txt 6 | # 7 | astroid==3.3.9 8 | # via pylint 9 | certifi==2025.1.31 10 | # via requests 11 | charset-normalizer==3.4.1 12 | # via requests 13 | coverage[toml]==7.8.0 14 | # via pytest-cov 15 | dill==0.4.0 16 | # via pylint 17 | idna==3.10 18 | # via requests 19 | iniconfig==2.1.0 20 | # via pytest 21 | isort==6.0.1 22 | # via pylint 23 | mccabe==0.7.0 24 | # via pylint 25 | packaging==25.0 26 | # via pytest 27 | platformdirs==4.3.7 28 | # via pylint 29 | pluggy==1.5.0 30 | # via pytest 31 | pylint==3.3.6 32 | # via edgegrid-python (setup.py) 33 | pytest==8.3.5 34 | # via 35 | # edgegrid-python (setup.py) 36 | # pytest-cov 37 | pytest-cov==6.1.1 38 | # via edgegrid-python (setup.py) 39 | requests==2.32.3 40 | # via 41 | # edgegrid-python (setup.py) 42 | # requests-toolbelt 43 | requests-toolbelt==1.0.0 44 | # via edgegrid-python (setup.py) 45 | tomlkit==0.13.2 46 | # via pylint 47 | urllib3==2.4.0 48 | # via requests 49 | 50 | 51 | # THIS SECTION WAS ADDED MANUALLY 52 | # Starting with version 3.12, setuptools is no longer included in the standard Python installation. 53 | # However, we still need it until we get rid of pkg_resource style namespace packages. 54 | setuptools 55 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains executable CRUD examples for Akamai API using the EdgeGrid Python library. API calls used in these examples are available to all users. But, if you find one of the write examples doesn't work for you, talk with your account's admin about your privilege level. 4 | 5 | ## Run 6 | 7 | To run any of the files: 8 | 9 | 1. Specify the location of your `.edgerc` file. The default is set to the home directory. 10 | 2. Provide the section header for the set of credentials you'd like to use. The default is `default`. 11 | 3. For update and delete operations, replace the dummy `credentialId` with your valid `credentialId`. 12 | 13 | > **Important:** Don't use the credentials you're actively using when running the update (inactivation) and delete operations. Otherwise, you'll block your access to the Akamai APIs. 14 | 15 | 4. Open a Terminal or shell instance and run the .py file. 16 | 17 | ``` 18 | $ python examples/.py 19 | ``` 20 | 21 | ## Sample files 22 | 23 | The example in each file contains a call to one of the Identity and Access Management (IAM) API endpoints. See the [IAM API reference](https://techdocs.akamai.com/iam-api/reference/api) doc for more information on each of the calls used. 24 | 25 | | Operation | Method | Endpoint | 26 | | --- | --- | --- | 27 | | [List your API client credentials.](/examples/get-credentials.py) | `GET` | `/identity-management/v3/api-clients/self/credentials` | 28 | | [Create new API client credentials.](/examples/create-credentials.py)
This is a *quick* client and grants you the default permissions associated with your account. | `POST` | `/identity-management/v3/api-clients/self/credentials` | 29 | | [Update your credentials by ID.](/examples/update-credentials.py) | `PUT` | `/identity-management/v3/api-clients/self/credentials/{credentialId}` | 30 | | [Delete your credentials by ID.](/examples/delete-credentials.py) | `DELETE` | `/identity-management/v3/api-clients/self/credentials/{credentialId}` | 31 | 32 | Suggested chained call order: 33 | 34 | 1. Get credentials to see your base information. 35 | 2. Create a client to create a new set of credentials. 36 | 3. Update credentials to inactivate the newly created set from step 2. 37 | 4. Delete a client to delete the inactivated credentials. 38 | 5. Get credentials to verify if they're gone (the status will be `DELETED`). -------------------------------------------------------------------------------- /examples/create-credentials.py: -------------------------------------------------------------------------------- 1 | # This example creates your new API client credentials. 2 | # 3 | # To run this example: 4 | # 5 | # 1. Specify the location of your .edgerc file and the section header of the set of credentials to use. 6 | # 7 | ## The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default. 8 | # 9 | # 2. Open a Terminal or shell instance and run "python examples/create-credentials.py". 10 | # 11 | # A successful call returns a new API client with its credentialId. Use this ID in both the update and delete examples. 12 | # 13 | # For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/post-self-credentials. 14 | 15 | import requests 16 | import json 17 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 18 | from urllib.parse import urljoin 19 | 20 | edgerc = EdgeRc('~/.edgerc') 21 | section = 'default' 22 | baseurl = 'https://%s' % edgerc.get(section, 'host') 23 | 24 | session = requests.Session() 25 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 26 | 27 | path = '/identity-management/v3/api-clients/self/credentials' 28 | headers = { 29 | "Accept": "application/json" 30 | } 31 | 32 | result = session.post(urljoin(baseurl, path), headers=headers) 33 | print(result.status_code) 34 | print(json.dumps(result.json(), indent=2)) -------------------------------------------------------------------------------- /examples/delete-credentials.py: -------------------------------------------------------------------------------- 1 | # This example deletes your API client credentials. 2 | # 3 | # To run this example: 4 | # 5 | # 1. Specify the location of your .edgerc file and the section header of the set of credentials to use. 6 | # 7 | # The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default. 8 | # 9 | # 2. Add the credentialId from the update example to the path. You can only delete inactive credentials. Sending the request on an active set will return a 400. Use the update credentials example for deactivation. 10 | # 11 | # **Important:** Don't use the credentials you're actively using when deleting a set of credentials. Otherwise, you'll block your access to the Akamai APIs. 12 | # 13 | # 3. Open a Terminal or shell instance and run "python examples/delete-credentials.py". 14 | # 15 | # A successful call returns "" null. 16 | # 17 | # For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/delete-self-credential. 18 | 19 | import requests 20 | import json 21 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 22 | from urllib.parse import urljoin 23 | 24 | edgerc = EdgeRc('~/.edgerc') 25 | section = 'default' 26 | baseurl = 'https://%s' % edgerc.get(section, 'host') 27 | 28 | session = requests.Session() 29 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 30 | credentialId = 123456 31 | 32 | path = '/identity-management/v3/api-clients/self/credentials/{}'.format(credentialId) 33 | 34 | result = session.delete(urljoin(baseurl, path)) 35 | print(result.status_code) -------------------------------------------------------------------------------- /examples/get-credentials.py: -------------------------------------------------------------------------------- 1 | # This example returns a list of your API client credentials. 2 | # 3 | # To run this example: 4 | # 5 | # 1. Specify the location of your .edgerc file and the section header of the set of credentials to use. 6 | # 7 | # The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default. 8 | # 9 | # 2. Open a Terminal or shell instance and run "python examples/get-credentials.py". 10 | # 11 | # A successful call returns your credentials grouped by credentialId. 12 | # 13 | # For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/get-self-credentials. 14 | 15 | import requests 16 | import json 17 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 18 | from urllib.parse import urljoin 19 | 20 | edgerc = EdgeRc('~/.edgerc') 21 | section = 'default' 22 | baseurl = 'https://%s' % edgerc.get(section, 'host') 23 | 24 | session = requests.Session() 25 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 26 | 27 | path = '/identity-management/v3/api-clients/self/credentials' 28 | headers = { 29 | "Accept": "application/json"} 30 | querystring = { 31 | "actions": True 32 | } 33 | 34 | result = session.get(urljoin(baseurl, path), headers=headers, params=querystring) 35 | print(result.status_code) 36 | print(json.dumps(result.json(), indent=2)) 37 | 38 | -------------------------------------------------------------------------------- /examples/update-credentials.py: -------------------------------------------------------------------------------- 1 | # This example updates the credentials from the create credentials example. 2 | # 3 | # To run this example: 4 | # 5 | # 1. Specify the location of your .edgerc file and the section header of the set of credentials to use. 6 | # 7 | # The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default. 8 | # 9 | # 2. Add the credentialId for the set of credentials created using the create example as a path parameter. 10 | # 11 | # 3. Edit the expiresOn date to today's date. Optionally, you can change the description value. 12 | # 13 | # **Important:** Don't use the credentials you're actively using when inactivating a set of credentials. Otherwise, you'll block your access to the Akamai APIs. 14 | # 15 | # 4. Open a Terminal or shell instance and run "python examples/update-credentials.py". 16 | # 17 | # A successful call returns. 18 | # 19 | # For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/put-self-credential. 20 | 21 | import requests 22 | import json 23 | from akamai.edgegrid import EdgeGridAuth, EdgeRc 24 | from urllib.parse import urljoin 25 | 26 | edgerc = EdgeRc('~/.edgerc') 27 | section = 'default' 28 | baseurl = 'https://%s' % edgerc.get(section, 'host') 29 | 30 | session = requests.Session() 31 | session.auth = EdgeGridAuth.from_edgerc(edgerc, section) 32 | credentialId = 123456 33 | 34 | path = '/identity-management/v3/api-clients/self/credentials/{}'.format(credentialId) 35 | headers = { 36 | "Content-Type": "application/json", 37 | "Accept": "application/json"} 38 | payload = { 39 | "status": "INACTIVE", 40 | "expiresOn": "2024-12-30T22:09:24.000Z", # The date cannot be more than two years out or it will return a 400 41 | "description": "Update this credential" 42 | } 43 | 44 | result = session.put(urljoin(baseurl, path), headers=headers, json=payload) 45 | print(result.status_code) 46 | print(json.dumps(result.json(), indent=2)) 47 | 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile 6 | # 7 | certifi==2025.1.31 8 | # via requests 9 | charset-normalizer==3.4.1 10 | # via requests 11 | idna==3.10 12 | # via requests 13 | requests==2.32.3 14 | # via 15 | # edgegrid-python (setup.py) 16 | # requests-toolbelt 17 | requests-toolbelt==1.0.0 18 | # via edgegrid-python (setup.py) 19 | urllib3==2.4.0 20 | # via requests 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='edgegrid-python', 5 | version='2.0.1', 6 | description='{OPEN} client authentication protocol for python-requests', 7 | url='https://github.com/akamai/AkamaiOPEN-edgegrid-python', 8 | namespace_packages=['akamai'], 9 | packages=find_packages(), 10 | python_requires=">=3.9", 11 | long_description=open("README.md").read(), 12 | long_description_content_type="text/markdown", 13 | install_requires=[ 14 | 'requests>=2.24.0', 15 | 'requests_toolbelt>=0.9.1', 16 | ], 17 | extras_require={ 18 | 'dev': [ 19 | 'pylint>=2.7.0', 20 | 'pytest>=6.1.0', 21 | 'pytest-cov>=2.12.1' 22 | ], 23 | }, 24 | include_package_data=True, 25 | license='Apache 2.0', 26 | classifiers=[ 27 | 'License :: OSI Approved :: Apache Software License', 28 | 'Programming Language :: Python :: 3', 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /tox.toml: -------------------------------------------------------------------------------- 1 | requires = ["tox>=4"] 2 | env_list = ["py39", "py310", "py311", "py312", "py313"] 3 | no_package = true 4 | 5 | [env_run_base] 6 | description = "run unit tests" 7 | deps = ["-r dev-requirements.txt", "-e ."] 8 | commands_pre = [["pylint", "akamai"]] 9 | # For simplicity, pass only the latest cov report to Cobertura (previous will be overwritten) 10 | commands = [["pytest", "-v", "--junit-xml=test/{envname}_tests.xml", "-o", "junit_family=xunit2", 11 | "--cov-report", "xml:test/coverage/cobertura-coverage.xml", "--cov=akamai"]] 12 | --------------------------------------------------------------------------------