├── .coveragerc ├── .flake8 ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── rules └── source │ └── format ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── api │ └── ovh │ │ ├── client.rst │ │ ├── config.rst │ │ ├── consumer_key.rst │ │ └── exceptions.rst ├── conf.py ├── img │ └── logo.png ├── index.rst └── make.bat ├── examples ├── README.md ├── serviceExpiration │ ├── api_get_service_that_expired_soon.md │ └── serviceThatWillExpired.py └── serviceList │ ├── api_get_service_list.md │ └── serviceList.py ├── ovh ├── __init__.py ├── client.py ├── config.py ├── consumer_key.py ├── exceptions.py └── oauth2.py ├── pyproject.toml ├── scripts ├── build-debian-package-docker.sh ├── build-debian-package-recipe.sh ├── bump-version.sh └── update-copyright.sh ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── data ├── invalid.ini ├── localPartial.ini ├── system.ini ├── user.ini ├── userPartial.ini ├── user_both.ini ├── user_oauth2.ini ├── user_oauth2_incompatible.ini └── user_oauth2_invalid.ini ├── test_client.py ├── test_config.py └── test_consumer_key.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit= 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | max_line_length = 120 3 | 4 | [flake8] 5 | max-line-length = 120 6 | ignore = W503 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - "main" 8 | - "master" 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -e .[dev] 28 | - name: Check black formatting 29 | run: black --check . 30 | - name: Check isort formatting 31 | run: isort --check . 32 | - name: Lint with flake8 33 | run: flake8 34 | - name: Test with pytest 35 | run: pytest --junitxml=junit/test-results.xml --cov=ovh --cov-report=xml --cov-report=html --cov-report=lcov:coverage/cov.info 36 | - name: Coveralls GitHub Action 37 | uses: coverallsapp/github-action@v2.0.0 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # execution artefacts 2 | *.swp 3 | *.pyc 4 | .coverage 5 | 6 | # dist artefacts 7 | build/ 8 | dist/ 9 | ovh.egg-info/ 10 | *.egg 11 | 12 | # documentation artefacts 13 | docs/_build 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## 1.2.0 (2024-07-23) 5 | 6 | - [buildsystem] add project URLs to setup.cfg by @florianvazelle in #131 7 | - [buildsystem] update CODEOWNERS to maintainer group by @deathiop in #135 8 | - [feature] handle Client Credential OAuth2 authentication method by @deathiop in #134 9 | 10 | ## 1.1.2 (2024-06-07) 11 | 12 | - [fix]: debian packaging: remove MIGRATION.rst 13 | 14 | ## 1.1.1 (2024-06-07) 15 | 16 | - [feature]: handle allowedIPs parameters in CK building 17 | 18 | ## 1.1.0 (2023-04-07) 19 | 20 | - [feature]: add support for v2 routes (#115) 21 | - [buildsystem]: move to github actions, using unittest (#112, #114, #117, #113) 22 | 23 | ## 1.0.1 (2023-03-07) 24 | 25 | - [buildsystem] missing changelog entry for 1.0.0 26 | - [buildsystem] add github actions 27 | - [buildsystem] apply flake8 linting 28 | - [buildsystem] apply isort formatting 29 | - [buildsystem] apply black formatting 30 | - [buildsystem] switch to pytest 31 | 32 | ## 1.0.0 (2022-03-15) 33 | 34 | - [buildsystem] remove python 2 support (#110) 35 | - [buildsystem] added compatibility for Python 3.8, 3.9, 3.10 (#108) 36 | - [feature] add headers customisation in `raw_call` (#84) 37 | - [fix] do not send JSON body when no parameter was provided (#85) 38 | - [buildsystem] improved coverage and bump coverage library (#100) 39 | - [buildsystem] add scripts for debian packaging (#110) 40 | 41 | ## 0.6.0 (2022-03-15) 42 | 43 | - [compatibility] add support for Python 3.10 44 | - [dependencies] drop vendored requests library, added requests>=2.11.0 45 | - [fix] previous 'disable pyopenssl for ovh to fix "EPIPE"' fix is handled 46 | by requests dependency update 47 | 48 | ## 0.5.0 (2018-12-13) 49 | - [compatibility] drop support for EOL Python 2.6, 3.2 and 3.3 (#71) 50 | - [feature] Add OVH US endpoint (#63 #70) 51 | - [buildsystem] auto Pypi deployment when new tag (#60) 52 | - [documentation] fix typos (#72) 53 | - [documentation] flag package as Stable (#59) 54 | 55 | ## 0.4.8 (2017-09-15) 56 | - [feature] Add ResourceExpiredError exception (#48) 57 | 58 | ## 0.4.7 (2017-03-10) 59 | - [api] add raw_call method returning a raw requests Response object 60 | - [documentation] add advanced usage documentation 61 | - [buildsystem] fix bump-version debian/Changelog generation 62 | 63 | ## 0.4.6 (2017-02-27) 64 | - [api] add query_id property to exceptions to help error reporting 65 | - [api] remove deprecated runabove api 66 | - [feature] remove Python SNI warnings, OVH API does not need SNI (#35) 67 | - [buildsystem] Add build dependency on python3-setuptool 68 | - [buildsystem] Add debian folder 69 | 70 | ## 0.4.5 (2016-07-18) 71 | - [fix] (regression) body boolean must be sent as boolean (#34) 72 | 73 | ## 0.4.4 (2016-07-15) 74 | - [buildsystem] fix PyPi upload 75 | 76 | ## 0.4.3 (2016-07-15) 77 | - [api] fix: api expects lower case boolean value in querystring. Closes #32 (#33) 78 | - [feature] Add response in exception (#30, #31) 79 | - [feature] Read custom file on runtime (#29) 80 | - [buildsystem] chore: use find_packages in setup.py instead of hard-coded list 81 | - [buildsystem] fix: drop conflicting d2to1 dependency (closes #25 closes #27) 82 | - [documentation] improv contributing guide (#26) 83 | 84 | ## 0.4.2 (2016-04-11) 85 | - [buildsystem] fix missing cacert.pem file in package. Closes #23 86 | 87 | ## 0.4.1 (2016-04-08) 88 | - [buildsystem] fix: include the vendorized packages and package data in the install process (#22) 89 | - [buildsystem] add python 3.5 support 90 | - [documentation] add license information to README 91 | 92 | ## 0.4.0 (2016-04-07) 93 | - [feature] add consumer key helpers 94 | - [fix] disable pyopenssl for ovh to fix "EPIPE" 95 | - [buildsystem] vendor 'requests' library to fix version and configuration conflicts 96 | - [buildsystem] add 'scripts' with release helpers 97 | - [documentation] add consumer_key documentation 98 | - [documentation] fix rst format for pypi 99 | - [documentation] add service list example 100 | - [documentation] add expiring service list example 101 | - [documentation] add dedicated server KVM example 102 | - [documentation] explicitly list supported python version 103 | 104 | ## 0.3.5 (2015-07-30) 105 | 106 | - [enhancement] API call timeouts. Defaults to 180s 107 | - [buildsystem] move to new Travis build system 108 | - [documentation] send complex / python keyword parameters 109 | 110 | ## 0.3.4 (2015-06-10) 111 | 112 | - [enhancement] add NotGrantedCall, NotCredential, Forbidden, InvalidCredential exceptions 113 | 114 | ## 0.3.3 (2015-03-11) 115 | 116 | - [fix] Python 3 tests false negative 117 | - [fix] More flexible requests dependency 118 | 119 | ## 0.3.2 (2015-02-16) 120 | 121 | - [fix] Python 3 build 122 | 123 | ## 0.3.1 (2015-02-16) 124 | 125 | - [enhancement] support '_' prefixed keyword argument alias when colliding with Python reserved keywords 126 | - [enhancement] add API documentation 127 | - [enhancement] Use requests Session objects (thanks @xtrochu-edp) 128 | 129 | ## 0.3.0 (2014-11-23) 130 | - [enhancement] add kimsufi API Europe/North-America 131 | - [enhancement] add soyoustart API Europe/North-America 132 | - [Q/A] add minimal integration test 133 | 134 | ## 0.2.1 (2014-09-26) 135 | - [enhancement] add links to 'CreateToken' pages in Readme 136 | - [compatibility] add support for Python 2.6, 3.2 and 3.3 137 | 138 | ## 0.2.0 (2014-09-19) 139 | - [feature] travis / coveralls / pypi integration 140 | - [feature] config files for credentials 141 | - [feature] support ``**kwargs`` notation for ``Client.get`` query string. 142 | - [enhancement] rewrite README 143 | - [enhancement] add CONTRIBUTING guidelines 144 | - [enhancement] add MIGRATION guide 145 | - [fix] workaround ``**kwargs`` query param and function arguments collision 146 | 147 | ## 0.1.0 (2014-09-09) 148 | - [feature] ConsumerKey lifecycle 149 | - [feature] OVH and RunAbove support 150 | - [feature] OAuth 1.0 support, request signing 151 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ovh/su-developer-platform-api-exposition 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Python-OVH 2 | ========================== 3 | 4 | This project accepts contributions. In order to contribute, you should 5 | pay attention to a few things: 6 | 7 | 1. your code must follow the coding style rules 8 | 2. your code must be unit-tested 9 | 3. your code must be documented 10 | 4. your work must be signed 11 | 5. the format of the submission must be email patches or GitHub Pull Requests 12 | 13 | 14 | Coding and documentation Style: 15 | ------------------------------- 16 | 17 | - The coding style follows `PEP-8: Style Guide for Python Code `_ (~100 chars/lines is a good limit) 18 | - The documentation style follows `PEP-257: Docstring Conventions `_ 19 | 20 | A good practice is to frequently run you code through `pylint `_ 21 | and make sure the code grades does not decrease. 22 | 23 | Submitting Modifications: 24 | ------------------------- 25 | 26 | The contributions should be email patches. The guidelines are the same 27 | as the patch submission for the Linux kernel except for the DCO which 28 | is defined below. The guidelines are defined in the 29 | 'SubmittingPatches' file, available in the directory 'Documentation' 30 | of the Linux kernel source tree. 31 | 32 | It can be accessed online too: 33 | 34 | https://www.kernel.org/doc/Documentation/process/submitting-patches.rst 35 | 36 | You can submit your patches via GitHub 37 | 38 | Licensing for new files: 39 | ------------------------ 40 | 41 | Python-OVH is licensed under a (modified) BSD license. Anything contributed to 42 | Python-OVH must be released under this license. 43 | 44 | When introducing a new file into the project, please make sure it has a 45 | copyright header making clear under which license it's being released. 46 | 47 | Developer Certificate of Origin: 48 | -------------------------------- 49 | 50 | To improve tracking of contributions to this project we will use a 51 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure 52 | on patches that are being emailed around or contributed in any other 53 | way. 54 | 55 | The sign-off is a simple line at the end of the explanation for the 56 | patch, which certifies that you wrote it or otherwise have the right 57 | to pass it on as an open-source patch. The rules are pretty simple: 58 | if you can certify the below: 59 | 60 | By making a contribution to this project, I certify that: 61 | 62 | (a) The contribution was created in whole or in part by me and I have 63 | the right to submit it under the open source license indicated in 64 | the file; or 65 | 66 | (b) The contribution is based upon previous work that, to the best of 67 | my knowledge, is covered under an appropriate open source License 68 | and I have the right under that license to submit that work with 69 | modifications, whether created in whole or in part by me, under 70 | the same open source license (unless I am permitted to submit 71 | under a different license), as indicated in the file; or 72 | 73 | (c) The contribution was provided directly to me by some other person 74 | who certified (a), (b) or (c) and I have not modified it. 75 | 76 | (d) The contribution is made free of any other party's intellectual 77 | property claims or rights. 78 | 79 | (e) I understand and agree that this project and the contribution are 80 | public and that a record of the contribution (including all 81 | personal information I submit with it, including my sign-off) is 82 | maintained indefinitely and may be redistributed consistent with 83 | this project or the open source license(s) involved. 84 | 85 | 86 | then you just add a line saying 87 | 88 | Signed-off-by: Random J Developer 89 | 90 | using your real name (sorry, no pseudonyms or anonymous contributions.) 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2025, OVH SAS. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of OVH SAS nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.ini *.cfg *.rst 2 | include LICENSE 3 | recursive-include ovh *.py 4 | recursive-include docs *.py *.rst *.png Makefile make.bat 5 | recursive-include tests *.py 6 | prune docs/_build 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/ovh/python-ovh/raw/master/docs/img/logo.png 2 | :alt: Python & OVHcloud APIs 3 | :target: https://pypi.python.org/pypi/ovh 4 | 5 | Lightweight wrapper around OVHcloud's APIs. Handles all the hard work including 6 | credential creation and requests signing. 7 | 8 | .. image:: https://img.shields.io/pypi/v/ovh.svg 9 | :alt: PyPi Version 10 | :target: https://pypi.python.org/pypi/ovh 11 | .. image:: https://img.shields.io/pypi/status/ovh.svg 12 | :alt: PyPi repository status 13 | :target: https://pypi.python.org/pypi/ovh 14 | .. image:: https://img.shields.io/pypi/pyversions/ovh.svg 15 | :alt: PyPi supported Python versions 16 | :target: https://pypi.python.org/pypi/ovh 17 | .. image:: https://img.shields.io/pypi/wheel/ovh.svg 18 | :alt: PyPi Wheel status 19 | :target: https://pypi.python.org/pypi/ovh 20 | .. image:: https://github.com/ovh/python-ovh/actions/workflows/test.yaml/badge.svg?branch=master 21 | :alt: Build Status 22 | :target: https://github.com/ovh/python-ovh/actions/workflows/test.yaml 23 | .. image:: https://coveralls.io/repos/github/ovh/python-ovh/badge.svg 24 | :alt: Coverage Status 25 | :target: https://coveralls.io/github/ovh/python-ovh 26 | 27 | .. code:: python 28 | 29 | import ovh 30 | 31 | # Instantiate. Visit https://api.ovh.com/createToken/?GET=/me 32 | # to get your credentials 33 | client = ovh.Client( 34 | endpoint='ovh-eu', 35 | application_key='', 36 | application_secret='', 37 | consumer_key='', 38 | ) 39 | 40 | # Print nice welcome message 41 | print("Welcome", client.get('/me')['firstname']) 42 | 43 | Installation 44 | ============ 45 | 46 | The python wrapper works with Python 3.7+. 47 | 48 | The easiest way to get the latest stable release is to grab it from `pypi 49 | `_ using ``pip``. 50 | 51 | .. code:: bash 52 | 53 | pip install ovh 54 | 55 | Alternatively, you may get latest development version directly from Git. 56 | 57 | .. code:: bash 58 | 59 | pip install -e git+https://github.com/ovh/python-ovh.git#egg=ovh 60 | 61 | People looking for Python 2 compatibility should use 0.6.x version. 62 | 63 | Example Usage 64 | ============= 65 | 66 | Use the API on behalf of a user 67 | ------------------------------- 68 | 69 | 1. Create an application 70 | ************************ 71 | 72 | To interact with the APIs, the SDK needs to identify itself using an 73 | ``application_key`` and an ``application_secret``. To get them, you need 74 | to register your application. Depending the API you plan to use, visit: 75 | 76 | - `OVHcloud Europe `_ 77 | - `OVHcloud US `_ 78 | - `OVHcloud North-America `_ 79 | - `So you Start Europe `_ 80 | - `So you Start North America `_ 81 | - `Kimsufi Europe `_ 82 | - `Kimsufi North America `_ 83 | 84 | Once created, you will obtain an **application key (AK)** and an **application 85 | secret (AS)**. 86 | 87 | 2. Configure your application 88 | ***************************** 89 | 90 | The easiest and safest way to use your application's credentials is to create an 91 | ``ovh.conf`` configuration file in application's working directory. Here is how 92 | it looks like: 93 | 94 | .. code:: ini 95 | 96 | [default] 97 | ; general configuration: default endpoint 98 | endpoint=ovh-eu 99 | 100 | [ovh-eu] 101 | ; configuration specific to 'ovh-eu' endpoint 102 | application_key=my_app_key 103 | application_secret=my_application_secret 104 | ; uncomment following line when writing a script application 105 | ; with a single consumer key. 106 | ;consumer_key=my_consumer_key 107 | ; uncomment to enable oauth2 authentication 108 | ;client_id=my_client_id 109 | ;client_secret=my_client_secret 110 | 111 | Depending on the API you want to use, you may set the ``endpoint`` to: 112 | 113 | * ``ovh-eu`` for OVHcloud Europe API 114 | * ``ovh-us`` for OVHcloud US API 115 | * ``ovh-ca`` for OVHcloud North-America API 116 | * ``soyoustart-eu`` for So you Start Europe API 117 | * ``soyoustart-ca`` for So you Start North America API 118 | * ``kimsufi-eu`` for Kimsufi Europe API 119 | * ``kimsufi-ca`` for Kimsufi North America API 120 | 121 | See Configuration_ for more information on available configuration mechanisms. 122 | 123 | .. note:: When using a versioning system, make sure to add ``ovh.conf`` to ignored 124 | files. It contains confidential/security-sensitive information! 125 | 126 | 3. Authorize your application to access a customer account using OAuth2 127 | *********************************************************************** 128 | 129 | ``python-ovh`` supports two forms of authentication: 130 | 131 | * OAuth2, using scopped service accounts, and compatible with OVHcloud IAM 132 | * application key & application secret & consumer key (covered in the next chapter) 133 | 134 | For OAuth2, first, you need to generate a pair of valid ``client_id`` and ``client_secret``: you 135 | can proceed by [following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343) 136 | 137 | Once you have retrieved your ``client_id`` and ``client_secret``, you can create and edit 138 | a configuration file that will be used by ``python-ovh``. 139 | 140 | 4. Authorize your application to access a customer account using custom OVHcloud authentication 141 | *********************************************************************************************** 142 | 143 | To allow your application to access a customer account using the API on your 144 | behalf, you need a **consumer key (CK)**. 145 | 146 | Here is a sample code you can use to allow your application to access a 147 | customer's information: 148 | 149 | .. code:: python 150 | 151 | import ovh 152 | 153 | # create a client using configuration 154 | client = ovh.Client() 155 | 156 | # Request RO, /me API access 157 | ck = client.new_consumer_key_request() 158 | ck.add_rules(ovh.API_READ_ONLY, "/me") 159 | 160 | # Request token 161 | validation = ck.request() 162 | 163 | print("Please visit %s to authenticate" % validation['validationUrl']) 164 | input("and press Enter to continue...") 165 | 166 | # Print nice welcome message 167 | print("Welcome", client.get('/me')['firstname']) 168 | print("Btw, your 'consumerKey' is '%s'" % validation['consumerKey']) 169 | 170 | Returned ``consumerKey`` should then be kept to avoid re-authenticating your 171 | end-user on each use. 172 | 173 | .. note:: To request full and unlimited access to the API, you may use ``add_recursive_rules``: 174 | 175 | .. code:: python 176 | 177 | # Allow all GET, POST, PUT, DELETE on /* (full API) 178 | ck.add_recursive_rules(ovh.API_READ_WRITE, '/') 179 | 180 | Install a new mail redirection 181 | ------------------------------ 182 | 183 | e-mail redirections may be freely configured on domains and DNS zones hosted by 184 | OVHcloud to an arbitrary destination e-mail using API call 185 | ``POST /email/domain/{domain}/redirection``. 186 | 187 | For this call, the api specifies that the source address shall be given under the 188 | ``from`` keyword. Which is a problem as this is also a reserved Python keyword. 189 | In this case, simply prefix it with a '_', the wrapper will automatically detect 190 | it as being a prefixed reserved keyword and will substitute it. Such aliasing 191 | is only supported with reserved keywords. 192 | 193 | .. code:: python 194 | 195 | import ovh 196 | 197 | DOMAIN = "example.com" 198 | SOURCE = "sales@example.com" 199 | DESTINATION = "contact@example.com" 200 | 201 | # create a client 202 | client = ovh.Client() 203 | 204 | # Create a new alias 205 | client.post('/email/domain/%s/redirection' % DOMAIN, 206 | _from=SOURCE, 207 | to=DESTINATION, 208 | localCopy=False 209 | ) 210 | print("Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION)) 211 | 212 | Grab bill list 213 | -------------- 214 | 215 | Let's say you want to integrate OVHcloud bills into your own billing system, you 216 | could just script around the ``/me/bills`` endpoints and even get the details 217 | of each bill lines using ``/me/bill/{billId}/details/{billDetailId}``. 218 | 219 | This example assumes an existing Configuration_ with valid ``application_key``, 220 | ``application_secret`` and ``consumer_key``. 221 | 222 | .. code:: python 223 | 224 | import ovh 225 | 226 | # create a client 227 | client = ovh.Client() 228 | 229 | # Grab bill list 230 | bills = client.get('/me/bill') 231 | for bill in bills: 232 | details = client.get('/me/bill/%s' % bill) 233 | print("%12s (%s): %10s --> %s" % ( 234 | bill, 235 | details['date'], 236 | details['priceWithTax']['text'], 237 | details['pdfUrl'], 238 | )) 239 | 240 | Enable network burst in SBG1 241 | ---------------------------- 242 | 243 | 'Network burst' is a free service but is opt-in. What if you have, say, 10 244 | servers in ``SBG-1`` datacenter? You certainly don't want to activate it 245 | manually for each servers. You could take advantage of a code like this. 246 | 247 | This example assumes an existing Configuration_ with valid ``application_key``, 248 | ``application_secret`` and ``consumer_key``. 249 | 250 | .. code:: python 251 | 252 | import ovh 253 | 254 | # create a client 255 | client = ovh.Client() 256 | 257 | # get list of all server names 258 | servers = client.get('/dedicated/server/') 259 | 260 | # find all servers in SBG-1 datacenter 261 | for server in servers: 262 | details = client.get('/dedicated/server/%s' % server) 263 | if details['datacenter'] == 'sbg1': 264 | # enable burst on server 265 | client.put('/dedicated/server/%s/burst' % server, status='active') 266 | print("Enabled burst for %s server located in SBG-1" % server) 267 | 268 | List application authorized to access your account 269 | -------------------------------------------------- 270 | 271 | Thanks to the application key / consumer key mechanism, it is possible to 272 | finely track applications having access to your data and revoke this access. 273 | This examples lists validated applications. It could easily be adapted to 274 | manage revocation too. 275 | 276 | This example assumes an existing Configuration_ with valid ``application_key``, 277 | ``application_secret`` and ``consumer_key``. 278 | 279 | .. code:: python 280 | 281 | import ovh 282 | from tabulate import tabulate 283 | 284 | # create a client 285 | client = ovh.Client() 286 | 287 | credentials = client.get('/me/api/credential', status='validated') 288 | 289 | # pretty print credentials status 290 | table = [] 291 | for credential_id in credentials: 292 | credential_method = '/me/api/credential/'+str(credential_id) 293 | credential = client.get(credential_method) 294 | application = client.get(credential_method+'/application') 295 | 296 | table.append([ 297 | credential_id, 298 | '[%s] %s' % (application['status'], application['name']), 299 | application['description'], 300 | credential['creation'], 301 | credential['expiration'], 302 | credential['lastUse'], 303 | ]) 304 | print(tabulate(table, headers=['ID', 'App Name', 'Description', 305 | 'Token Creation', 'Token Expiration', 'Token Last Use'])) 306 | 307 | Before running this example, make sure you have the 308 | `tabulate `_ library installed. It's a 309 | pretty cool library to pretty print tabular data in a clean and easy way. 310 | 311 | >>> pip install tabulate 312 | 313 | 314 | Open a KVM (remote screen) on a dedicated server 315 | ------------------------------------------------ 316 | 317 | Recent dedicated servers come with an IPMI interface. A lightweight control board embedded 318 | on the server. Using IPMI, it is possible to get a remote screen on a server. This is 319 | particularly useful to tweak the BIOS or troubleshoot boot issues. 320 | 321 | Hopefully, this can easily be automated using a simple script. It assumes Java Web Start is 322 | fully installed on the machine and a consumer key allowed on the server exists. 323 | 324 | .. code:: python 325 | 326 | import ovh 327 | import sys 328 | import time 329 | import tempfile 330 | import subprocess 331 | 332 | # check arguments 333 | if len(sys.argv) != 3: 334 | print("Usage: %s SERVER_NAME ALLOWED_IP_V4" % sys.argv[0]) 335 | sys.exit(1) 336 | 337 | server_name = sys.argv[1] 338 | allowed_ip = sys.argv[2] 339 | 340 | # create a client 341 | client = ovh.Client() 342 | 343 | # create a KVM 344 | client.post('/dedicated/server/'+server_name+'/features/ipmi/access', ipToAllow=allowed_ip, ttl=15, type="kvmipJnlp") 345 | 346 | # open the KVM, when ready 347 | while True: 348 | try: 349 | # use a named temfile and feed it to java web start 350 | with tempfile.NamedTemporaryFile() as f: 351 | f.write(client.get('/dedicated/server/'+server_name+'/features/ipmi/access?type=kvmipJnlp')['value']) 352 | f.flush() 353 | subprocess.call(["javaws", f.name]) 354 | break 355 | except: 356 | time.sleep(1) 357 | 358 | Running is only a simple command line: 359 | 360 | .. code:: bash 361 | 362 | # Basic 363 | python open_kvm.py ns1234567.ip-42-42-42.eu $(curl ifconfig.ovh) 364 | 365 | # Use a specific consumer key 366 | OVH_CONSUMER_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA python open_kvm.py ns1234567.ip-42-42-42.eu $(curl -s ifconfig.ovh) 367 | 368 | Configuration 369 | ============= 370 | 371 | You have 3 ways to provide configuration to the client: 372 | - write it directly in the application code 373 | - read environment variables or predefined configuration files 374 | - read it from a custom configuration file 375 | 376 | Embed the configuration in the code 377 | ----------------------------------- 378 | 379 | The straightforward way to use OVHcloud's API keys is to embed them directly in the 380 | application code. While this is very convenient, it lacks of elegance and 381 | flexibility. 382 | 383 | Example usage: 384 | 385 | .. code:: python 386 | 387 | client = ovh.Client( 388 | endpoint='ovh-eu', 389 | application_key='', 390 | application_secret='', 391 | consumer_key='', 392 | ) 393 | 394 | Environment vars and predefined configuration files 395 | --------------------------------------------------- 396 | 397 | Alternatively it is suggested to use configuration files or environment 398 | variables so that the same code may run seamlessly in multiple environments. 399 | Production and development for instance. 400 | 401 | This wrapper will first look for direct instantiation parameters then 402 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and 403 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not 404 | provided, it will look for a configuration file of the form: 405 | 406 | .. code:: ini 407 | 408 | [default] 409 | ; general configuration: default endpoint 410 | endpoint=ovh-eu 411 | 412 | [ovh-eu] 413 | ; configuration specific to 'ovh-eu' endpoint 414 | application_key=my_app_key 415 | application_secret=my_application_secret 416 | consumer_key=my_consumer_key 417 | 418 | The client will successively attempt to locate this configuration file in 419 | 420 | 1. Current working directory: ``./ovh.conf`` 421 | 2. Current user's home directory ``~/.ovh.conf`` 422 | 3. System wide configuration ``/etc/ovh.conf`` 423 | 424 | This lookup mechanism makes it easy to overload credentials for a specific 425 | project or user. 426 | 427 | Example usage: 428 | 429 | .. code:: python 430 | 431 | client = ovh.Client() 432 | 433 | Use v1 and v2 API versions 434 | -------------------------- 435 | 436 | When using OVHcloud APIs (not So you Start or Kimsufi ones), you are given the 437 | opportunity to aim for two API versions. For the European API, for example: 438 | 439 | - the v1 is reachable through https://eu.api.ovh.com/v1 440 | - the v2 is reachable through https://eu.api.ovh.com/v2 441 | - the legacy URL is https://eu.api.ovh.com/1.0 442 | 443 | Calling ``client.get``, you can target the API version you want: 444 | 445 | .. code:: python 446 | 447 | client = ovh.Client(endpoint="ovh-eu") 448 | 449 | # Call to https://eu.api.ovh.com/v1/xdsl/xdsl-yourservice 450 | client.get("/v1/xdsl/xdsl-yourservice") 451 | 452 | # Call to https://eu.api.ovh.com/v2/xdsl/xdsl-yourservice 453 | client.get("/v2/xdsl/xdsl-yourservice") 454 | 455 | # Legacy call to https://eu.api.ovh.com/1.0/xdsl/xdsl-yourservice 456 | client.get("/xdsl/xdsl-yourservice") 457 | 458 | Custom configuration file 459 | ------------------------- 460 | 461 | You can also specify a custom configuration file. With this method, you won't be able to inherit values from environment. 462 | 463 | Example usage: 464 | 465 | .. code:: python 466 | 467 | client = ovh.Client(config_file='/my/config.conf') 468 | 469 | Passing parameters 470 | ================== 471 | 472 | You can call all the methods of the API with the necessary arguments. 473 | 474 | If an API needs an argument colliding with a Python reserved keyword, it 475 | can be prefixed with an underscore. For example, ``from`` argument of 476 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``. 477 | 478 | With characters invalid in python argument name like a dot, you can: 479 | 480 | .. code:: python 481 | 482 | import ovh 483 | 484 | params = {} 485 | params['date.from'] = '2014-01-01' 486 | params['date.to'] = '2015-01-01' 487 | 488 | # create a client 489 | client = ovh.Client() 490 | 491 | # pass parameters using ** 492 | client.post('/me/bills', **params) 493 | 494 | Advanced usage 495 | ============== 496 | 497 | Un-authenticated calls 498 | ---------------------- 499 | 500 | If the user has not authenticated yet (ie, there is no valid Consumer Key), you 501 | may force ``python-ovh`` to issue the call by passing ``_need_auth=True`` to 502 | the high level ``get()``, ``post()``, ``put()`` and ``delete()`` helpers or 503 | ``need_auth=True`` to the low level method ``Client.call()`` and 504 | ``Client.raw_call()``. 505 | 506 | This is needed when calling ``POST /auth/credential`` and ``GET /auth/time`` 507 | which are used internally for authentication and can optionally be done for 508 | most of the ``/order`` calls. 509 | 510 | Access the raw requests response objects 511 | ---------------------------------------- 512 | 513 | The high level ``get()``, ``post()``, ``put()`` and ``delete()`` helpers as well 514 | as the lower level ``call()`` will returned a parsed json response or raise in 515 | case of error. 516 | 517 | In some rare scenario, advanced setups, you may need to perform customer 518 | processing on the raw request response. It may be accessed via ``raw_call()``. 519 | This is the lowest level call in ``python-ovh``. See the source for more 520 | information. 521 | 522 | Hacking 523 | ======= 524 | 525 | This wrapper uses standard Python tools, so you should feel at home with it. 526 | Here is a quick outline of what it may look like. A good practice is to run 527 | this from a ``virtualenv``. 528 | 529 | Get the sources 530 | --------------- 531 | 532 | .. code:: bash 533 | 534 | git clone https://github.com/ovh/python-ovh.git 535 | cd python-ovh 536 | python setup.py develop 537 | 538 | You've developed a new cool feature? Fixed an annoying bug? We'd be happy 539 | to hear from you! 540 | 541 | Run the tests 542 | ------------- 543 | 544 | Simply run ``pytest``. It will automatically load its configuration from 545 | ``setup.cfg`` and output full coverage status. Since we all love quality, please 546 | note that we do not accept contributions with test coverage under 100%. 547 | 548 | .. code:: bash 549 | 550 | pip install -e .[dev] 551 | pytest 552 | 553 | Build the documentation 554 | ----------------------- 555 | 556 | Documentation is managed using the excellent ``Sphinx`` system. For example, to 557 | build HTML documentation: 558 | 559 | .. code:: bash 560 | 561 | cd python-ovh/docs 562 | make html 563 | 564 | Supported APIs 565 | ============== 566 | 567 | OVHcloud Europe 568 | --------------- 569 | 570 | - **Documentation**: https://eu.api.ovh.com/ 571 | - **Community support**: api-subscribe@ml.ovh.net 572 | - **Console**: https://eu.api.ovh.com/console 573 | - **Create application credentials**: https://eu.api.ovh.com/createApp/ 574 | - **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/ 575 | 576 | OVHcloud US 577 | ----------- 578 | 579 | - **Documentation**: https://api.us.ovhcloud.com/ 580 | - **Console**: https://api.us.ovhcloud.com/console/ 581 | - **Create application credentials**: https://api.us.ovhcloud.com/createApp/ 582 | - **Create script credentials** (all keys at once): https://api.us.ovhcloud.com/createToken/ 583 | 584 | OVHcloud North America 585 | ---------------------- 586 | 587 | - **Documentation**: https://ca.api.ovh.com/ 588 | - **Community support**: api-subscribe@ml.ovh.net 589 | - **Console**: https://ca.api.ovh.com/console 590 | - **Create application credentials**: https://ca.api.ovh.com/createApp/ 591 | - **Create script credentials** (all keys at once): https://ca.api.ovh.com/createToken/ 592 | 593 | So you Start Europe 594 | ------------------- 595 | 596 | - **Documentation**: https://eu.api.soyoustart.com/ 597 | - **Community support**: api-subscribe@ml.ovh.net 598 | - **Console**: https://eu.api.soyoustart.com/console/ 599 | - **Create application credentials**: https://eu.api.soyoustart.com/createApp/ 600 | - **Create script credentials** (all keys at once): https://eu.api.soyoustart.com/createToken/ 601 | 602 | So you Start North America 603 | -------------------------- 604 | 605 | - **Documentation**: https://ca.api.soyoustart.com/ 606 | - **Community support**: api-subscribe@ml.ovh.net 607 | - **Console**: https://ca.api.soyoustart.com/console/ 608 | - **Create application credentials**: https://ca.api.soyoustart.com/createApp/ 609 | - **Create script credentials** (all keys at once): https://ca.api.soyoustart.com/createToken/ 610 | 611 | Kimsufi Europe 612 | -------------- 613 | 614 | - **Documentation**: https://eu.api.kimsufi.com/ 615 | - **Community support**: api-subscribe@ml.ovh.net 616 | - **Console**: https://eu.api.kimsufi.com/console/ 617 | - **Create application credentials**: https://eu.api.kimsufi.com/createApp/ 618 | - **Create script credentials** (all keys at once): https://eu.api.kimsufi.com/createToken/ 619 | 620 | Kimsufi North America 621 | --------------------- 622 | 623 | - **Documentation**: https://ca.api.kimsufi.com/ 624 | - **Community support**: api-subscribe@ml.ovh.net 625 | - **Console**: https://ca.api.kimsufi.com/console/ 626 | - **Create application credentials**: https://ca.api.kimsufi.com/createApp/ 627 | - **Create script credentials** (all keys at once): https://ca.api.kimsufi.com/createToken/ 628 | 629 | Related links 630 | ============= 631 | 632 | - **Contribute**: https://github.com/ovh/python-ovh 633 | - **Report bugs**: https://github.com/ovh/python-ovh/issues 634 | - **Download**: http://pypi.python.org/pypi/ovh 635 | 636 | License 637 | ======= 638 | 639 | 3-Clause BSD 640 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | python-ovh (1.2.0) trusty; urgency=medium 2 | 3 | * build: add project URLs to setup.cfg (#131) 4 | * chore: update CODEOWNERS to maintainer group (#135) 5 | * feat: handle Client Credential OAuth2 authentication method (#134) 6 | 7 | python-ovh (1.1.2) trusty; urgency=medium 8 | 9 | * fix: debian packaging: remove file MIGRATION.rst 10 | 11 | -- Romain Beuque Fri, 07 Jun 2024 16:10:04 +0000 12 | 13 | python-ovh (1.1.1) trusty; urgency=medium 14 | 15 | * feat: handle allowedIPs parameters in CK building 16 | 17 | -- Adrien Barreau Fri, 07 Jun 2024 15:11:19 +0000 18 | 19 | python-ovh (1.1.0) trusty; urgency=medium 20 | 21 | * feat: add support for v2 routes (#115) 22 | * Build with github actions, modernize testing (#112, #114, #117, #113) 23 | 24 | -- Romain Beuque Fri, 07 Apr 2023 07:57:42 +0000 25 | 26 | python-ovh (1.0.1) experimental; urgency=medium 27 | 28 | * chore: missing changelog entry for 1.0.0 (#112) 29 | * build: add github actions (#112) 30 | * chore: apply flake8 linting (#112) 31 | * chore: apply isort formatting (#112) 32 | * chore: apply black formatting (#112) 33 | * test: switch to pytest (#112) 34 | 35 | -- Adrien Barreau Mon, 06 Mar 2023 16:40:26 +0000 36 | 37 | python-ovh (1.0.0) experimental; urgency=medium 38 | 39 | * breaking: remove python 2 support (#110) 40 | * feat: added compatibility for Python 3.8, 3.9, 3.10 (#108) 41 | * feat: add headers customisation in `raw_call` (#84) 42 | * fix: do not send JSON body when no parameter was provided (#85) 43 | * chore: improved coverage and bump coverage library (#100) 44 | * chore: add scripts for debian packaging (#110) 45 | 46 | -- Romain Beuque Tue, 15 Mar 2022 11:55:32 +0000 47 | 48 | python-ovh (0.6.0) trusty; urgency=medium 49 | 50 | * feat: added compatibility for Python 3.7 (#80) 51 | * feat: delete function now supports body parameters (#109) 52 | * fix: if HTTP status is 204 No Response, do not attempt to parse response 53 | body (#92) 54 | * fix: query parameters None should be JSON encoded and empty query params 55 | should not be sent (#102) 56 | 57 | -- Romain Beuque Tue, 15 Mar 2022 09:45:15 +0000 58 | 59 | python-ovh (0.5.0) trusty; urgency=medium 60 | 61 | * New upstream release v0.5.0 62 | * [compatibility] drop support for EOL Python 2.6, 3.2 and 3.3 (#71) 63 | * [feature] Add OVH US endpoint (#63 #70) 64 | * [buildsystem] auto Pypi deployment when new tag (#60) 65 | * [documentation] fix typos (#72) 66 | * [documentation] flag package as Stable (#59) 67 | 68 | -- Romain Beuque Thu, 13 Dec 2018 15:40:12 +0100 69 | 70 | python-ovh (0.4.8) trusty; urgency=low 71 | 72 | * New upstream release v0.4.8 73 | * [feature] Add ResourceExpiredError exception (#48) 74 | 75 | -- Geoffrey Bauduin Fri, 15 Sep 2017 11:53:30 +0200 76 | 77 | python-ovh (0.4.7) trusty; urgency=low 78 | 79 | * New upstream release v0.4.7 80 | * [api] add raw_call method returning a raw requests Response object 81 | * [documentation] add advanced usage documentation 82 | * [buildsystem] fix bump-version debian/Changelog generation 83 | 84 | -- Jean-Tiare Le Bigot Fri, 10 Mar 2017 13:00:16 +0100 85 | 86 | python-ovh (0.4.5) trusty; urgency=low 87 | 88 | * New upstream release v0.4.5 89 | * Add build dependency on python3-setuptool update copyrights to 2017 90 | feat(query-id): add query_id property to exceptions in order to 91 | provide debugging facilities when encounter API issues Add debian 92 | folder fix: remove Python SNI warnings, OVH API does not need SNI 93 | (#35) fix: coveralls version 94 | 95 | -- Jean-Tiare Le Bigot Wed, 15 Feb 2017 11:47:29 +0100 96 | 97 | python-ovh (0.4.4) trusty; urgency=medium 98 | 99 | * New upstream release v0.4.4 100 | * Add VERSION file, needed for our compile system 101 | * Add debian folder 102 | * fix: remove Python SNI warnings, OVH API does not need SNI (#35) 103 | * fix: coveralls version 104 | * fix: (regression) body boolean must be sent as boolean (#34) 105 | 106 | -- Arnaud Morin Mon, 03 Oct 2016 14:34:21 +0200 107 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python-ovh 2 | Section: python 3 | Priority: optional 4 | Maintainer: Romain Beuque 5 | Build-Depends: debhelper (>= 9), dh-python, python3-setuptools, python3-all 6 | Standards-Version: 3.9.5 7 | X-Python3-Version: >= 3.4 8 | 9 | Package: python3-ovh 10 | Architecture: all 11 | Depends: ${misc:Depends}, ${python3:Depends}, python3, python3-requests 12 | Description: Wrapper around OVH's APIs (Python 3) 13 | Lightweight wrapper around OVH's APIs. Handles all the hard work 14 | including credential creation and requests signing. 15 | . 16 | This package provides Python 3 module bindings only. 17 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: python-ovh 3 | Source: https://github.com/ovh/python-ovh 4 | 5 | Files: * 6 | Copyright: 2013-2025 OVH SAS 7 | License: 3-clause BSD 8 | See LICENSE 9 | 10 | 11 | Files: debian/* 12 | Copyright: 2013-2025 OVH SAS 13 | License: 3-clause BSD 14 | See LICENSE 15 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.rst 2 | CONTRIBUTING.rst 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #! /usr/bin/make -f 2 | 3 | #export DH_VERBOSE = 1 4 | export PYBUILD_NAME = ovh 5 | 6 | # do not launch unit tests during build 7 | export DEB_BUILD_OPTIONS=nocheck 8 | 9 | %: 10 | dh $@ --with python3 --buildsystem=pybuild 11 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Python-OVH.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Python-OVH.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Python-OVH" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Python-OVH" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/python-ovh/3a1485d0563916f449fddfe86fa1e5a93c0e1b3b/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/api/ovh/client.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | Client Module 3 | ############# 4 | 5 | .. currentmodule:: ovh.client 6 | 7 | .. automodule:: ovh.client 8 | 9 | .. autoclass:: Client 10 | 11 | Constructor 12 | =========== 13 | 14 | __init__ 15 | -------- 16 | 17 | .. automethod:: Client.__init__ 18 | 19 | High level helpers 20 | ================== 21 | 22 | request_consumerkey 23 | ------------------- 24 | 25 | Helpers to generate a consumer key. See ``new_consumer_key_request`` 26 | below for a full working example or :py:class:`ConsumerKeyRequest` 27 | for detailed implementation. 28 | 29 | The basic idea of ``ConsumerKeyRequest`` is to generate appropriate 30 | authorization requests from human readable function calls. In short: 31 | use it! 32 | 33 | .. automethod:: Client.new_consumer_key_request 34 | 35 | .. automethod:: Client.request_consumerkey 36 | 37 | get/post/put/delete 38 | ------------------- 39 | 40 | Shortcuts around :py:func:`Client.call`. This is the recommended way to use the 41 | wrapper. 42 | 43 | For example, requesting the list of all bills would look like: 44 | 45 | .. code:: python 46 | 47 | bills = client.get('/me/bills') 48 | 49 | In a similar fashion, enabling network burst on a specific server would look 50 | like: 51 | 52 | .. code:: python 53 | 54 | client.put('/dedicated/server/%s/burst' % server_name, status='active') 55 | 56 | :param str target: Rest Method as shown in API's console. 57 | :param boolean need_auth: When `False`, bypass the signature process. This is 58 | interesting when calling authentication related method. Defaults to `True` 59 | :param dict kwargs: (:py:func:`Client.post` and :py:func:`Client.put` only) 60 | all extra keyword arguments are passed as `data` dict to `call`. This is a 61 | syntaxic sugar to call API entrypoints using a regular method syntax. 62 | 63 | .. automethod:: Client.get 64 | .. automethod:: Client.post 65 | .. automethod:: Client.put 66 | .. automethod:: Client.delete 67 | 68 | Low level API 69 | ============= 70 | 71 | call 72 | ---- 73 | 74 | .. automethod:: Client.call 75 | 76 | time_delta 77 | ---------- 78 | 79 | .. autoattribute:: Client.time_delta 80 | -------------------------------------------------------------------------------- /docs/api/ovh/config.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | Config Module 3 | ############# 4 | 5 | .. currentmodule:: ovh.config 6 | 7 | .. automodule:: ovh.config 8 | 9 | .. autoclass:: ConfigurationManager 10 | 11 | Methods 12 | ======= 13 | 14 | __init__ 15 | -------- 16 | 17 | .. automethod:: ConfigurationManager.__init__ 18 | 19 | get 20 | --- 21 | 22 | .. automethod:: ConfigurationManager.get 23 | 24 | Globals 25 | ======= 26 | 27 | .. autodata:: ovh.config.CONFIG_PATH 28 | :annotation: 29 | .. autodata:: ovh.config.config 30 | :annotation: 31 | -------------------------------------------------------------------------------- /docs/api/ovh/consumer_key.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | Client Module 3 | ############# 4 | 5 | .. currentmodule:: ovh.consumer_key 6 | 7 | .. automodule:: ovh.consumer_key 8 | 9 | .. autoclass:: ConsumerKeyRequest 10 | 11 | Constructor 12 | =========== 13 | 14 | __init__ 15 | -------- 16 | 17 | .. automethod:: ConsumerKeyRequest.__init__ 18 | 19 | Helpers 20 | ======= 21 | 22 | Generate rules 23 | -------------- 24 | 25 | .. automethod:: ConsumerKeyRequest.add_rule 26 | .. automethod:: ConsumerKeyRequest.add_rules 27 | .. automethod:: ConsumerKeyRequest.add_recursive_rules 28 | 29 | Trigger request 30 | --------------- 31 | 32 | .. automethod:: ConsumerKeyRequest.request 33 | 34 | -------------------------------------------------------------------------------- /docs/api/ovh/exceptions.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | Exceptions Module 3 | ################# 4 | 5 | .. currentmodule:: ovh.exceptions 6 | 7 | .. automodule:: ovh.exceptions 8 | 9 | .. autoexception:: APIError 10 | .. autoexception:: HTTPError 11 | .. autoexception:: InvalidKey 12 | .. autoexception:: InvalidResponse 13 | .. autoexception:: InvalidRegion 14 | .. autoexception:: ReadOnlyError 15 | .. autoexception:: ResourceNotFoundError 16 | .. autoexception:: BadParametersError 17 | .. autoexception:: ResourceConflictError 18 | .. autoexception:: NetworkError 19 | .. autoexception:: NotGrantedCall 20 | .. autoexception:: NotCredential 21 | .. autoexception:: Forbidden 22 | .. autoexception:: InvalidCredential 23 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python-OVH documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Aug 26 13:44:18 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | 20 | # -- General configuration ------------------------------------------------ 21 | 22 | # If your documentation needs a minimal Sphinx version, state it here. 23 | # needs_sphinx = '1.0' 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 27 | # ones. 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.doctest", 31 | "sphinx.ext.coverage", 32 | "sphinx.ext.viewcode", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = ".rst" 40 | 41 | # The encoding of source files. 42 | # source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = "Python-OVH" 49 | copyright = "2013-2014, OVH SAS" 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = "0.3" 57 | # The full version, including alpha/beta/rc tags. 58 | release = "0.5.0" 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | # today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | # today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = ["_build"] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | # default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | # add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | # add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | # show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = "sphinx" 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | # modindex_common_prefix = [] 94 | 95 | # If true, keep warnings as "system message" paragraphs in the built documents. 96 | # keep_warnings = False 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = "default" 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | # html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | # html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | # html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | # html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | # html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ["_static"] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | # html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | # html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | # html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | # html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | # html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | # html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | # html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | # html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | # html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | # html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | # html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | # html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | # html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = "python-ovh-doc" 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | # 'papersize': 'letterpaper', 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | # 'pointsize': '10pt', 191 | # Additional stuff for the LaTeX preamble. 192 | # 'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, 197 | # author, documentclass [howto, manual, or own class]). 198 | latex_documents = [ 199 | ("index", "Python-OVH.tex", "Python-OVH Documentation", "Jean-Tiare Le Bigot", "manual"), 200 | ] 201 | 202 | # The name of an image file (relative to this directory) to place at the top of 203 | # the title page. 204 | # latex_logo = None 205 | 206 | # For "manual" documents, if this is true, then toplevel headings are parts, 207 | # not chapters. 208 | # latex_use_parts = False 209 | 210 | # If true, show page references after internal links. 211 | # latex_show_pagerefs = False 212 | 213 | # If true, show URL addresses after external links. 214 | # latex_show_urls = False 215 | 216 | # Documents to append as an appendix to all manuals. 217 | # latex_appendices = [] 218 | 219 | # If false, no module index is generated. 220 | # latex_domain_indices = True 221 | 222 | 223 | # -- Options for manual page output --------------------------------------- 224 | 225 | # One entry per manual page. List of tuples 226 | # (source start file, name, description, authors, manual section). 227 | man_pages = [("index", "python-ovh", "Python-OVH Documentation", ["Jean-Tiare Le Bigot"], 1)] 228 | 229 | # If true, show URL addresses after external links. 230 | # man_show_urls = False 231 | 232 | 233 | # -- Options for Texinfo output ------------------------------------------- 234 | 235 | # Grouping the document tree into Texinfo files. List of tuples 236 | # (source start file, target name, title, author, 237 | # dir menu entry, description, category) 238 | texinfo_documents = [ 239 | ( 240 | "index", 241 | "Python-OVH", 242 | "Python-OVH Documentation", 243 | "Jean-Tiare Le Bigot", 244 | "Python-OVH", 245 | "OVH Rest API wrapper.", 246 | "API", 247 | ), 248 | ] 249 | 250 | # Documents to append as an appendix to all manuals. 251 | # texinfo_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | # texinfo_domain_indices = True 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | # texinfo_show_urls = 'footnote' 258 | 259 | # If true, do not generate a @detailmenu in the "Top" node's menu. 260 | # texinfo_no_detailmenu = False 261 | -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ovh/python-ovh/3a1485d0563916f449fddfe86fa1e5a93c0e1b3b/docs/img/logo.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Python-OVH documentation master file, created by 2 | sphinx-quickstart on Tue Aug 26 13:44:18 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root ``toctree`` directive. 5 | 6 | Python-OVH: lightweight wrapper around OVH's APIs 7 | ================================================= 8 | 9 | Thin wrapper around OVH's APIs. Handles all the hard work including credential 10 | creation and requests signing. 11 | 12 | .. code:: python 13 | 14 | import ovh 15 | 16 | # Instantiate. Visit https://api.ovh.com/createToken/index.cgi?GET=/me 17 | # to get your credentials 18 | client = ovh.Client( 19 | endpoint='ovh-eu', 20 | application_key='', 21 | application_secret='', 22 | consumer_key='', 23 | ) 24 | 25 | # Print nice welcome message 26 | print("Welcome", client.get('/me')['firstname']) 27 | 28 | Installation 29 | ============ 30 | 31 | The easiest way to get the latest stable release is to grab it from `pypi 32 | `_ using ``pip``. 33 | 34 | .. code:: bash 35 | 36 | pip install ovh 37 | 38 | Alternatively, you may get latest development version directly from Git. 39 | 40 | .. code:: bash 41 | 42 | pip install -e git+https://github.com/ovh/python-ovh.git#egg=ovh 43 | 44 | API Documentation 45 | ================= 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | :glob: 50 | 51 | api/ovh/* 52 | 53 | Example Usage 54 | ============= 55 | 56 | Use the API on behalf of a user 57 | ------------------------------- 58 | 59 | 1. Create an application 60 | ************************ 61 | 62 | To interact with the APIs, the SDK needs to identify itself using an 63 | ``application_key`` and an ``application_secret``. To get them, you need 64 | to register your application. Depending the API you plan yo use, visit: 65 | 66 | - `OVH Europe `_ 67 | - `OVH North-America `_ 68 | 69 | Once created, you will obtain an **application key (AK)** and an **application 70 | secret (AS)**. 71 | 72 | 2. Configure your application 73 | ***************************** 74 | 75 | The easiest and safest way to use your application's credentials is create an 76 | ``ovh.conf`` configuration file in application's working directory. Here is how 77 | it looks like: 78 | 79 | .. code:: ini 80 | 81 | [default] 82 | ; general configuration: default endpoint 83 | endpoint=ovh-eu 84 | 85 | [ovh-eu] 86 | ; configuration specific to 'ovh-eu' endpoint 87 | application_key=my_app_key 88 | application_secret=my_application_secret 89 | ; uncomment following line when writing a script application 90 | ; with a single consumer key. 91 | ;consumer_key=my_consumer_key 92 | 93 | Depending on the API you want to use, you may set the ``endpoint`` to: 94 | 95 | * ``ovh-eu`` for OVH Europe API 96 | * ``ovh-ca`` for OVH North-America API 97 | 98 | See Configuration_ for more informations on available configuration mechanisms. 99 | 100 | .. note:: When using a versioning system, make sure to add ``ovh.conf`` to ignored 101 | files. It contains confidential/security-sensitive information! 102 | 103 | 3. Authorize your application to access a customer account 104 | ********************************************************** 105 | 106 | To allow your application to access a customer account using the API on your 107 | behalf, you need a **consumer key (CK)**. 108 | 109 | .. code:: python 110 | 111 | try: 112 | input = raw_input 113 | except NameError: 114 | pass 115 | 116 | import ovh 117 | 118 | # create a client using configuration 119 | client = ovh.Client() 120 | 121 | # Request RO, /me API access 122 | access_rules = [ 123 | {'method': 'GET', 'path': '/me'}, 124 | ] 125 | 126 | # Request token 127 | validation = client.request_consumerkey(access_rules) 128 | 129 | print("Please visit %s to authenticate" % validation['validationUrl']) 130 | input("and press Enter to continue...") 131 | 132 | # Print nice welcome message 133 | print("Welcome", client.get('/me')['firstname']) 134 | print("Btw, your 'consumerKey' is '%s'" % validation['consumerKey']) 135 | 136 | 137 | Returned ``consumerKey`` should then be kept to avoid re-authenticating your 138 | end-user on each use. 139 | 140 | .. note:: To request full and unlimited access to the API, you may use wildcards: 141 | 142 | .. code:: python 143 | 144 | access_rules = [ 145 | {'method': 'GET', 'path': '/*'}, 146 | {'method': 'POST', 'path': '/*'}, 147 | {'method': 'PUT', 'path': '/*'}, 148 | {'method': 'DELETE', 'path': '/*'} 149 | ] 150 | 151 | Install a new mail redirection 152 | ------------------------------ 153 | 154 | e-mail redirections may be freely configured on domains and DNS zones hosted by 155 | OVH to an arbitrary destination e-mail using API call 156 | ``POST /email/domain/{domain}/redirection``. 157 | 158 | For this call, the api specifies that the source address shall be given under the 159 | ``from`` keyword. Which is a problem as this is also a reserved Python keyword. 160 | In this case, simply prefix it with a '_', the wrapper will automatically detect 161 | it as being a prefixed reserved keyword and will substitute it. Such aliasing 162 | is only supported with reserved keywords. 163 | 164 | .. code:: python 165 | 166 | import ovh 167 | 168 | DOMAIN = "example.com" 169 | SOURCE = "sales@example.com" 170 | DESTINATION = "contact@example.com" 171 | 172 | # create a client 173 | client = ovh.Client() 174 | 175 | # Create a new alias 176 | client.post('/email/domain/%s/redirection' % DOMAIN, 177 | _from=SOURCE, 178 | to=DESTINATION 179 | localCopy=False 180 | ) 181 | print("Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION)) 182 | 183 | Grab bill list 184 | -------------- 185 | 186 | Let's say you want to integrate OVH bills into your own billing system, you 187 | could just script around the ``/me/bills`` endpoints and even get the details 188 | of each bill lines using ``/me/bill/{billId}/details/{billDetailId}``. 189 | 190 | This example assumes an existing Configuration_ with valid ``application_key``, 191 | ``application_secret`` and ``consumer_key``. 192 | 193 | .. code:: python 194 | 195 | import ovh 196 | 197 | # create a client without a consumerKey 198 | client = ovh.Client() 199 | 200 | # Grab bill list 201 | bills = client.get('/me/bill') 202 | for bill in bills: 203 | details = client.get('/me/bill/%s' % bill) 204 | print("%12s (%s): %10s --> %s" % ( 205 | bill, 206 | details['date'], 207 | details['priceWithTax']['text'], 208 | details['pdfUrl'], 209 | )) 210 | 211 | Enable network burst in SBG1 212 | ---------------------------- 213 | 214 | 'Network burst' is a free service but is opt-in. What if you have, say, 10 215 | servers in ``SBG-1`` datacenter? You certainly don't want to activate it 216 | manually for each servers. You could take advantage of a code like this. 217 | 218 | This example assumes an existing Configuration_ with valid ``application_key``, 219 | ``application_secret`` and ``consumer_key``. 220 | 221 | .. code:: python 222 | 223 | import ovh 224 | 225 | # create a client 226 | client = ovh.Client() 227 | 228 | # get list of all server names 229 | servers = client.get('/dedicated/server/') 230 | 231 | # find all servers in SBG-1 datacenter 232 | for server in servers: 233 | details = client.get('/dedicated/server/%s' % server) 234 | if details['datacenter'] == 'sbg1': 235 | # enable burst on server 236 | client.put('/dedicated/server/%s/burst' % server, status='active') 237 | print("Enabled burst for %s server located in SBG-1" % server) 238 | 239 | List application authorized to access your account 240 | -------------------------------------------------- 241 | 242 | Thanks to the application key / consumer key mechanism, it is possible to 243 | finely track applications having access to your data and revoke this access. 244 | This examples lists validated applications. It could easily be adapted to 245 | manage revocation too. 246 | 247 | This example assumes an existing Configuration_ with valid ``application_key``, 248 | ``application_secret`` and ``consumer_key``. 249 | 250 | .. code:: python 251 | 252 | import ovh 253 | from tabulate import tabulate 254 | 255 | # create a client 256 | client = ovh.Client() 257 | 258 | credentials = client.get('/me/api/credential', status='validated') 259 | 260 | # pretty print credentials status 261 | table = [] 262 | for credential_id in credentials: 263 | credential_method = '/me/api/credential/'+str(credential_id) 264 | credential = client.get(credential_method) 265 | application = client.get(credential_method+'/application') 266 | 267 | table.append([ 268 | credential_id, 269 | '[%s] %s' % (application['status'], application['name']), 270 | application['description'], 271 | credential['creation'], 272 | credential['expiration'], 273 | credential['lastUse'], 274 | ]) 275 | print(tabulate(table, headers=['ID', 'App Name', 'Description', 276 | 'Token Creation', 'Token Expiration', 'Token Last Use'])) 277 | 278 | Before running this example, make sure you have the 279 | `tabulate `_ library installed. It's a 280 | pretty cool library to pretty print tabular data in a clean and easy way. 281 | 282 | >>> pip install tabulate 283 | 284 | Configuration 285 | ============= 286 | 287 | The straightforward way to use OVH's API keys is to embed them directly in the 288 | application code. While this is very convenient, it lacks of elegance and 289 | flexibility. 290 | 291 | Alternatively it is suggested to use configuration files or environment 292 | variables so that the same code may run seamlessly in multiple environments. 293 | Production and development for instance. 294 | 295 | This wrapper will first look for direct instantiation parameters then 296 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and 297 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not 298 | provided, it will look for a configuration file of the form: 299 | 300 | .. code:: ini 301 | 302 | [default] 303 | ; general configuration: default endpoint 304 | endpoint=ovh-eu 305 | 306 | [ovh-eu] 307 | ; configuration specific to 'ovh-eu' endpoint 308 | application_key=my_app_key 309 | application_secret=my_application_secret 310 | consumer_key=my_consumer_key 311 | 312 | The client will successively attempt to locate this configuration file in 313 | 314 | 1. Current working directory: ``./ovh.conf`` 315 | 2. Current user's home directory ``~/.ovh.conf`` 316 | 3. System wide configuration ``/etc/ovh.conf`` 317 | 318 | This lookup mechanism makes it easy to overload credentials for a specific 319 | project or user. 320 | 321 | Passing parameters 322 | ================== 323 | 324 | You can call all the methods of the API with the necessary arguments. 325 | 326 | If an API needs an argument colliding with a Python reserved keyword, it 327 | can be prefixed with an underscore. For example, ``from`` argument of 328 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``. 329 | 330 | With characters invalid in python argument name like a dot, you can: 331 | 332 | .. code:: python 333 | 334 | import ovh 335 | 336 | params = {} 337 | params['date.from'] = '2014-01-01' 338 | params['date.to'] = '2015-01-01' 339 | 340 | # create a client 341 | client = ovh.Client() 342 | 343 | # pass parameters using ** 344 | client.post('/me/bills', **params) 345 | 346 | Advanced usage 347 | ============== 348 | 349 | Un-authenticated calls 350 | ---------------------- 351 | 352 | If the user has not authenticated yet (ie, there is no valid Consumer Key), you 353 | may force ``python-ovh`` to issue the call by passing ``_need_auth=True`` to 354 | the high level ``get()``, ``post()``, ``put()`` and ``delete()`` helpers or 355 | ``need_auth=True`` to the low level method ``Client.call()`` and 356 | ``Client.raw_call()``. 357 | 358 | This is needed when calling ``POST /auth/credential`` and ``GET /auth/time`` 359 | which are used internally for authentication and can optionally be done for 360 | most of the ``/order`` calls. 361 | 362 | Access the raw requests response objects 363 | ---------------------------------------- 364 | 365 | The high level ``get()``, ``post()``, ``put()`` and ``delete()`` helpers as well 366 | as the lower level ``call()`` will returned a parsed json response or raise in 367 | case of error. 368 | 369 | In some rare scenario, advanced setups, you may need to perform customer 370 | processing on the raw request response. It may be accessed via ``raw_call()``. 371 | This is the lowest level call in ``python-ovh``. See the source for more 372 | information. 373 | 374 | Hacking 375 | ======= 376 | 377 | This wrapper uses standard Python tools, so you should feel at home with it. 378 | Here is a quick outline of what it may look like. A good practice is to run 379 | this from a ``virtualenv``. 380 | 381 | Get the sources 382 | --------------- 383 | 384 | .. code:: bash 385 | 386 | git clone https://github.com/ovh/python-ovh.git 387 | cd python-ovh 388 | python setup.py develop 389 | 390 | You've developed a new cool feature ? Fixed an annoying bug ? We'd be happy 391 | to hear from you ! 392 | 393 | Run the tests 394 | ------------- 395 | 396 | Simply run ``nosetests``. It will automatically load its configuration from 397 | ``setup.cfg`` and output full coverage status. Since we all love quality, please 398 | note that we do not accept contributions with test coverage under 100%. 399 | 400 | .. code:: bash 401 | 402 | pip install -e .[dev] 403 | nosetests # 100% coverage is a hard minimum 404 | 405 | 406 | Build the documentation 407 | ----------------------- 408 | 409 | Documentation is managed using the excellent ``Sphinx`` system. For example, to 410 | build HTML documentation: 411 | 412 | .. code:: bash 413 | 414 | cd python-ovh/docs 415 | make html 416 | 417 | Supported APIs 418 | ============== 419 | 420 | OVH Europe 421 | ---------- 422 | 423 | - **Documentation**: https://eu.api.ovh.com/ 424 | - **Community support**: api-subscribe@ml.ovh.net 425 | - **Console**: https://eu.api.ovh.com/console 426 | - **Create application credentials**: https://eu.api.ovh.com/createApp/ 427 | 428 | OVH North America 429 | ----------------- 430 | 431 | - **Documentation**: https://ca.api.ovh.com/ 432 | - **Community support**: api-subscribe@ml.ovh.net 433 | - **Console**: https://ca.api.ovh.com/console 434 | - **Create application credentials**: https://ca.api.ovh.com/createApp/ 435 | 436 | Related links 437 | ============= 438 | 439 | - **Contribute**: https://github.com/ovh/python-ovh 440 | - **Report bugs**: https://github.com/ovh/python-ovh/issues 441 | - **Download**: http://pypi.python.org/pypi/ovh 442 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Python-OVH.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Python-OVH.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Python wrapper examples 2 | ----------------------- 3 | 4 | In this part, you can find real use cases for the OVH Python wrapper 5 | 6 | ## OVH services 7 | 8 | Following examples are related to cross services proposed by OVH. 9 | 10 | - [How to get the list of services expiring soon?](serviceExpiration/api_get_service_that_expired_soon.md) 11 | 12 | -------------------------------------------------------------------------------- /examples/serviceExpiration/api_get_service_that_expired_soon.md: -------------------------------------------------------------------------------- 1 | How to get list of services expiring soon with Python wrapper? 2 | -------------------------------------------------------------- 3 | 4 | This documentation will help you to list what services will be expired soon and need to be renew. The following script will check the expiration date of each services attached to your consumer_key 5 | 6 | ## Requirements 7 | 8 | - Having an OVH Account with services inside 9 | 10 | ## Install Python wrapper 11 | 12 | The easiest way to get the latest stable release is to grab it from pypi using ```pip```. 13 | 14 | ```bash 15 | pip install tabulate ovh 16 | ``` 17 | 18 | ## Create a new token 19 | 20 | You can create a new token using this url: [https://api.ovh.com/createToken/?GET=/*](https://api.ovh.com/createToken/?GET=/*). 21 | Keep application key, application secret and consumer key and replace default values in ```ovh.conf``` file. 22 | 23 | ```ini 24 | [default] 25 | ; general configuration: default endpoint 26 | endpoint=ovh-eu 27 | 28 | [ovh-eu] 29 | ; configuration specific to 'ovh-eu' endpoint 30 | application_key=my_app_key 31 | application_secret=my_application_secret 32 | ; uncomment following line when writing a script application 33 | ; with a single consumer key. 34 | consumer_key=my_consumer_key 35 | ``` 36 | 37 | Be warned, this token is only valid to get information of your OVH services. You cannot changes or delete your products with it. 38 | If you need a more generic token, you may adjust the **Rights** fields at your needs. 39 | 40 | ## Download the script 41 | 42 | - Download and edit the python file to get service that will expired. You can download [this file](serviceThatWillExpired.py). By default, delay is defined as 60 days. You can edit the script to change the ```delay```. 43 | 44 | ## Run script 45 | 46 | ```bash 47 | python serviceThatWillExpired.py 48 | ``` 49 | 50 | For instance, using the example values in this script, the answer would look like: 51 | ```bash 52 | Type ID status expiration date 53 | ----------------------- -------------------------------- -------- ----------------- 54 | cdn/webstorage cdnstatic-no42-1337 ok 2016-02-14 55 | cloud/project 42xxxxxxxxxxxxxxxxxxxxxxxxxxx42 expired 2016-01-30 56 | hosting/privateDatabase no42-001 ok 2016-02-15 57 | license/office office42.o365.ovh.com ok 2016-02-15 58 | router router-rbx-1-sdr-1337 expired 2016-01-31 59 | ``` 60 | 61 | ## What's more? 62 | 63 | You can discover all OVH possibilities by using API console to show all available endpoints: [https://api.ovh.com/console](https://api.ovh.com/console) 64 | 65 | -------------------------------------------------------------------------------- /examples/serviceExpiration/serviceThatWillExpired.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from tabulate import tabulate 4 | 5 | import ovh 6 | 7 | # Services type desired to mine. To speed up the script, delete service type you don't use! 8 | service_types = [ 9 | "allDom", 10 | "cdn/dedicated", 11 | "cdn/website", 12 | "cdn/webstorage", 13 | "cloud/project", 14 | "cluster/hadoop", 15 | "dedicated/housing", 16 | "dedicated/nas", 17 | "dedicated/nasha", 18 | "dedicated/server", 19 | "dedicatedCloud", 20 | "domain/zone", 21 | "email/domain", 22 | "email/exchange", 23 | "freefax", 24 | "hosting/privateDatabase", 25 | "hosting/web", 26 | "hosting/windows", 27 | "hpcspot", 28 | "license/cloudLinux", 29 | "license/cpanel", 30 | "license/directadmin", 31 | "license/office", 32 | "license/plesk", 33 | "license/sqlserver", 34 | "license/virtuozzo", 35 | "license/windows", 36 | "license/worklight", 37 | "overTheBox", 38 | "pack/xdsl", 39 | "partner", 40 | "router", 41 | "sms", 42 | "telephony", 43 | "telephony/spare", 44 | "veeamCloudConnect", 45 | "vps", 46 | "xdsl", 47 | "xdsl/spare", 48 | ] 49 | # Delay before expiration in days 50 | delay = 60 51 | 52 | # Create a client using ovh.conf 53 | client = ovh.Client() 54 | 55 | # Compute now + delay 56 | delay_date = datetime.datetime.now() + datetime.timedelta(days=delay) 57 | 58 | services_will_expired = [] 59 | 60 | # Check all OVH product (service type) 61 | for service_type in service_types: 62 | service_list = client.get("/%s" % service_type) 63 | 64 | # If we found you have this one or more of this product, we get these information 65 | for service in service_list: 66 | service_infos = client.get("/%s/%s/serviceInfos" % (service_type, service)) 67 | service_expiration_date = datetime.datetime.strptime(service_infos["expiration"], "%Y-%m-%d") 68 | 69 | # If the expiration date is before (now + delay) date, we add it into our listing 70 | if service_expiration_date < delay_date: 71 | services_will_expired.append([service_type, service, service_infos["status"], service_infos["expiration"]]) 72 | 73 | # At the end, we show service expired or that will expire (in a table with tabulate) 74 | print(tabulate(services_will_expired, headers=["Type", "ID", "status", "expiration date"])) 75 | -------------------------------------------------------------------------------- /examples/serviceList/api_get_service_list.md: -------------------------------------------------------------------------------- 1 | How to get list of services with Python wrapper? 2 | ------------------------------------------------ 3 | 4 | This documentation will help you to list your services at ovh. 5 | 6 | ## Requirements 7 | 8 | - Having an OVH Account with services inside 9 | 10 | ## Install Python wrapper 11 | 12 | The easiest way to get the latest stable release is to grab it from pypi using ```pip```. 13 | 14 | ```bash 15 | pip install tabulate ovh 16 | ``` 17 | 18 | ## Create a new token 19 | 20 | You can create a new token using this url: [https://api.ovh.com/createToken/?GET=/*](https://api.ovh.com/createToken/?GET=/*). 21 | Keep application key, application secret and consumer key and replace default values in ```ovh.conf``` file. 22 | 23 | ```ini 24 | [default] 25 | ; general configuration: default endpoint 26 | endpoint=ovh-eu 27 | 28 | [ovh-eu] 29 | ; configuration specific to 'ovh-eu' endpoint 30 | application_key=my_app_key 31 | application_secret=my_application_secret 32 | ; uncomment following line when writing a script application 33 | ; with a single consumer key. 34 | consumer_key=my_consumer_key 35 | ``` 36 | 37 | Be warned, this token is only valid to get information of your OVH services. You cannot changes or delete your products with it. 38 | If you need a more generic token, you may adjust the **Rights** fields at your needs. 39 | 40 | ## Download the script 41 | 42 | - Download and edit the python file to get service that will expired. You can download [this file](serviceList.py). 43 | 44 | ## Run script 45 | 46 | ```bash 47 | python serviceList.py 48 | ``` 49 | 50 | For instance, using the example values in this script, the answer would look like: 51 | ```bash 52 | Type ID status expiration date 53 | ----------------------- -------------------------------- -------- ----------------- 54 | cdn/webstorage cdnstatic-no42-1337 ok 2016-02-14 55 | cloud/project 42xxxxxxxxxxxxxxxxxxxxxxxxxxx42 expired 2016-01-30 56 | hosting/privateDatabase no42-001 ok 2016-02-15 57 | license/office office42.o365.ovh.com ok 2016-02-15 58 | router router-rbx-1-sdr-1337 expired 2016-01-31 59 | ``` 60 | 61 | ## What's more? 62 | 63 | You can discover all OVH possibilities by using API console to show all available endpoints: [https://api.ovh.com/console](https://api.ovh.com/console) 64 | 65 | -------------------------------------------------------------------------------- /examples/serviceList/serviceList.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from tabulate import tabulate 4 | 5 | import ovh 6 | 7 | # Services type desired to mine. To speed up the script, delete service type you don't use! 8 | service_types = [ 9 | "allDom", 10 | "cdn/dedicated", 11 | "cdn/website", 12 | "cdn/webstorage", 13 | "cloud/project", 14 | "cluster/hadoop", 15 | "dedicated/housing", 16 | "dedicated/nas", 17 | "dedicated/nasha", 18 | "dedicated/server", 19 | "dedicatedCloud", 20 | "domain/zone", 21 | "email/domain", 22 | "email/exchange", 23 | "freefax", 24 | "hosting/privateDatabase", 25 | "hosting/web", 26 | "hosting/windows", 27 | "hpcspot", 28 | "license/cloudLinux", 29 | "license/cpanel", 30 | "license/directadmin", 31 | "license/office", 32 | "license/plesk", 33 | "license/sqlserver", 34 | "license/virtuozzo", 35 | "license/windows", 36 | "license/worklight", 37 | "overTheBox", 38 | "pack/xdsl", 39 | "partner", 40 | "router", 41 | "sms", 42 | "telephony", 43 | "telephony/spare", 44 | "veeamCloudConnect", 45 | "vps", 46 | "xdsl", 47 | "xdsl/spare", 48 | ] 49 | 50 | # Create a client using ovh.conf 51 | client = ovh.Client() 52 | 53 | services_will_expired = [] 54 | 55 | # Check all OVH product (service type) 56 | for service_type in service_types: 57 | service_list = client.get("/%s" % service_type) 58 | 59 | # If we found you have this one or more of this product, we get these information 60 | for service in service_list: 61 | service_infos = client.get("/%s/%s/serviceInfos" % (service_type, service)) 62 | service_expiration_date = datetime.datetime.strptime(service_infos["expiration"], "%Y-%m-%d") 63 | services_will_expired.append([service_type, service, service_infos["status"], service_infos["expiration"]]) 64 | 65 | # At the end, we show service expired or that will expire (in a table with tabulate) 66 | print(tabulate(services_will_expired, headers=["Type", "ID", "status", "expiration date"])) 67 | -------------------------------------------------------------------------------- /ovh/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | # flake8: noqa 28 | from .client import Client 29 | from .consumer_key import API_READ_ONLY, API_READ_WRITE, API_READ_WRITE_SAFE, ConsumerKeyRequest 30 | from .exceptions import ( 31 | APIError, 32 | BadParametersError, 33 | Forbidden, 34 | HTTPError, 35 | InvalidCredential, 36 | InvalidKey, 37 | InvalidRegion, 38 | InvalidResponse, 39 | NetworkError, 40 | NotCredential, 41 | NotGrantedCall, 42 | ReadOnlyError, 43 | ResourceConflictError, 44 | ResourceNotFoundError, 45 | ) 46 | -------------------------------------------------------------------------------- /ovh/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | This module provides a simple python wrapper over the OVH REST API. 29 | It handles requesting credential, signing queries... 30 | 31 | - To get your API keys: https://eu.api.ovh.com/createApp/ 32 | - To get started with API: 33 | https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042784 34 | """ 35 | 36 | import hashlib 37 | import json 38 | import keyword 39 | import time 40 | from urllib.parse import urlencode 41 | 42 | from requests import Session 43 | from requests.exceptions import RequestException 44 | 45 | from . import config 46 | from .consumer_key import ConsumerKeyRequest 47 | from .exceptions import ( 48 | APIError, 49 | BadParametersError, 50 | Forbidden, 51 | HTTPError, 52 | InvalidConfiguration, 53 | InvalidCredential, 54 | InvalidKey, 55 | InvalidRegion, 56 | InvalidResponse, 57 | NetworkError, 58 | NotCredential, 59 | NotGrantedCall, 60 | ResourceConflictError, 61 | ResourceExpiredError, 62 | ResourceNotFoundError, 63 | ) 64 | from .oauth2 import OAuth2 65 | 66 | # Mapping between OVH API region names and corresponding endpoints 67 | ENDPOINTS = { 68 | "ovh-eu": "https://eu.api.ovh.com/1.0", 69 | "ovh-us": "https://api.us.ovhcloud.com/1.0", 70 | "ovh-ca": "https://ca.api.ovh.com/1.0", 71 | "kimsufi-eu": "https://eu.api.kimsufi.com/1.0", 72 | "kimsufi-ca": "https://ca.api.kimsufi.com/1.0", 73 | "soyoustart-eu": "https://eu.api.soyoustart.com/1.0", 74 | "soyoustart-ca": "https://ca.api.soyoustart.com/1.0", 75 | } 76 | 77 | # Default timeout for each request. 180 seconds connect, 180 seconds read. 78 | TIMEOUT = 180 79 | 80 | # OAuth2 token provider URLs 81 | OAUTH2_TOKEN_URLS = { 82 | "ovh-eu": "https://www.ovh.com/auth/oauth2/token", 83 | "ovh-ca": "https://ca.ovh.com/auth/oauth2/token", 84 | "ovh-us": "https://us.ovhcloud.com/auth/oauth2/token", 85 | } 86 | 87 | 88 | class Client: 89 | """ 90 | Low level OVH Client. It abstracts all the authentication and request 91 | signing logic along with some nice tools helping with key generation. 92 | 93 | All low level request logic including signing and error handling takes place 94 | in :py:func:`Client.call` function. Convenient wrappers 95 | :py:func:`Client.get` :py:func:`Client.post`, :py:func:`Client.put`, 96 | :py:func:`Client.delete` should be used instead. :py:func:`Client.post`, 97 | :py:func:`Client.put` both accept arbitrary list of keyword arguments 98 | mapped to ``data`` param of :py:func:`Client.call`. 99 | 100 | Example usage: 101 | 102 | .. code:: python 103 | 104 | from ovh import Client, APIError 105 | 106 | REGION = 'ovh-eu' 107 | APP_KEY="" 108 | APP_SECRET="" 109 | CONSUMER_KEY="" 110 | 111 | client = Client(REGION, APP_KEY, APP_SECRET, CONSUMER_KEY) 112 | 113 | try: 114 | print(client.get('/me')) 115 | except APIError as e: 116 | print("Ooops, failed to get my info:", e.msg) 117 | 118 | """ 119 | 120 | def __init__( 121 | self, 122 | endpoint=None, 123 | application_key=None, 124 | application_secret=None, 125 | consumer_key=None, 126 | timeout=TIMEOUT, 127 | config_file=None, 128 | client_id=None, 129 | client_secret=None, 130 | ): 131 | """ 132 | Creates a new Client. No credential check is done at this point. 133 | 134 | When using OAuth2 authentication, ``client_id`` and ``client_secret`` 135 | will be used to initiate a Client Credential OAuth2 flow. 136 | 137 | When using the OVHcloud authentication method, the ``application_key`` 138 | identifies your application while ``application_secret`` authenticates 139 | it. On the other hand, the ``consumer_key`` uniquely identifies your 140 | application's end user without requiring his personal password. 141 | 142 | If any of ``endpoint``, ``application_key``, ``application_secret``, 143 | ``consumer_key``, ``client_id`` or ``client_secret`` is not provided, 144 | this client will attempt to locate from them from environment, 145 | ``~/.ovh.cfg`` or ``/etc/ovh.cfg``. 146 | 147 | See :py:mod:`ovh.config` for more information on supported 148 | configuration mechanisms. 149 | 150 | ``timeout`` can either be a float or a tuple. If it is a float it 151 | sets the same timeout for both connection and read. If it is a tuple 152 | connection and read timeout will be set independently. To use the 153 | latter approach you need at least requests v2.4.0. Default value is 154 | 180 seconds for connection and 180 seconds for read. 155 | 156 | :param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS`` 157 | :param str application_key: Application key as provided by OVHcloud 158 | :param str application_secret: Application secret key as provided by OVHcloud 159 | :param str consumer_key: uniquely identifies 160 | :param str client_id: OAuth2 client ID 161 | :param str client_secret: OAuth2 client secret 162 | :param tuple timeout: Connection and read timeout for each request 163 | :param float timeout: Same timeout for both connection and read 164 | :raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``. 165 | """ 166 | 167 | configuration = config.ConfigurationManager() 168 | 169 | # Load a custom config file if requested 170 | if config_file is not None: 171 | configuration.read(config_file) 172 | 173 | # load endpoint 174 | if endpoint is None: 175 | endpoint = configuration.get("default", "endpoint") 176 | 177 | try: 178 | self._endpoint = ENDPOINTS[endpoint] 179 | except KeyError: 180 | raise InvalidRegion("Unknown endpoint %s. Valid endpoints: %s", endpoint, ENDPOINTS.keys()) 181 | 182 | # load keys 183 | if application_key is None: 184 | application_key = configuration.get(endpoint, "application_key") 185 | self._application_key = application_key 186 | 187 | if application_secret is None: 188 | application_secret = configuration.get(endpoint, "application_secret") 189 | self._application_secret = application_secret 190 | 191 | if consumer_key is None: 192 | consumer_key = configuration.get(endpoint, "consumer_key") 193 | self._consumer_key = consumer_key 194 | 195 | # load OAuth2 data 196 | if client_id is None: 197 | client_id = configuration.get(endpoint, "client_id") 198 | self._client_id = client_id 199 | 200 | if client_secret is None: 201 | client_secret = configuration.get(endpoint, "client_secret") 202 | self._client_secret = client_secret 203 | 204 | # configuration validation 205 | if bool(self._client_id) is not bool(self._client_secret): 206 | raise InvalidConfiguration("Invalid OAuth2 config, both client_id and client_secret must be given") 207 | 208 | if bool(self._application_key) is not bool(self._application_secret): 209 | raise InvalidConfiguration( 210 | "Invalid authentication config, both application_key and application_secret must be given" 211 | ) 212 | 213 | if self._client_id is not None and self._application_key is not None: 214 | raise InvalidConfiguration( 215 | "Can't use both application_key/application_secret and OAuth2 client_id/client_secret" 216 | ) 217 | if self._client_id is None and self._application_key is None: 218 | raise InvalidConfiguration( 219 | "Missing authentication information, you need to provide at least an application_key/application_secret" 220 | " or a client_id/client_secret" 221 | ) 222 | if self._client_id and endpoint not in OAUTH2_TOKEN_URLS: 223 | raise InvalidConfiguration( 224 | "OAuth2 authentication is not compatible with endpoint " 225 | + endpoint 226 | + " (it can only be used with ovh-eu, ovh-ca and ovh-us)" 227 | ) 228 | 229 | # when in OAuth2 mode, instantiate the oauthlib client 230 | if self._client_id: 231 | self._oauth2 = OAuth2( 232 | client_id=self._client_id, 233 | client_secret=self._client_secret, 234 | token_url=OAUTH2_TOKEN_URLS[endpoint], 235 | ) 236 | else: 237 | self._oauth2 = None 238 | 239 | # lazy load time delta 240 | self._time_delta = None 241 | 242 | # use a requests session to reuse HTTPS connections between requests 243 | self._session = Session() 244 | 245 | # Override default timeout 246 | self._timeout = timeout 247 | 248 | # high level API 249 | 250 | @property 251 | def time_delta(self): 252 | """ 253 | Request signatures are valid only for a short amount of time to mitigate 254 | risk of attack replay scenarii which requires to use a common time 255 | reference. This function queries endpoint's time and computes the delta. 256 | This entrypoint does not require authentication. 257 | 258 | This method is *lazy*. It will only load it once even though it is used 259 | for each request. 260 | 261 | .. note:: You should not need to use this property directly 262 | 263 | :returns: time distance between local and server time in seconds. 264 | :rtype: int 265 | """ 266 | if self._time_delta is None: 267 | server_time = self.get("/auth/time", _need_auth=False) 268 | self._time_delta = server_time - int(time.time()) 269 | return self._time_delta 270 | 271 | def new_consumer_key_request(self): 272 | """ 273 | Create a new consumer key request. This is the recommended way to create 274 | a new consumer key request. 275 | 276 | Full example: 277 | 278 | >>> import ovh 279 | >>> client = ovh.Client("ovh-eu") 280 | >>> ck = client.new_consumer_key_request() 281 | >>> ck.add_rules(ovh.API_READ_ONLY, "/me") 282 | >>> ck.add_recursive_rules(ovh.API_READ_WRITE, "/sms") 283 | >>> ck.request() 284 | { 285 | 'state': 'pendingValidation', 286 | 'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i', 287 | 'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0' 288 | } 289 | """ # noqa:E501 290 | return ConsumerKeyRequest(self) 291 | 292 | def request_consumerkey(self, access_rules, redirect_url=None, allowedIPs=None): 293 | """ 294 | Create a new "consumer key" identifying this application's end user. API 295 | will return a ``consumerKey`` and a ``validationUrl``. The end user must 296 | visit the ``validationUrl``, authenticate and validate the requested 297 | ``access_rules`` to link his account to the ``consumerKey``. Once this 298 | is done, he may optionally be redirected to ``redirect_url`` and the 299 | application can start using the ``consumerKey``. If adding an ``allowedIPs`` 300 | parameter, the generated credentials will only be usable from these IPs. 301 | 302 | The new ``consumerKey`` is automatically loaded into 303 | ``self._consumer_key`` and is ready to used as soon as validated. 304 | 305 | As signing requires a valid ``consumerKey``, the method does not require 306 | authentication, only a valid ``applicationKey`` 307 | 308 | ``access_rules`` is a list of the form: 309 | 310 | .. code:: python 311 | 312 | # Grant full, unrestricted API access 313 | access_rules = [ 314 | {'method': 'GET', 'path': '/*'}, 315 | {'method': 'POST', 'path': '/*'}, 316 | {'method': 'PUT', 'path': '/*'}, 317 | {'method': 'DELETE', 'path': '/*'} 318 | ] 319 | 320 | To request a new consumer key, you may use a code like: 321 | 322 | .. code:: python 323 | 324 | try: 325 | input = raw_input 326 | except NameError: 327 | pass 328 | 329 | # Request RO, /me API access 330 | access_rules = [ 331 | {'method': 'GET', 'path': '/me'}, 332 | ] 333 | 334 | # Request token 335 | validation = client.request_consumerkey(access_rules, redirect_url="https://optional-redirect-url.example.org", allowedIPs=["127.0.0.1/32"]) 336 | 337 | print("Please visit", validation['validationUrl'], "to authenticate") 338 | input("and press Enter to continue...") 339 | 340 | # Print nice welcome message 341 | print("Welcome", client.get('/me')['firstname']) 342 | 343 | 344 | :param list access_rules: Mapping specifying requested privileges. 345 | :param str redirect_url: Where to redirect end user upon validation (optional). 346 | :param list allowedIPs: CIDRs that will be allowed to use these credentials (optional). 347 | :raises APIError: When ``self.call`` fails. 348 | :returns: dict with ``consumerKey`` and ``validationUrl`` keys 349 | :rtype: dict 350 | """ # noqa:E501 351 | res = self.post( 352 | "/auth/credential", 353 | _need_auth=False, 354 | accessRules=access_rules, 355 | redirection=redirect_url, 356 | allowedIPs=allowedIPs, 357 | ) 358 | self._consumer_key = res["consumerKey"] 359 | return res 360 | 361 | # API shortcuts 362 | 363 | def _canonicalize_kwargs(self, kwargs): 364 | """ 365 | If an API needs an argument colliding with a Python reserved keyword, it 366 | can be prefixed with an underscore. For example, ``from`` argument of 367 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from`` 368 | 369 | :param dict kwargs: input kwargs 370 | :return dict: filtered kawrgs 371 | """ 372 | arguments = {} 373 | 374 | for k, v in kwargs.items(): 375 | if k[0] == "_" and k[1:] in keyword.kwlist: 376 | k = k[1:] 377 | arguments[k] = v 378 | 379 | return arguments 380 | 381 | def _prepare_query_string(self, kwargs): 382 | """ 383 | Boolean needs to be send as lowercase 'false' or 'true' in querystring. 384 | This function prepares arguments for querystring and encodes them. 385 | 386 | :param dict kwargs: input kwargs 387 | :return string: prepared querystring 388 | """ 389 | arguments = {} 390 | 391 | for k, v in kwargs.items(): 392 | if isinstance(v, bool): 393 | v = str(v).lower() 394 | elif v is None: 395 | v = "null" 396 | arguments[k] = v 397 | 398 | return urlencode(arguments) 399 | 400 | def get(self, _target, _need_auth=True, **kwargs): 401 | """ 402 | 'GET' :py:func:`Client.call` wrapper. 403 | 404 | Query string parameters can be set either directly in ``_target`` or as 405 | keyword arguments. If an argument collides with a Python reserved 406 | keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``. 407 | 408 | :param string _target: API method to call 409 | :param string _need_auth: If True, send authentication headers. This is 410 | the default 411 | """ 412 | if kwargs: 413 | kwargs = self._canonicalize_kwargs(kwargs) 414 | query_string = self._prepare_query_string(kwargs) 415 | if query_string != "": 416 | if "?" in _target: 417 | _target = "%s&%s" % (_target, query_string) 418 | else: 419 | _target = "%s?%s" % (_target, query_string) 420 | 421 | return self.call("GET", _target, None, _need_auth) 422 | 423 | def put(self, _target, _need_auth=True, **kwargs): 424 | """ 425 | 'PUT' :py:func:`Client.call` wrapper 426 | 427 | Body parameters can be set either directly in ``_target`` or as keyword 428 | arguments. If an argument collides with a Python reserved keyword, 429 | prefix it with a '_'. For instance, ``from`` becomes ``_from``. 430 | 431 | :param string _target: API method to call 432 | :param string _need_auth: If True, send authentication headers. This is 433 | the default 434 | """ 435 | kwargs = self._canonicalize_kwargs(kwargs) 436 | if not kwargs: 437 | kwargs = None 438 | return self.call("PUT", _target, kwargs, _need_auth) 439 | 440 | def post(self, _target, _need_auth=True, **kwargs): 441 | """ 442 | 'POST' :py:func:`Client.call` wrapper 443 | 444 | Body parameters can be set either directly in ``_target`` or as keyword 445 | arguments. If an argument collides with a Python reserved keyword, 446 | prefix it with a '_'. For instance, ``from`` becomes ``_from``. 447 | 448 | :param string _target: API method to call 449 | :param string _need_auth: If True, send authentication headers. This is 450 | the default 451 | """ 452 | kwargs = self._canonicalize_kwargs(kwargs) 453 | if not kwargs: 454 | kwargs = None 455 | return self.call("POST", _target, kwargs, _need_auth) 456 | 457 | def delete(self, _target, _need_auth=True, **kwargs): 458 | """ 459 | 'DELETE' :py:func:`Client.call` wrapper 460 | 461 | Query string parameters can be set either directly in ``_target`` or as 462 | keyword arguments. If an argument collides with a Python reserved 463 | keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``. 464 | 465 | :param string _target: API method to call 466 | :param string _need_auth: If True, send authentication headers. This is 467 | the default 468 | """ 469 | if kwargs: 470 | kwargs = self._canonicalize_kwargs(kwargs) 471 | query_string = self._prepare_query_string(kwargs) 472 | if query_string != "": 473 | if "?" in _target: 474 | _target = "%s&%s" % (_target, query_string) 475 | else: 476 | _target = "%s?%s" % (_target, query_string) 477 | 478 | return self.call("DELETE", _target, None, _need_auth) 479 | 480 | # low level helpers 481 | 482 | def call(self, method, path, data=None, need_auth=True): 483 | """ 484 | Low level call helper. If ``consumer_key`` is not ``None``, inject 485 | authentication headers and sign the request. 486 | 487 | Request signature is a sha1 hash on following fields, joined by '+' 488 | - application_secret 489 | - consumer_key 490 | - METHOD 491 | - full request url 492 | - body 493 | - server current time (takes time delta into account) 494 | 495 | :param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE 496 | :param str path: api entrypoint to call, relative to endpoint base path 497 | :param data: any json serializable data to send as request's body 498 | :param boolean need_auth: if False, bypass signature 499 | :raises HTTPError: when underlying request failed for network reason 500 | :raises InvalidResponse: when API response could not be decoded 501 | """ 502 | # attempt request 503 | try: 504 | result = self.raw_call(method=method, path=path, data=data, need_auth=need_auth) 505 | except RequestException as error: 506 | raise HTTPError("Low HTTP request failed error", error) 507 | 508 | status = result.status_code 509 | 510 | # attempt to decode and return the response 511 | try: 512 | if status != 204: 513 | json_result = result.json() 514 | else: 515 | json_result = None 516 | except ValueError as error: 517 | raise InvalidResponse("Failed to decode API response", error) 518 | 519 | # error check 520 | if status >= 100 and status < 300: 521 | return json_result 522 | elif status == 403 and json_result.get("errorCode") == "NOT_GRANTED_CALL": 523 | raise NotGrantedCall(json_result.get("message"), response=result) 524 | elif status == 403 and json_result.get("errorCode") == "NOT_CREDENTIAL": 525 | raise NotCredential(json_result.get("message"), response=result) 526 | elif status == 403 and json_result.get("errorCode") == "INVALID_KEY": 527 | raise InvalidKey(json_result.get("message"), response=result) 528 | elif status == 403 and json_result.get("errorCode") == "INVALID_CREDENTIAL": 529 | raise InvalidCredential(json_result.get("message"), response=result) 530 | elif status == 403 and json_result.get("errorCode") == "FORBIDDEN": 531 | raise Forbidden(json_result.get("message"), response=result) 532 | elif status == 404: 533 | raise ResourceNotFoundError(json_result.get("message"), response=result) 534 | elif status == 400: 535 | raise BadParametersError(json_result.get("message"), response=result) 536 | elif status == 409: 537 | raise ResourceConflictError(json_result.get("message"), response=result) 538 | elif status == 460: 539 | raise ResourceExpiredError(json_result.get("message"), response=result) 540 | elif status == 0: 541 | raise NetworkError() 542 | else: 543 | raise APIError(json_result.get("message"), response=result) 544 | 545 | def _get_target(self, path): 546 | """ 547 | _get_target returns the URL to target given an endpoint and a path. 548 | If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint. 549 | 550 | :param str path: path to use prefix from 551 | :returns: target with one of /1.0 and /v1|2 path segment 552 | :rtype: str 553 | """ 554 | endpoint = self._endpoint 555 | if endpoint.endswith("/1.0") and path.startswith(("/v1", "/v2")): 556 | endpoint = endpoint[:-4] 557 | return endpoint + path 558 | 559 | def raw_call(self, method, path, data=None, need_auth=True, headers=None): 560 | """ 561 | Lowest level call helper. If ``consumer_key`` is not ``None``, inject 562 | authentication headers and sign the request. 563 | Will return ``requests.Response`` object or let any 564 | ``requests`` exception pass through. 565 | 566 | Request signature is a sha1 hash on following fields, joined by '+' 567 | - application_secret 568 | - consumer_key 569 | - METHOD 570 | - full request url 571 | - body 572 | - server current time (takes time delta into account) 573 | 574 | :param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE 575 | :param str path: api entrypoint to call, relative to endpoint base path 576 | :param data: any json serializable data to send as request's body 577 | :param boolean need_auth: if False, bypass signature 578 | :param dict headers: A dict containing the headers that should be sent to 579 | the OVH API. ``raw_call`` will override the 580 | OVH API authentication headers, as well as 581 | the Content-Type header. 582 | """ 583 | body = "" 584 | target = self._get_target(path) 585 | 586 | if headers is None: 587 | headers = {} 588 | 589 | # include payload 590 | if data is not None: 591 | headers["Content-type"] = "application/json" 592 | body = json.dumps(data, separators=(",", ":")) # Separators to prevent adding useless spaces 593 | 594 | # sign request. Never sign 'time' or will recurse infinitely 595 | if need_auth: 596 | if self._oauth2: 597 | return self._oauth2.session.request(method, target, headers=headers, data=body, timeout=self._timeout) 598 | 599 | if not self._application_secret: 600 | raise InvalidKey("Invalid ApplicationSecret '%s'" % self._application_secret) 601 | 602 | if not self._consumer_key: 603 | raise InvalidKey("Invalid ConsumerKey '%s'" % self._consumer_key) 604 | 605 | now = str(int(time.time()) + self.time_delta) 606 | signature = hashlib.sha1() 607 | signature.update( 608 | "+".join([self._application_secret, self._consumer_key, method.upper(), target, body, now]).encode( 609 | "utf-8" 610 | ) 611 | ) 612 | 613 | headers["X-Ovh-Consumer"] = self._consumer_key 614 | headers["X-Ovh-Timestamp"] = now 615 | headers["X-Ovh-Signature"] = "$1$" + signature.hexdigest() 616 | 617 | headers["X-Ovh-Application"] = self._application_key 618 | return self._session.request(method, target, headers=headers, data=body, timeout=self._timeout) 619 | -------------------------------------------------------------------------------- /ovh/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | The straightforward way to use OVH's API keys is to embed them directly in the 29 | application code. While this is very convenient, it lacks of elegance and 30 | flexibility. 31 | 32 | Alternatively it is suggested to use configuration files or environment 33 | variables so that the same code may run seamlessly in multiple environments. 34 | Production and development for instance. 35 | 36 | This wrapper will first look for direct instantiation parameters then 37 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and 38 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not 39 | provided, it will look for a configuration file of the form: 40 | 41 | .. code:: ini 42 | 43 | [default] 44 | ; general configuration: default endpoint 45 | endpoint=ovh-eu 46 | 47 | [ovh-eu] 48 | ; configuration specific to 'ovh-eu' endpoint 49 | application_key=my_app_key 50 | application_secret=my_application_secret 51 | consumer_key=my_consumer_key 52 | client_id=my_client_id 53 | client_secret=my_client_secret 54 | 55 | The client will successively attempt to locate this configuration file in 56 | 57 | 1. Current working directory: ``./ovh.conf`` 58 | 2. Current user's home directory ``~/.ovh.conf`` 59 | 3. System wide configuration ``/etc/ovh.conf`` 60 | 61 | This lookup mechanism makes it easy to overload credentials for a specific 62 | project or user. 63 | """ 64 | 65 | from configparser import NoOptionError, NoSectionError, RawConfigParser 66 | import os 67 | 68 | __all__ = ["config"] 69 | 70 | #: Locations where to look for configuration file by *increasing* priority 71 | CONFIG_PATH = [ 72 | "/etc/ovh.conf", 73 | os.path.expanduser("~/.ovh.conf"), 74 | os.path.realpath("./ovh.conf"), 75 | ] 76 | 77 | 78 | class ConfigurationManager: 79 | """ 80 | Application wide configuration manager 81 | """ 82 | 83 | def __init__(self): 84 | """ 85 | Create a config parser and load config from environment. 86 | """ 87 | # create config parser 88 | self.config = RawConfigParser() 89 | self.config.read(CONFIG_PATH) 90 | 91 | def get(self, section, name): 92 | """ 93 | Load parameter ``name`` from configuration, respecting priority order. 94 | Most of the time, ``section`` will correspond to the current api 95 | ``endpoint``. ``default`` section only contains ``endpoint`` and general 96 | configuration. 97 | 98 | :param str section: configuration section or region name. Ignored when 99 | looking in environment 100 | :param str name: configuration parameter to lookup 101 | """ 102 | # 1/ try env 103 | try: 104 | return os.environ["OVH_" + name.upper()] 105 | except KeyError: 106 | pass 107 | 108 | # 2/ try from specified section/endpoint 109 | try: 110 | return self.config.get(section, name) 111 | except (NoSectionError, NoOptionError): 112 | pass 113 | 114 | # not found, sorry 115 | return None 116 | 117 | def read(self, config_file): 118 | # Read an other config file 119 | self.config.read(config_file) 120 | 121 | 122 | #: System wide instance :py:class:`ConfigurationManager` instance 123 | config = ConfigurationManager() 124 | -------------------------------------------------------------------------------- /ovh/consumer_key.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | """ 29 | This module provides a consumer key creation helper. Consumer keys are linked 30 | with permissions defining which endpoint they are allowed to call. Just like 31 | a physical key can unlock some doors but not others. 32 | 33 | OVH API consumer keys authorization is pattern based. This makes it extremely 34 | powerful and flexible as it may apply on only a very specific subset of the API 35 | but it's also trickier to get right on simple scenarios. 36 | 37 | Hence this module 38 | """ 39 | 40 | # Common authorization patterns 41 | API_READ_ONLY = ["GET"] 42 | API_READ_WRITE = ["GET", "POST", "PUT", "DELETE"] 43 | API_READ_WRITE_SAFE = ["GET", "POST", "PUT"] 44 | 45 | 46 | class ConsumerKeyRequest(object): 47 | """ 48 | ConsumerKey request. The generated consumer key will be linked to the 49 | client's ``application_key``. When performing the request, the 50 | ``consumer_key`` will automatically be registered in the client. 51 | 52 | It is recommended to save the generated key as soon as it validated to avoid 53 | requesting a new one on each API access. 54 | """ 55 | 56 | def __init__(self, client): 57 | """ 58 | Create a new consumer key helper on API ``client``. The keys will be 59 | tied to the ``application_key`` defined in the client. 60 | """ 61 | self._client = client 62 | self._access_rules = [] 63 | 64 | def request(self, redirect_url=None, allowedIPs=None): 65 | """ 66 | Create the consumer key with the configures autorizations. The user will 67 | need to validate it before it can be used with the API 68 | 69 | >>> ck.request() 70 | { 71 | 'state': 'pendingValidation', 72 | 'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i', 73 | 'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0' 74 | } 75 | """ # noqa: E501 76 | return self._client.request_consumerkey(self._access_rules, redirect_url, allowedIPs) 77 | 78 | def add_rule(self, method, path): 79 | """ 80 | Add a new rule to the request. Will grant the ``(method, path)`` tuple. 81 | Path can be any API route pattern like ``/sms/*`` or ``/me``. For example, 82 | to grant RO access on personal data: 83 | 84 | >>> ck.add_rule("GET", "/me") 85 | """ 86 | self._access_rules.append({"method": method.upper(), "path": path}) 87 | 88 | def add_rules(self, methods, path): 89 | """ 90 | Add rules for ``path`` pattern, for each methods in ``methods``. This is 91 | a convenient helper over ``add_rule``. For example, this could be used 92 | to grant all access on the API at once: 93 | 94 | >>> ck.add_rules(["GET", "POST", "PUT", "DELETE"], "/*") 95 | """ 96 | for method in methods: 97 | self.add_rule(method, path) 98 | 99 | def add_recursive_rules(self, methods, path): 100 | """ 101 | Use this method to grant access on a full API tree. This is the 102 | recommended way to grant access in the API. It will take care of granted 103 | the root call *AND* sub-calls for you. Which is commonly forgotten... 104 | For example, to grant a full access on ``/sms``: 105 | 106 | >>> ck.add_recursive_rules(["GET", "POST", "PUT", "DELETE"], "/sms") 107 | """ 108 | path = path.rstrip("*/ ") 109 | if path: 110 | self.add_rules(methods, path) 111 | self.add_rules(methods, path + "/*") 112 | -------------------------------------------------------------------------------- /ovh/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | All exceptions used in OVH SDK derives from `APIError` 29 | """ 30 | 31 | 32 | class APIError(Exception): 33 | """Base OVH API exception, all specific exceptions inherits from it.""" 34 | 35 | def __init__(self, *args, **kwargs): 36 | self.response = kwargs.pop("response", None) 37 | if self.response is not None: 38 | self.query_id = self.response.headers.get("X-OVH-QUERYID") 39 | else: 40 | self.query_id = None 41 | super(APIError, self).__init__(*args, **kwargs) 42 | 43 | def __str__(self): 44 | if self.query_id: # pragma: no cover 45 | return "{} \nOVH-Query-ID: {}".format(super(APIError, self).__str__(), self.query_id) 46 | else: # pragma: no cover 47 | return super(APIError, self).__str__() 48 | 49 | 50 | class HTTPError(APIError): 51 | """Raised when the request fails at a low level (DNS, network, ...)""" 52 | 53 | 54 | class InvalidKey(APIError): 55 | """Raised when trying to sign request with invalid key""" 56 | 57 | 58 | class InvalidCredential(APIError): 59 | """Raised when trying to sign request with invalid consumer key""" 60 | 61 | 62 | class InvalidConfiguration(APIError): 63 | """Raised when trying to load an invalid configuration into a client""" 64 | 65 | 66 | class InvalidResponse(APIError): 67 | """Raised when api response is not valid json""" 68 | 69 | 70 | class InvalidRegion(APIError): 71 | """Raised when region is not in `REGIONS`.""" 72 | 73 | 74 | class ReadOnlyError(APIError): 75 | """Raised when attempting to modify readonly data.""" 76 | 77 | 78 | class ResourceNotFoundError(APIError): 79 | """Raised when requested resource does not exist.""" 80 | 81 | 82 | class BadParametersError(APIError): 83 | """Raised when request contains bad parameters.""" 84 | 85 | 86 | class ResourceConflictError(APIError): 87 | """Raised when trying to create an already existing resource.""" 88 | 89 | 90 | class NetworkError(APIError): 91 | """Raised when there is an error from network layer.""" 92 | 93 | 94 | class NotGrantedCall(APIError): 95 | """Raised when there is an error from network layer.""" 96 | 97 | 98 | class NotCredential(APIError): 99 | """Raised when there is an error from network layer.""" 100 | 101 | 102 | class Forbidden(APIError): 103 | """Raised when there is an error from network layer.""" 104 | 105 | 106 | class ResourceExpiredError(APIError): 107 | """Raised when requested resource expired.""" 108 | 109 | 110 | class OAuth2FailureError(APIError): 111 | """Raised when the OAuth2 workflow fails""" 112 | -------------------------------------------------------------------------------- /ovh/oauth2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | Thanks to https://github.com/requests/requests-oauthlib/issues/260 for the base used in this file. 29 | """ 30 | 31 | from oauthlib.oauth2 import BackendApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError 32 | from requests_oauthlib import OAuth2Session 33 | 34 | from .exceptions import OAuth2FailureError 35 | 36 | 37 | class RefreshOAuth2Session(OAuth2Session): 38 | _error = None 39 | 40 | def __init__(self, token_url, **kwargs): 41 | self.token_url = token_url 42 | super().__init__(**kwargs) 43 | 44 | # This hijacks the hook mechanism to save details about the last token creation failure. 45 | # For now, there is no easy other way to access to these details; 46 | # see https://github.com/requests/requests-oauthlib/pull/441 47 | self.register_compliance_hook("access_token_response", self.save_error) 48 | self.register_compliance_hook("refresh_token_response", self.save_error) 49 | 50 | # See __init__, used as compliance hooks 51 | def save_error(self, resp): 52 | if 200 <= resp.status_code <= 299: 53 | self._error = "Received invalid body: " + resp.text 54 | if resp.status_code >= 400: 55 | self._error = "Token creation failed with status_code={}, body={}".format(resp.status_code, resp.text) 56 | return resp 57 | 58 | # Wraps OAuth2Session.fetch_token to enrich returned exception messages, wrapped in an unique class 59 | def fetch_token(self, *args, **kwargs): 60 | try: 61 | return super().fetch_token(*args, **kwargs) 62 | except MissingTokenError as e: 63 | desc = "OAuth2 failure: " + e.description 64 | if self._error: 65 | desc += " " + self._error 66 | 67 | raise OAuth2FailureError(desc) from e 68 | except OAuth2Error as e: 69 | raise OAuth2FailureError("OAuth2 failure: " + str(e)) from e 70 | 71 | # Wraps OAuth2Session.request to handle TokenExpiredError by fetching a new token and retrying 72 | def request(self, *args, **kwargs): 73 | try: 74 | return super().request(*args, **kwargs) 75 | except TokenExpiredError: 76 | self.token = self.fetch_token(token_url=self.token_url, **self.auto_refresh_kwargs) 77 | self.token_updater(self.token) 78 | return super().request(*args, **kwargs) 79 | 80 | 81 | class OAuth2: 82 | _session = None 83 | _token = None 84 | 85 | def __init__(self, client_id, client_secret, token_url): 86 | self.client_id = client_id 87 | self.client_secret = client_secret 88 | self.token_url = token_url 89 | 90 | def token_updater(self, token): 91 | self._token = token 92 | 93 | @property 94 | def session(self): 95 | if self._session is None: 96 | self._session = RefreshOAuth2Session( 97 | token_url=self.token_url, 98 | client=BackendApplicationClient( 99 | client_id=self.client_id, 100 | scope=["all"], 101 | ), 102 | token=self.token, 103 | token_updater=self.token_updater, 104 | auto_refresh_kwargs={ 105 | "client_id": self.client_id, 106 | "client_secret": self.client_secret, 107 | }, 108 | ) 109 | return self._session 110 | 111 | @property 112 | def token(self): 113 | if self._token is None: 114 | self._token = RefreshOAuth2Session( 115 | token_url=self.token_url, 116 | client=BackendApplicationClient( 117 | client_id=self.client_id, 118 | scope=["all"], 119 | ), 120 | ).fetch_token( 121 | token_url=self.token_url, 122 | client_id=self.client_id, 123 | client_secret=self.client_secret, 124 | ) 125 | return self._token 126 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ['py310'] 4 | include=''' 5 | ^( 6 | \/[^\/]* 7 | | \/docs\/conf 8 | | \/examples\/.* 9 | | \/ovh\/.* 10 | | \/tests\/.* 11 | ).py$ 12 | ''' 13 | 14 | [tool.isort] 15 | profile = "black" 16 | line_length = 120 17 | multi_line_output = 3 18 | forced_separate = ["tests"] 19 | no_lines_before = "LOCALFOLDER" 20 | known_first_party = ["ovh"] 21 | force_sort_within_sections = true 22 | -------------------------------------------------------------------------------- /scripts/build-debian-package-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | exec docker run -it --rm --name python-ovh-debian-builder -v python-ovh-debian-builder-output:/output -v "${PWD}:/python-ovh:ro" debian:buster /python-ovh/scripts/build-debian-package-recipe.sh 5 | -------------------------------------------------------------------------------- /scripts/build-debian-package-recipe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir -p /home/pkg/ 5 | cp -r /python-ovh/ /home/pkg/src 6 | cd /home/pkg/src 7 | 8 | export DEBIAN_FRONTEND="noninteractive" 9 | apt-get update 10 | # Add basic packages 11 | apt-get install -y ca-certificates apt-transport-https 12 | 13 | # Build package tooling 14 | apt-get -yq install procps build-essential devscripts quilt debhelper 15 | apt-get -yq install dh-systemd 16 | 17 | 18 | DEBUILD_OPTIONS=--buildinfo-option=-O 19 | 20 | mkdir -p /home/pkg/src/ovh 21 | if [ ! -f /home/pkg/src/ovh/bbb ] && [ ! -f /home/pkg/src/ovh/build ]; then 22 | echo "INFO: BuildBot is creating an executable file in ovh/bbb" 23 | mkdir -p /home/pkg/src/ovh 24 | cat > /home/pkg/src/ovh/bbb << EOF 25 | set -e 26 | debuild $DEBUILD_OPTIONS -us -uc -b -j$(nproc) 27 | EOF 28 | 29 | cat /home/pkg/src/ovh/bbb 30 | chmod +x /home/pkg/src/ovh/bbb 31 | fi 32 | 33 | echo "BUILDBOT> Prepare the build process with Debian build dependencies (if debian/control file exists)" 34 | if [ -f /home/pkg/src/debian/control ]; then 35 | mk-build-deps -r -t "apt-get --no-install-recommends -y" -i /home/pkg/src/debian/control 36 | else 37 | echo "INFO: /home/pkg/src/debian/control is absent...skipping mk-build-deps" 38 | fi 39 | if [ -f /home/pkg/src/ovh/bbb ]; then 40 | echo "BUILDBOT> Starting the build process via /home/pkg/src/ovh/bbb" 41 | cd /home/pkg/src && ./ovh/bbb 42 | elif [ -f /home/pkg/src/ovh/build ]; then 43 | echo "BUILDBOT> Starting the build process via /home/pkg/src/ovh/build" 44 | cd /home/pkg/src && ./ovh/build 45 | fi 46 | 47 | echo "BUILDBOT> Moving output to the artifact directory" 48 | cd /home/pkg && find . -maxdepth 1 -type f -print -exec mv '{}' /output/ \; 49 | chown -R 1000:1000 /output/ 50 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Usage: ./scripts/bump-version.sh 4 | # 5 | 6 | set -e 7 | 8 | VERSION="$1" 9 | CURRENT_VERSION="$(git describe --tags --abbrev=0 | grep -o '[.0-9]*')" 10 | CURRENT_VERSION_EXP="$(echo $CURRENT_VERSION | sed 's/\./\\./g')" 11 | 12 | if ! [[ "${VERSION}" != "" && "${VERSION}" =~ ^[.0-9]*$ ]] 13 | then 14 | echo "Usage: ./scripts/bump-version.sh " >&2 15 | echo "Current version: ${CURRENT_VERSION}" 16 | exit 1 17 | fi 18 | 19 | # Move to project root 20 | cd "$(dirname "$0")"/.. 21 | 22 | # Edit version number in files 23 | grep -rlI "${CURRENT_VERSION_EXP}" | grep -vP '(^\.git|CHANGELOG\.md)' | xargs sed -i "s/${CURRENT_VERSION_EXP}/${VERSION}/g" 24 | 25 | # Prepare Changelog 26 | CHANGES=$(git log --oneline --no-merges v${CURRENT_VERSION}.. | sed 's/^[a-f0-9]*/ -/g' | sed ':a;N;$!ba;s/\n/\\n/g') 27 | 28 | if [ -z "${CHANGES}" ] 29 | then 30 | echo "Ooops, no changes detected since last version (${CURRENT_VERSION})" 31 | exit 1 32 | fi 33 | 34 | sed -i "4i## ${VERSION} ($(date --iso))\n${CHANGES}\n" CHANGELOG.md 35 | vim CHANGELOG.md 36 | 37 | # Upgrading debian/changelog 38 | dch --noquery --distribution trusty --newversion ${VERSION} "New upstream release v${VERSION}" 39 | awk "/## ${VERSION}/{f=1;next}/##/{f=0} f" CHANGELOG.md | sed 's/^\s*-\s*//' | while IFS= read -r line ; do 40 | dch --noquery --distribution trusty -a "$line" 41 | done 42 | 43 | # Commit and tag 44 | git commit -sam "[auto] bump version to v${VERSION}" 45 | git tag v${VERSION} 46 | 47 | echo "All done!" 48 | 49 | -------------------------------------------------------------------------------- /scripts/update-copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Usage: ./scripts/update-copyright.sh 4 | # 5 | 6 | PCRE_MATCH_COPYRIGHT="Copyright \(c\) 2013-[0-9]{4}, OVH SAS." 7 | PCRE_MATCH_DEBIAN="Copyright: [-0-9]* OVH SAS" 8 | YEAR=$(date +%Y) 9 | 10 | echo -n "Updating copyright headers to ${YEAR}... " 11 | grep -rPl "${PCRE_MATCH_COPYRIGHT}" | xargs sed -ri "s/${PCRE_MATCH_COPYRIGHT}/Copyright (c) 2013-${YEAR}, OVH SAS./g" 12 | grep -rPl "${PCRE_MATCH_DEBIAN}" | xargs sed -ri "s/${PCRE_MATCH_DEBIAN}/Copyright: 2013-${YEAR} OVH SAS/g" 13 | echo "[OK]" 14 | 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ovh 3 | description = "Official module to perform HTTP requests to the OVHcloud APIs" 4 | long_description = file: README.rst 5 | version = 1.2.0 6 | author = OVHcloud team - Romain Beuque 7 | author_email = api@ml.ovh.net 8 | url = https://api.ovh.com 9 | license = BSD 10 | license_file = LICENSE 11 | project_urls = 12 | Changelog = https://github.com/ovh/python-ovh/blob/master/CHANGELOG.md 13 | Repository = https://github.com/ovh/python-ovh.git 14 | Issues = https://github.com/ovh/python-ovh/issues 15 | keywords = ovh, sdk, rest, ovhcloud 16 | classifiers = 17 | License :: OSI Approved :: BSD License 18 | Development Status :: 5 - Production/Stable 19 | Intended Audience :: Developers 20 | Operating System :: OS Independent 21 | Programming Language :: Python 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3.12 29 | Topic :: Software Development :: Libraries :: Python Modules 30 | Topic :: System :: Archiving :: Packaging 31 | 32 | [options] 33 | packages = find: 34 | setup_requires = 35 | setuptools>=30.3.0 36 | # requests: we need ssl+pooling fix from https://docs.python-requests.org/en/latest/community/updates/#id40 37 | install_requires = 38 | requests>=2.31.0 39 | requests-oauthlib>=2.0.0 40 | include_package_data = True 41 | 42 | [options.packages.find] 43 | exclude = 44 | tests 45 | 46 | [options.extras_require] 47 | dev = 48 | Sphinx==1.2.2 49 | black 50 | coverage~=7.2.2 51 | flake8 52 | isort 53 | pytest~=7.2.2 54 | pytest-cov==4.0.0 55 | setuptools>=30.3.0 56 | wheel 57 | 58 | [bdist_wheel] 59 | universal = 1 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distribute_setup import use_setuptools 8 | 9 | use_setuptools() 10 | from setuptools import setup 11 | 12 | 13 | setup() 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /tests/data/invalid.ini: -------------------------------------------------------------------------------- 1 | [ovh 2 | consumer_key=local 3 | -------------------------------------------------------------------------------- /tests/data/localPartial.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | consumer_key=local 3 | -------------------------------------------------------------------------------- /tests/data/system.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | application_key=system 3 | application_secret=system 4 | consumer_key=system 5 | -------------------------------------------------------------------------------- /tests/data/user.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | application_key=user 3 | application_secret=user 4 | consumer_key=user 5 | -------------------------------------------------------------------------------- /tests/data/userPartial.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | application_secret=user 3 | consumer_key=user 4 | -------------------------------------------------------------------------------- /tests/data/user_both.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | application_key=user 3 | application_secret=user 4 | client_id=foo 5 | client_secret=bar -------------------------------------------------------------------------------- /tests/data/user_oauth2.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | client_id=foo 3 | client_secret=bar -------------------------------------------------------------------------------- /tests/data/user_oauth2_incompatible.ini: -------------------------------------------------------------------------------- 1 | [kimsufi-eu] 2 | client_id=foo 3 | client_secret=bar -------------------------------------------------------------------------------- /tests/data/user_oauth2_invalid.ini: -------------------------------------------------------------------------------- 1 | [ovh-eu] 2 | client_id=foo 3 | client_secret= -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import time 28 | from unittest import mock 29 | 30 | import pytest 31 | import requests 32 | 33 | from ovh.client import ENDPOINTS, Client 34 | from ovh.exceptions import ( 35 | APIError, 36 | BadParametersError, 37 | Forbidden, 38 | HTTPError, 39 | InvalidCredential, 40 | InvalidKey, 41 | InvalidResponse, 42 | NetworkError, 43 | NotCredential, 44 | NotGrantedCall, 45 | OAuth2FailureError, 46 | ResourceConflictError, 47 | ResourceExpiredError, 48 | ResourceNotFoundError, 49 | ) 50 | 51 | # Mock values 52 | MockApplicationKey = "TDPKJdwZwAQPwKX2" 53 | MockApplicationSecret = "9ufkBmLaTQ9nz5yMUlg79taH0GNnzDjk" 54 | MockConsumerKey = "5mBuy6SUQcRw2ZUxg0cG68BoDKpED4KY" 55 | MockTime = 1457018875 56 | 57 | 58 | class TestClient: 59 | @mock.patch("time.time", return_value=1457018875.467238) 60 | @mock.patch.object(Client, "call", return_value=1457018881) 61 | def test_time_delta(self, m_call, m_time): 62 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 63 | assert api._time_delta is None 64 | assert m_call.called is False 65 | assert m_time.called is False 66 | 67 | # nominal 68 | assert api.time_delta == 6 69 | assert m_call.called is True 70 | assert m_time.called is True 71 | assert api._time_delta == 6 72 | assert m_call.call_args_list == [mock.call("GET", "/auth/time", None, False)] 73 | 74 | # ensure cache 75 | m_call.reset_mock() 76 | assert api.time_delta == 6 77 | assert m_call.called is False 78 | 79 | @mock.patch.object(Client, "call", return_value={"consumerKey": "CK"}) 80 | def test_request_consumerkey(self, m_call): 81 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 82 | ret = api.request_consumerkey([{"method": "GET", "path": "/"}], "https://example.com", ["127.0.0.1/32"]) 83 | 84 | m_call.assert_called_once_with( 85 | "POST", 86 | "/auth/credential", 87 | { 88 | "redirection": "https://example.com", 89 | "accessRules": [{"method": "GET", "path": "/"}], 90 | "allowedIPs": ["127.0.0.1/32"], 91 | }, 92 | False, 93 | ) 94 | assert ret == {"consumerKey": "CK"} 95 | 96 | def test_new_consumer_key_request(self): 97 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 98 | ck = api.new_consumer_key_request() 99 | assert ck._client == api 100 | 101 | # test wrappers 102 | 103 | def test__canonicalize_kwargs(self): 104 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 105 | assert api._canonicalize_kwargs({}) == {} 106 | assert api._canonicalize_kwargs({"from": "value"}) == {"from": "value"} 107 | assert api._canonicalize_kwargs({"_to": "value"}) == {"_to": "value"} 108 | assert api._canonicalize_kwargs({"_from": "value"}) == {"from": "value"} 109 | 110 | @mock.patch.object(Client, "call") 111 | def test_query_string(self, m_call): 112 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 113 | 114 | for method, call in (("GET", api.get), ("DELETE", api.delete)): 115 | m_call.reset_mock() 116 | 117 | assert call("https://eu.api.ovh.com/") == m_call.return_value 118 | assert call("https://eu.api.ovh.com/", param="test") == m_call.return_value 119 | assert call("https://eu.api.ovh.com/?query=string", param="test") == m_call.return_value 120 | assert call("https://eu.api.ovh.com/?query=string", checkbox=True) == m_call.return_value 121 | assert call("https://eu.api.ovh.com/", _from="start", to="end") == m_call.return_value 122 | 123 | assert m_call.call_args_list == [ 124 | mock.call(method, "https://eu.api.ovh.com/", None, True), 125 | mock.call(method, "https://eu.api.ovh.com/?param=test", None, True), 126 | mock.call(method, "https://eu.api.ovh.com/?query=string¶m=test", None, True), 127 | mock.call(method, "https://eu.api.ovh.com/?query=string&checkbox=true", None, True), 128 | mock.call(method, "https://eu.api.ovh.com/?from=start&to=end", None, True), 129 | ] 130 | 131 | @mock.patch.object(Client, "call") 132 | def test_body(self, m_call): 133 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 134 | 135 | for method, call in (("POST", api.post), ("PUT", api.put)): 136 | m_call.reset_mock() 137 | 138 | assert call("https://eu.api.ovh.com/") == m_call.return_value 139 | assert call("https://eu.api.ovh.com/", param="test") == m_call.return_value 140 | assert call("https://eu.api.ovh.com/?query=string", param="test") == m_call.return_value 141 | assert call("https://eu.api.ovh.com/?query=string", checkbox=True) == m_call.return_value 142 | assert call("https://eu.api.ovh.com/", _from="start", to="end") == m_call.return_value 143 | 144 | assert m_call.call_args_list == [ 145 | mock.call(method, "https://eu.api.ovh.com/", None, True), 146 | mock.call(method, "https://eu.api.ovh.com/", {"param": "test"}, True), 147 | mock.call(method, "https://eu.api.ovh.com/?query=string", {"param": "test"}, True), 148 | mock.call(method, "https://eu.api.ovh.com/?query=string", {"checkbox": True}, True), 149 | mock.call(method, "https://eu.api.ovh.com/", {"from": "start", "to": "end"}, True), 150 | ] 151 | 152 | # test core function 153 | 154 | @mock.patch("time.time", return_value=1457018875.467238) 155 | @mock.patch("ovh.client.Session.request") 156 | @mock.patch("ovh.client.Client.time_delta", new_callable=mock.PropertyMock, return_value=0) 157 | def test_call_signature(self, m_time_delta, m_req, m_time): 158 | m_res = m_req.return_value 159 | m_res.status_code = 200 160 | m_json = m_res.json.return_value 161 | 162 | body = {"a": "b", "c": "d"} 163 | j_body = '{"a":"b","c":"d"}' 164 | 165 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) 166 | urlUnauth = "https://eu.api.ovh.com/1.0/unauth" 167 | urlAuth = "https://eu.api.ovh.com/1.0/auth" 168 | 169 | for method in "GET", "POST", "PUT", "DELETE": 170 | assert api.call(method, "/unauth", None if method in ("GET", "DELETE") else body, False) == m_json 171 | assert api.call(method, "/auth", None if method in ("GET", "DELETE") else body, True) == m_json 172 | 173 | signatures = { 174 | "GET": "$1$e9556054b6309771395efa467c22e627407461ad", 175 | "POST": "$1$ec2fb5c7a81f64723c77d2e5b609ae6f58a84fc1", 176 | "PUT": "$1$8a75a9e7c8e7296c9dbeda6a2a735eb6bd58ec4b", 177 | "DELETE": "$1$a1eecd00b3b02b6cf5708b84b9ff42059a950d85", 178 | } 179 | 180 | def _h(m, auth): 181 | h = {"X-Ovh-Application": MockApplicationKey} 182 | if m in ("POST", "PUT"): 183 | h["Content-type"] = "application/json" 184 | if auth: 185 | h["X-Ovh-Consumer"] = MockConsumerKey 186 | h["X-Ovh-Timestamp"] = str(MockTime) 187 | h["X-Ovh-Signature"] = signatures[m] 188 | return h 189 | 190 | assert m_req.call_args_list == [ 191 | mock.call("GET", urlUnauth, headers=_h("GET", False), data="", timeout=180), 192 | mock.call("GET", urlAuth, headers=_h("GET", True), data="", timeout=180), 193 | mock.call("POST", urlUnauth, headers=_h("POST", False), data=j_body, timeout=180), 194 | mock.call("POST", urlAuth, headers=_h("POST", True), data=j_body, timeout=180), 195 | mock.call("PUT", urlUnauth, headers=_h("PUT", False), data=j_body, timeout=180), 196 | mock.call("PUT", urlAuth, headers=_h("PUT", True), data=j_body, timeout=180), 197 | mock.call("DELETE", urlUnauth, headers=_h("DELETE", False), data="", timeout=180), 198 | mock.call("DELETE", urlAuth, headers=_h("DELETE", True), data="", timeout=180), 199 | ] 200 | 201 | @mock.patch("ovh.client.Session.request") 202 | def test_call_query_id(self, m_req): 203 | m_res = m_req.return_value 204 | m_res.status_code = 99 205 | m_res.headers = {"X-OVH-QUERYID": "FR.test1"} 206 | 207 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 208 | with pytest.raises(APIError) as e: 209 | api.call("GET", "/unit/test", None, False) 210 | assert e.value.query_id == "FR.test1" 211 | 212 | @mock.patch("ovh.client.Session.request") 213 | def test_call_errors(self, m_req): 214 | m_res = m_req.return_value 215 | 216 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 217 | 218 | # request fails, somehow 219 | m_req.side_effect = requests.RequestException 220 | with pytest.raises(HTTPError): 221 | api.call("GET", "/unauth", None, False) 222 | m_req.side_effect = None 223 | 224 | # response decoding fails 225 | m_res.json.side_effect = ValueError 226 | with pytest.raises(InvalidResponse): 227 | api.call("GET", "/unauth", None, False) 228 | m_res.json.side_effect = None 229 | 230 | # HTTP errors 231 | for status_code, body, exception in ( 232 | (404, {}, ResourceNotFoundError), 233 | (403, {"errorCode": "NOT_GRANTED_CALL"}, NotGrantedCall), 234 | (403, {"errorCode": "NOT_CREDENTIAL"}, NotCredential), 235 | (403, {"errorCode": "INVALID_KEY"}, InvalidKey), 236 | (403, {"errorCode": "INVALID_CREDENTIAL"}, InvalidCredential), 237 | (403, {"errorCode": "FORBIDDEN"}, Forbidden), 238 | (400, {}, BadParametersError), 239 | (409, {}, ResourceConflictError), 240 | (460, {}, ResourceExpiredError), 241 | (0, {}, NetworkError), 242 | (99, {}, APIError), 243 | (306, {}, APIError), 244 | ): 245 | m_res.status_code = status_code 246 | m_res.json.return_value = body 247 | with pytest.raises(exception): 248 | api.call("GET", "/unauth", None, False) 249 | 250 | # errors 251 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret, None) 252 | with pytest.raises(InvalidKey): 253 | api.call("GET", "/unit/test", None, True) 254 | 255 | @mock.patch("ovh.client.Session.request", return_value="Let's assume requests will return this") 256 | def test_raw_call_with_headers(self, m_req): 257 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret) 258 | r = api.raw_call("GET", "/unit/path", None, False, headers={"Custom-Header": "1"}) 259 | assert r == "Let's assume requests will return this" 260 | assert m_req.call_args_list == [ 261 | mock.call( 262 | "GET", 263 | "https://eu.api.ovh.com/1.0/unit/path", 264 | headers={ 265 | "Custom-Header": "1", 266 | "X-Ovh-Application": MockApplicationKey, 267 | }, 268 | data="", 269 | timeout=180, 270 | ) 271 | ] 272 | 273 | # Perform real API tests. 274 | def test_endpoints(self): 275 | for endpoint in ENDPOINTS.keys(): 276 | auth_time = Client(endpoint, MockApplicationKey, MockApplicationSecret).get("/auth/time", _need_auth=False) 277 | assert auth_time > 0 278 | 279 | @mock.patch("time.time", return_value=1457018875.467238) 280 | @mock.patch("ovh.client.Session.request") 281 | @mock.patch("ovh.client.Client.time_delta", new_callable=mock.PropertyMock, return_value=0) 282 | def test_version_in_url(self, m_time_delta, m_req, m_time): 283 | m_res = m_req.return_value 284 | m_res.status_code = 200 285 | 286 | api = Client("ovh-eu", MockApplicationKey, MockApplicationSecret, MockConsumerKey) 287 | api.call("GET", "/call", None, True) 288 | api.call("GET", "/v1/call", None, True) 289 | api.call("GET", "/v2/call", None, True) 290 | 291 | signatures = { 292 | "1.0": "$1$7f2db49253edfc41891023fcd1a54cf61db05fbb", 293 | "v1": "$1$e6e7906d385eb28adcbfbe6b66c1528a42d741ad", 294 | "v2": "$1$bb63b132a6f84ad5433d0c534d48d3f7c3804285", 295 | } 296 | 297 | def _h(prefix): 298 | return { 299 | "X-Ovh-Application": MockApplicationKey, 300 | "X-Ovh-Consumer": MockConsumerKey, 301 | "X-Ovh-Timestamp": str(MockTime), 302 | "X-Ovh-Signature": signatures[prefix], 303 | } 304 | 305 | assert m_req.call_args_list == [ 306 | mock.call("GET", "https://eu.api.ovh.com/1.0/call", headers=_h("1.0"), data="", timeout=180), 307 | mock.call("GET", "https://eu.api.ovh.com/v1/call", headers=_h("v1"), data="", timeout=180), 308 | mock.call("GET", "https://eu.api.ovh.com/v2/call", headers=_h("v2"), data="", timeout=180), 309 | ] 310 | 311 | @mock.patch("ovh.client.Session.request") 312 | def test_oauth2(self, m_req): 313 | def resp(*args, **kwargs): 314 | if args[0] == "POST" and args[1] == "https://www.ovh.com/auth/oauth2/token": 315 | resp = mock.Mock() 316 | resp.status_code = 200 317 | resp.text = """{ 318 | "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", 319 | "token_type":"Bearer", 320 | "expires_in":3, 321 | "scope":"all" 322 | }""" 323 | return resp 324 | 325 | if args[0] == "GET" and args[1] == "https://eu.api.ovh.com/1.0/call": 326 | resp = mock.Mock() 327 | resp.status_code = 200 328 | resp.text = "{}" 329 | return resp 330 | 331 | raise NotImplementedError("FIXME") 332 | 333 | m_req.side_effect = resp 334 | 335 | call_oauth = mock.call( 336 | "POST", 337 | "https://www.ovh.com/auth/oauth2/token", 338 | headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, 339 | data={"grant_type": "client_credentials", "scope": "all"}, 340 | files=None, 341 | timeout=None, 342 | auth=mock.ANY, 343 | verify=None, 344 | proxies=None, 345 | cert=None, 346 | ) 347 | call_api = mock.call( 348 | "GET", 349 | "https://eu.api.ovh.com/1.0/call", 350 | headers={"Authorization": "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3"}, 351 | data="", 352 | files=None, 353 | timeout=180, 354 | ) 355 | 356 | # First call triggers the fetch of a token, then the real call 357 | api = Client("ovh-eu", client_id="oauth2_id", client_secret="oauth2_secret") 358 | api.call("GET", "/call", None, True) 359 | assert m_req.call_args_list == [call_oauth, call_api] 360 | 361 | # Calling the API again does not trigger the fetch of a new token 362 | api.call("GET", "/call", None, True) 363 | assert m_req.call_args_list == [call_oauth, call_api, call_api] 364 | 365 | # The fetched token had an `expires_in` set to 3, sleep more than that, which makes us fetch a now token 366 | time.sleep(4) 367 | api.call("GET", "/call", None, True) 368 | assert m_req.call_args_list == [call_oauth, call_api, call_api, call_oauth, call_api] 369 | 370 | @mock.patch("ovh.client.Session.request") 371 | def test_oauth2_503(self, m_req): 372 | m_res = m_req.return_value 373 | m_res.status_code = 503 374 | m_res.text = "

