├── .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 = "