test

" 375 | 376 | api = Client("ovh-eu", client_id="oauth2_id", client_secret="oauth2_secret") 377 | 378 | with pytest.raises(OAuth2FailureError) as e: 379 | api.call("GET", "/call", None, True) 380 | assert str(e.value) == ( 381 | "OAuth2 failure: Missing access token parameter. Token creation failed with status_code=503, " 382 | "body=

test

" 383 | ) 384 | 385 | @mock.patch("ovh.client.Session.request") 386 | def test_oauth2_bad_json(self, m_req): 387 | m_res = m_req.return_value 388 | m_res.status_code = 200 389 | m_res.text = "

test

" 390 | 391 | api = Client("ovh-eu", client_id="oauth2_id", client_secret="oauth2_secret") 392 | 393 | with pytest.raises(OAuth2FailureError) as e: 394 | api.call("GET", "/call", None, True) 395 | assert str(e.value) == ( 396 | "OAuth2 failure: Missing access token parameter. Received invalid body: " 397 | "

test

" 398 | ) 399 | 400 | @mock.patch("ovh.client.Session.request") 401 | def test_oauth2_unknown_client(self, m_req): 402 | m_res = m_req.return_value 403 | m_res.status_code = 200 404 | m_res.text = '{"error":"invalid_client", "error_description":"ovhcloud oauth2 client does not exists"}' 405 | 406 | api = Client("ovh-eu", client_id="oauth2_id", client_secret="oauth2_secret") 407 | 408 | with pytest.raises(OAuth2FailureError) as e: 409 | api.call("GET", "/call", None, True) 410 | assert str(e.value) == "OAuth2 failure: (invalid_client) ovhcloud oauth2 client does not exists" 411 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from configparser import MissingSectionHeaderError 28 | import os 29 | from pathlib import Path 30 | from unittest.mock import patch 31 | 32 | import pytest 33 | 34 | import ovh 35 | from ovh.exceptions import InvalidConfiguration, InvalidRegion 36 | 37 | TEST_DATA = str(Path(__file__).resolve().parent / "data") 38 | systemConf = TEST_DATA + "/system.ini" 39 | userPartialConf = TEST_DATA + "/userPartial.ini" 40 | userConf = TEST_DATA + "/user.ini" 41 | userOAuth2Conf = TEST_DATA + "/user_oauth2.ini" 42 | userOAuth2InvalidConf = TEST_DATA + "/user_oauth2_invalid.ini" 43 | userOAuth2IncompatibleConfig = TEST_DATA + "/user_oauth2_incompatible.ini" 44 | userBothConf = TEST_DATA + "/user_both.ini" 45 | localPartialConf = TEST_DATA + "/localPartial.ini" 46 | doesNotExistConf = TEST_DATA + "/doesNotExist.ini" 47 | invalidINIConf = TEST_DATA + "/invalid.ini" 48 | errorConf = TEST_DATA 49 | 50 | 51 | class TestConfig: 52 | def test_real_lookup_path(self): 53 | home = os.environ["HOME"] 54 | pwd = os.environ["PWD"] 55 | 56 | assert ovh.config.CONFIG_PATH == [ 57 | "/etc/ovh.conf", 58 | home + "/.ovh.conf", 59 | pwd + "/ovh.conf", 60 | ] 61 | 62 | @patch("ovh.config.CONFIG_PATH", [systemConf, userPartialConf, localPartialConf]) 63 | def test_config_from_files(self): 64 | client = ovh.Client(endpoint="ovh-eu") 65 | assert client._application_key == "system" 66 | assert client._application_secret == "user" 67 | assert client._consumer_key == "local" 68 | 69 | @patch("ovh.config.CONFIG_PATH", [userConf]) 70 | def test_config_from_given_config_file(self): 71 | client = ovh.Client(endpoint="ovh-eu", config_file=systemConf) 72 | assert client._application_key == "system" 73 | assert client._application_secret == "system" 74 | assert client._consumer_key == "system" 75 | 76 | @patch("ovh.config.CONFIG_PATH", [userConf]) 77 | def test_config_from_only_one_file(self): 78 | client = ovh.Client(endpoint="ovh-eu") 79 | assert client._application_key == "user" 80 | assert client._application_secret == "user" 81 | assert client._consumer_key == "user" 82 | 83 | @patch("ovh.config.CONFIG_PATH", [doesNotExistConf]) 84 | def test_config_from_non_existing_file(self): 85 | with pytest.raises(InvalidConfiguration) as e: 86 | ovh.Client(endpoint="ovh-eu") 87 | 88 | assert str(e.value) == ( 89 | "Missing authentication information, you need to provide at least an " 90 | "application_key/application_secret or a client_id/client_secret" 91 | ) 92 | 93 | @patch("ovh.config.CONFIG_PATH", [invalidINIConf]) 94 | def test_config_from_invalid_ini_file(self): 95 | with pytest.raises(MissingSectionHeaderError): 96 | ovh.Client(endpoint="ovh-eu") 97 | 98 | @patch("ovh.config.CONFIG_PATH", [errorConf]) 99 | def test_config_from_invalid_file(self): 100 | with pytest.raises(InvalidConfiguration) as e: 101 | ovh.Client(endpoint="ovh-eu") 102 | 103 | assert str(e.value) == ( 104 | "Missing authentication information, you need to provide at least an " 105 | "application_key/application_secret or a client_id/client_secret" 106 | ) 107 | 108 | @patch("ovh.config.CONFIG_PATH", [userOAuth2Conf]) 109 | def test_config_oauth2(self): 110 | client = ovh.Client(endpoint="ovh-eu") 111 | assert client._client_id == "foo" 112 | assert client._client_secret == "bar" 113 | 114 | @patch("ovh.config.CONFIG_PATH", [userBothConf]) 115 | def test_config_invalid_both(self): 116 | with pytest.raises(InvalidConfiguration) as e: 117 | ovh.Client(endpoint="ovh-eu") 118 | 119 | assert str(e.value) == "Can't use both application_key/application_secret and OAuth2 client_id/client_secret" 120 | 121 | @patch("ovh.config.CONFIG_PATH", [userOAuth2InvalidConf]) 122 | def test_config_invalid_oauth2(self): 123 | with pytest.raises(InvalidConfiguration) as e: 124 | ovh.Client(endpoint="ovh-eu") 125 | 126 | assert str(e.value) == "Invalid OAuth2 config, both client_id and client_secret must be given" 127 | 128 | @patch("ovh.config.CONFIG_PATH", [userOAuth2IncompatibleConfig]) 129 | def test_config_incompatible_oauth2(self): 130 | with pytest.raises(InvalidConfiguration) as e: 131 | ovh.Client(endpoint="kimsufi-eu") 132 | 133 | assert str(e.value) == ( 134 | "OAuth2 authentication is not compatible with endpoint kimsufi-eu " 135 | + "(it can only be used with ovh-eu, ovh-ca and ovh-us)" 136 | ) 137 | 138 | @patch("ovh.config.CONFIG_PATH", [userConf]) 139 | @patch.dict( 140 | "os.environ", 141 | { 142 | "OVH_ENDPOINT": "ovh-eu", 143 | "OVH_APPLICATION_KEY": "env", 144 | "OVH_APPLICATION_SECRET": "env", 145 | "OVH_CONSUMER_KEY": "env", 146 | }, 147 | ) 148 | def test_config_from_env(self): 149 | client = ovh.Client(endpoint="ovh-eu") 150 | assert client._application_key == "env" 151 | assert client._application_secret == "env" 152 | assert client._consumer_key == "env" 153 | 154 | @patch("ovh.config.CONFIG_PATH", [userConf]) 155 | def test_config_from_args(self): 156 | client = ovh.Client( 157 | endpoint="ovh-eu", application_key="param", application_secret="param", consumer_key="param" 158 | ) 159 | assert client._application_key == "param" 160 | assert client._application_secret == "param" 161 | assert client._consumer_key == "param" 162 | 163 | def test_invalid_endpoint(self): 164 | with pytest.raises(InvalidRegion): 165 | ovh.Client(endpoint="not_existing") 166 | -------------------------------------------------------------------------------- /tests/test_consumer_key.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2025, OVH SAS. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of OVH SAS nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from unittest import mock 28 | 29 | import ovh 30 | 31 | 32 | class TestConsumerKey: 33 | def test_add_rules(self): 34 | # Prepare 35 | m_client = mock.Mock() 36 | ck = ovh.ConsumerKeyRequest(m_client) 37 | 38 | # Test: No-op 39 | assert ck._access_rules == [] 40 | 41 | # Test: allow one 42 | ck.add_rule("GET", "/me") 43 | assert ck._access_rules == [{"method": "GET", "path": "/me"}] 44 | 45 | # Test: allow RO on /xdsl 46 | ck._access_rules = [] 47 | ck.add_rules(ovh.API_READ_ONLY, "/xdsl") 48 | assert ck._access_rules == [ 49 | {"method": "GET", "path": "/xdsl"}, 50 | ] 51 | 52 | # Test: allow safe methods on domain 53 | ck._access_rules = [] 54 | ck.add_rules(ovh.API_READ_WRITE_SAFE, "/domains/test.com") 55 | assert ck._access_rules == [ 56 | {"method": "GET", "path": "/domains/test.com"}, 57 | {"method": "POST", "path": "/domains/test.com"}, 58 | {"method": "PUT", "path": "/domains/test.com"}, 59 | ] 60 | 61 | # Test: allow all sms, strips suffix 62 | ck._access_rules = [] 63 | ck.add_recursive_rules(ovh.API_READ_WRITE, "/sms/*") 64 | assert ck._access_rules == [ 65 | {"method": "GET", "path": "/sms"}, 66 | {"method": "POST", "path": "/sms"}, 67 | {"method": "PUT", "path": "/sms"}, 68 | {"method": "DELETE", "path": "/sms"}, 69 | {"method": "GET", "path": "/sms/*"}, 70 | {"method": "POST", "path": "/sms/*"}, 71 | {"method": "PUT", "path": "/sms/*"}, 72 | {"method": "DELETE", "path": "/sms/*"}, 73 | ] 74 | 75 | # Test: allow all, does not insert the empty rule 76 | ck._access_rules = [] 77 | ck.add_recursive_rules(ovh.API_READ_WRITE, "/") 78 | assert ck._access_rules == [ 79 | {"method": "GET", "path": "/*"}, 80 | {"method": "POST", "path": "/*"}, 81 | {"method": "PUT", "path": "/*"}, 82 | {"method": "DELETE", "path": "/*"}, 83 | ] 84 | 85 | # Test launch request 86 | ck._access_rules = [] 87 | ck.add_recursive_rules(ovh.API_READ_WRITE, "/") 88 | assert ck.request() is m_client.request_consumerkey.return_value 89 | m_client.request_consumerkey.assert_called_once_with(ck._access_rules, None, None) 90 | --------------------------------------------------------------------------------