├── docs
├── img
│ └── logo.png
├── api
│ └── ovh
│ │ ├── config.rst
│ │ ├── exceptions.rst
│ │ └── client.rst
├── Makefile
├── make.bat
├── conf.py
└── index.rst
├── tests
├── fixtures
│ ├── home_ovh.conf
│ ├── etc_ovh.conf
│ └── pwd_ovh.conf
├── __init__.py
├── test_config.py
└── test_client.py
├── .gitignore
├── requirements-dev.txt
├── MANIFEST.in
├── .travis.yml
├── setup.py
├── setup.cfg
├── CHANGELOG.md
├── LICENSE
├── ovh
├── __init__.py
├── exceptions.py
├── config.py
└── client.py
├── CONTRIBUTING.rst
├── MIGRATION.rst
└── README.rst
/docs/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NicolasLM/python-ovh/master/docs/img/logo.png
--------------------------------------------------------------------------------
/tests/fixtures/home_ovh.conf:
--------------------------------------------------------------------------------
1 | [ovh-eu]
2 | ; OVH EU is per *user*, overloads the consumer_key
3 | consumer_key=I am kidding at home
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # execution artefacts
2 | *.pyc
3 | .coverage
4 |
5 | # dist artefacts
6 | build/
7 | dist/
8 | ovh.egg-info/
9 | *.egg
10 |
11 | # documentation artefacts
12 | docs/_build
13 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # tests, common
2 | coverage==3.7.1
3 | mock==1.0.1
4 | nose==1.3.3
5 | yanc==0.2.4
6 | Sphinx==1.2.2
7 | coveralls==0.4.2
8 |
9 | # Python 2.6
10 | ordereddict==1.0
11 |
12 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.6"
4 | - "2.7"
5 | - "3.2"
6 | - "3.3"
7 | - "3.4"
8 | install:
9 | - pip install .
10 | - pip install -r requirements-dev.txt
11 | script: nosetests
12 | after_success:
13 | coveralls
14 |
15 |
--------------------------------------------------------------------------------
/tests/fixtures/etc_ovh.conf:
--------------------------------------------------------------------------------
1 | [default]
2 | ; general configuration: default endpoint
3 | endpoint=ovh-eu
4 |
5 | [ovh-eu]
6 | ; OVH EU is *global*
7 | application_key=This is a *fake* global application key
8 | application_secret=This is a *real* global application secret
9 | consumer_key=I am globally kidding
10 |
--------------------------------------------------------------------------------
/tests/fixtures/pwd_ovh.conf:
--------------------------------------------------------------------------------
1 | [default]
2 | ; general configuration: default endpoint
3 | endpoint=runabove-ca
4 |
5 | [runabove-ca]
6 | ; Runabove is *local*, but OVH-EU is still available
7 | application_key=This is a fake local application key
8 | application_secret=This is a *real* local application key
9 | consumer_key=I am locally kidding
10 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | try:
4 | from setuptools import setup
5 | except ImportError:
6 | from distribute_setup import use_setuptools
7 | use_setuptools()
8 | from setuptools import setup
9 |
10 | setup(
11 | setup_requires=['d2to1'],
12 | d2to1=True,
13 | tests_require=[
14 | "coverage==3.7.1",
15 | "mock==1.0.1",
16 | "nose==1.3.3",
17 | "yanc==0.2.4",
18 | ],
19 | )
20 |
21 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = ovh
3 | version = 0.3.4
4 | author = Jean-Tiare Le Bigot
5 | author-email = jean-tiare.le-bigot@ovh.net
6 | home-page = https://api.ovh.com
7 | summary = Official OVH.com API wrapper
8 | description-file = README.rst
9 | license = BSD
10 | platforms = "Posix; MacOS X; Windows"
11 | requires-dist =
12 | setuptools
13 | requests>=2.3.0
14 | d2to1==0.2.11
15 | classifier =
16 | License :: OSI Approved :: BSD License
17 | Development Status :: 4 - Beta
18 | Intended Audience :: Developers
19 | Operating System :: OS Independent
20 | Programming Language :: Python
21 | Programming Language :: Python :: 2.6
22 | Programming Language :: Python :: 2.7
23 | Programming Language :: Python :: 3.2
24 | Programming Language :: Python :: 3.3
25 | Topic :: Software Development :: Libraries :: Python Modules
26 | Topic :: System :: Archiving :: Packaging
27 | keywords =
28 | ovh
29 | sdk
30 | rest
31 |
32 | [files]
33 | packages = ovh
34 |
35 | [test]
36 | test-suite = nose.collector
37 |
38 | [nosetests]
39 | verbosity = 2
40 | where = tests/
41 | with-yanc = 1
42 | with-coverage = 1
43 | cover-package = ovh
44 | cover-erase = 1
45 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | ## 0.3.4 (2015-06-10)
5 |
6 | - [enhancement] add NotGrantedCall, NotCredential, Forbidden, InvalidCredential exceptions
7 |
8 | ## 0.3.3 (2015-03-11)
9 |
10 | - [fix] Python 3 tests false negative
11 | - [fix] More flexible requests dependency
12 |
13 | ## 0.3.2 (2015-02-16)
14 |
15 | - [fix] Python 3 build
16 |
17 | ## 0.3.1 (2015-02-16)
18 |
19 | - [enhancement] support '_' prefixed keyword argument alias when colliding with Python reserved keywords
20 | - [enhancement] add API documentation
21 | - [enhancement] Use requests Session objects (thanks @xtrochu-edp)
22 |
23 | ## 0.3.0 (2014-11-23)
24 | - [enhancement] add kimsufi API Europe/North-America
25 | - [enhancement] add soyoustart API Europe/North-America
26 | - [Q/A] add minimal integration test
27 |
28 | ## 0.2.1 (2014-09-26)
29 | - [enhancement] add links to 'CreateToken' pages in Readme
30 | - [compatibility] add support for Python 2.6, 3.2 and 3.3
31 |
32 | ## 0.2.0 (2014-09-19)
33 | - [feature] travis / coveralls / pypi integration
34 | - [feature] config files for credentials
35 | - [feature] support ``**kwargs`` notation for ``Client.get`` query string.
36 | - [enhancement] rewrite README
37 | - [enhancement] add CONTRIBUTING guidelines
38 | - [enhancement] add MIGRATION guide
39 | - [fix] workaround ``**kwargs`` query param and function arguments collision
40 |
41 | ## 0.1.0 (2014-09-09)
42 | - [feature] ConsumerKey lifecycle
43 | - [feature] OVH and RunAbove support
44 | - [feature] OAuth 1.0 support, request signing
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2014, 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/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/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 | .. automethod:: Client.request_consumerkey
26 |
27 | get/post/put/delete
28 | -------------------
29 |
30 | Shortcuts around :py:func:`Client.call`. This is the recommended way to use the
31 | wrapper.
32 |
33 | For example, requesting the list of all bills would look like:
34 |
35 | .. code:: python
36 |
37 | bills = client.get('/me/bills')
38 |
39 | In a similar fashion, enabling network burst on a specific server would look
40 | like:
41 |
42 | .. code:: python
43 |
44 | client.put('/dedicated/server/%s/burst' % server_name, status='active')
45 |
46 | :param str target: Rest Method as shown in API's console.
47 | :param boolean need_auth: When `False`, bypass the signature process. This is
48 | interesting when calling authentication related method. Defaults to `True`
49 | :param dict kwargs: (:py:func:`Client.post` and :py:func:`Client.put` only)
50 | all extra keyword arguments are passed as `data` dict to `call`. This is a
51 | syntaxic sugar to call API entrypoints using a regular method syntax.
52 |
53 | .. automethod:: Client.get
54 | .. automethod:: Client.post
55 | .. automethod:: Client.put
56 | .. automethod:: Client.delete
57 |
58 | Low level API
59 | =============
60 |
61 | call
62 | ----
63 |
64 | .. automethod:: Client.call
65 |
66 | time_delta
67 | ----------
68 |
69 | .. autoattribute:: Client.time_delta
70 |
--------------------------------------------------------------------------------
/ovh/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | from .client import Client
30 | from .exceptions import (
31 | APIError, NetworkError, InvalidResponse, InvalidRegion, ReadOnlyError,
32 | ResourceNotFoundError, BadParametersError, ResourceConflictError, HTTPError,
33 | InvalidKey, InvalidCredential, NotGrantedCall, NotCredential, Forbidden,
34 | )
35 |
--------------------------------------------------------------------------------
/ovh/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | """
30 | All exceptions used in OVH SDK derives from `APIError`
31 | """
32 |
33 | class APIError(Exception):
34 | """Base OVH API exception, all specific exceptions inherits from it."""
35 |
36 | class HTTPError(APIError):
37 | """Raised when the request fails at a low level (DNS, network, ...)"""
38 |
39 | class InvalidKey(APIError):
40 | """Raised when trying to sign request with invalid key"""
41 |
42 | class InvalidCredential(APIError):
43 | """Raised when trying to sign request with invalid consumer key"""
44 |
45 | class InvalidResponse(APIError):
46 | """Raised when api response is not valid json"""
47 |
48 | class InvalidRegion(APIError):
49 | """Raised when region is not in `REGIONS`."""
50 |
51 | class ReadOnlyError(APIError):
52 | """Raised when attempting to modify readonly data."""
53 |
54 | class ResourceNotFoundError(APIError):
55 | """Raised when requested resource does not exist."""
56 |
57 | class BadParametersError(APIError):
58 | """Raised when request contains bad parameters."""
59 |
60 | class ResourceConflictError(APIError):
61 | """Raised when trying to create an already existing resource."""
62 |
63 | class NetworkError(APIError):
64 | """Raised when there is an error from network layer."""
65 |
66 | class NotGrantedCall(APIError):
67 | """Raised when there is an error from network layer."""
68 |
69 | class NotCredential(APIError):
70 | """Raised when there is an error from network layer."""
71 |
72 | class Forbidden(APIError):
73 | """Raised when there is an error from network layer."""
74 |
--------------------------------------------------------------------------------
/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 fully (100% coverage) unit-tested
9 | 3. your code must be fully 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 `_
18 | - The documentation style follows `PEP-257: Docstring Conventions `_
19 |
20 | A good practice is to frequently run you code through `pylint
21 | `_
22 | and make sure the code grades ``10.0``.
23 |
24 | Submitting Modifications:
25 | -------------------------
26 |
27 | The contributions should be email patches. The guidelines are the same
28 | as the patch submission for the Linux kernel except for the DCO which
29 | is defined below. The guidelines are defined in the
30 | 'SubmittingPatches' file, available in the directory 'Documentation'
31 | of the Linux kernel source tree.
32 |
33 | It can be accessed online too:
34 |
35 | https://www.kernel.org/doc/Documentation/SubmittingPatches
36 |
37 | You can submit your patches via GitHub
38 |
39 | Licensing for new files:
40 | ------------------------
41 |
42 | Python-OVH is licensed under a (modified) BSD license. Anything contributed to
43 | Python-OVH must be released under this license.
44 |
45 | When introducing a new file into the project, please make sure it has a
46 | copyright header making clear under which license it's being released.
47 |
48 | Developer Certificate of Origin:
49 | --------------------------------
50 |
51 | To improve tracking of contributions to this project we will use a
52 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure
53 | on patches that are being emailed around or contributed in any other
54 | way.
55 |
56 | The sign-off is a simple line at the end of the explanation for the
57 | patch, which certifies that you wrote it or otherwise have the right
58 | to pass it on as an open-source patch. The rules are pretty simple:
59 | if you can certify the below:
60 |
61 | By making a contribution to this project, I certify that:
62 |
63 | (a) The contribution was created in whole or in part by me and I have
64 | the right to submit it under the open source license indicated in
65 | the file; or
66 |
67 | (b) The contribution is based upon previous work that, to the best of
68 | my knowledge, is covered under an appropriate open source License
69 | and I have the right under that license to submit that work with
70 | modifications, whether created in whole or in part by me, under
71 | the same open source license (unless I am permitted to submit
72 | under a different license), as indicated in the file; or
73 |
74 | (c) The contribution was provided directly to me by some other person
75 | who certified (a), (b) or (c) and I have not modified it.
76 |
77 | (d) The contribution is made free of any other party's intellectual
78 | property claims or rights.
79 |
80 | (e) I understand and agree that this project and the contribution are
81 | public and that a record of the contribution (including all
82 | personal information I submit with it, including my sign-off) is
83 | maintained indefinitely and may be redistributed consistent with
84 | this project or the open source license(s) involved.
85 |
86 |
87 | then you just add a line saying
88 |
89 | Signed-off-by: Random J Developer
90 |
91 | using your real name (sorry, no pseudonyms or anonymous contributions.)
92 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | from ovh import config
30 | import unittest
31 | import mock
32 | import os
33 |
34 | M_CONFIG_PATH = [
35 | './fixtures/etc_ovh.conf',
36 | './fixtures/home_ovh.conf',
37 | './fixtures/pwd_ovh.conf',
38 | ]
39 |
40 | M_ENVIRON = {
41 | 'OVH_ENDPOINT': 'endpoint from environ',
42 | 'OVH_APPLICATION_KEY': 'application key from environ',
43 | 'OVH_APPLICATION_SECRET': 'application secret from environ',
44 | 'OVH_CONSUMER_KEY': 'consumer key from from environ',
45 | }
46 |
47 | class testConfig(unittest.TestCase):
48 | def setUp(self):
49 | """Overload configuration lookup path"""
50 | self._orig_CONFIG_PATH = config.CONFIG_PATH
51 | config.CONFIG_PATH = M_CONFIG_PATH
52 |
53 | def tearDown(self):
54 | """Restore configuraton lookup path"""
55 | config.CONFIG_PATH = self._orig_CONFIG_PATH
56 |
57 | def test_real_lookup_path(self):
58 | home = os.environ['HOME']
59 | pwd = os.environ['PWD']
60 |
61 | self.assertEqual([
62 | '/etc/ovh.conf',
63 | home+'/.ovh.conf',
64 | pwd+'/tests/ovh.conf',
65 |
66 | ], self._orig_CONFIG_PATH)
67 |
68 | def test_config_get_conf(self):
69 | conf = config.ConfigurationManager()
70 |
71 | self.assertEqual('runabove-ca', conf.get('default', 'endpoint'))
72 | self.assertEqual('This is a *fake* global application key', conf.get('ovh-eu', 'application_key'))
73 | self.assertEqual('This is a *real* global application secret', conf.get('ovh-eu', 'application_secret'))
74 | self.assertEqual('I am kidding at home', conf.get('ovh-eu', 'consumer_key'))
75 | self.assertEqual('This is a fake local application key', conf.get('runabove-ca', 'application_key'))
76 | self.assertEqual('This is a *real* local application key', conf.get('runabove-ca', 'application_secret'))
77 | self.assertEqual('I am locally kidding', conf.get('runabove-ca', 'consumer_key'))
78 |
79 | self.assertTrue(conf.get('ovh-eu', 'non-existent') is None)
80 | self.assertTrue(conf.get('ovh-ca', 'application_key') is None)
81 | self.assertTrue(conf.get('ovh-laponie', 'application_key') is None)
82 |
83 | def test_config_get_conf_env_rules_them_all(self):
84 | conf = config.ConfigurationManager()
85 |
86 | with mock.patch.dict('os.environ', M_ENVIRON):
87 | self.assertEqual(M_ENVIRON['OVH_ENDPOINT'], conf.get('wathever', 'endpoint'))
88 | self.assertEqual(M_ENVIRON['OVH_APPLICATION_KEY'], conf.get('wathever', 'application_key'))
89 | self.assertEqual(M_ENVIRON['OVH_APPLICATION_SECRET'], conf.get('wathever', 'application_secret'))
90 | self.assertEqual(M_ENVIRON['OVH_CONSUMER_KEY'], conf.get('wathever', 'consumer_key'))
91 |
92 | self.assertTrue(conf.get('ovh-eu', 'non-existent') is None)
93 |
--------------------------------------------------------------------------------
/MIGRATION.rst:
--------------------------------------------------------------------------------
1 | ############################
2 | Migrate from legacy wrappers
3 | ############################
4 |
5 | This guide specifically targets developers comming from the legacy wrappers
6 | previously distributed on https://api.ovh.com/g934.first_step_with_api. It
7 | highligts the main evolutions between these 2 major version as well as some
8 | tips to help with the migration. If you have any further questions, feel free
9 | to drop a mail on api@ml.ovh.net (api-subscribe@ml.ovh.net to subscribe).
10 |
11 | Installation
12 | ============
13 |
14 | Legacy wrappers were distributed as zip files for direct integration into
15 | final projects. This new version is fully integrated with Python's standard
16 | distribution channels.
17 |
18 | Recommended way to add ``python-ovh`` to a project: add ``ovh`` to a
19 | ``requirements.txt`` file ate the root of the project.
20 |
21 | .. code::
22 |
23 | # file: requirements.txt
24 | ovh # add '==0.2.0' to force 0.2.0 version
25 |
26 |
27 | To refresh the dependencies, just run:
28 |
29 | .. code:: bash
30 |
31 | pip install -r requirements.txt
32 |
33 | Usage
34 | =====
35 |
36 | Import and the client class
37 | ---------------------------
38 |
39 | Legacy method:
40 | **************
41 |
42 | .. code:: python
43 |
44 | from OvhApi import Api, OVH_API_EU, OVH_API_CA
45 |
46 | New method:
47 | ***********
48 |
49 | .. code:: python
50 |
51 | from ovh import Client
52 |
53 | Instanciate a new client
54 | ------------------------
55 |
56 | Legacy method:
57 | **************
58 |
59 | .. code:: python
60 |
61 | client = Api(OVH_API_EU, 'app key', 'app secret', 'consumer key')
62 |
63 | New method (*compatibility*):
64 | *****************************
65 |
66 | .. code:: python
67 |
68 | client = Client('ovh-eu', 'app key', 'app secret', 'consumer key')
69 |
70 | Similarly, ``OVH_API_CA`` has been replaced by ``'ovh-ca'``.
71 |
72 | New method (*compatibility*):
73 | *****************************
74 |
75 | To avoid embedding credentials in a project, this new version introduced a new
76 | configuration mechanism using either environment variables or configuration
77 | files.
78 |
79 | In a Nutshell, you may put your credentials in a file like ``/etc/ovh.conf`` or
80 | ``~/.ovh.conf`` like this one:
81 |
82 | .. code:: ini
83 |
84 | [default]
85 | ; general configuration: default endpoint
86 | endpoint=ovh-eu
87 |
88 | [ovh-eu]
89 | ; configuration specific to 'ovh-eu' endpoint
90 | application_key=my_app_key
91 | application_secret=my_application_secret
92 | ; uncomment following line when writing a script application
93 | ; with a single consumer key.
94 | ;consumer_key=my_consumer_key
95 |
96 | And then simply create a client instance:
97 |
98 | .. code:: python
99 |
100 | from ovh import Client
101 | client = Client()
102 |
103 | With no additional boilerplate!
104 |
105 | For more informations on available configuration mechanism, please see
106 | https://github.com/ovh/python-ovh/blob/master/README.rst#configuration
107 |
108 | Use the client
109 | --------------
110 |
111 | Legacy method:
112 | **************
113 |
114 | .. code:: python
115 |
116 | # API helpers
117 | data = client.get('/my/method?filter_1=value_1&filter_2=value_2')
118 | data = client.post('/my/method', {'param_1': 'value_1', 'param_2': 'value_2'})
119 | data = client.put('/my/method', {'param_1': 'value_1', 'param_2': 'value_2'})
120 | data = client.delete('/my/method')
121 |
122 | # Advanced, low level call
123 | data = client.rawCall('GET', '/my/method?my_filter=my_value', content=None)
124 |
125 | New method (*compatibility*):
126 | *****************************
127 |
128 | .. code:: python
129 |
130 | # API helpers
131 | data = client.get('/my/method?filter_1=value_1&filter_2=value_2')
132 | data = client.post('/my/method', **{'param_1': 'value_1', 'param_2': 'value_2'})
133 | data = client.put('/my/method', **{'param_1': 'value_1', 'param_2': 'value_2'})
134 | data = client.delete('/my/method')
135 |
136 | # Advanced, low level call
137 | data = client.rawCall('GET', '/my/method?my_filter=my_value', data=None)
138 |
139 |
140 | New method (*recommended*):
141 | ***************************
142 |
143 | .. code:: python
144 |
145 | # API helpers
146 | data = client.get('/my/method', filter_1='value_1', filter_2='value_2')
147 | data = client.post('/my/method', param_1='value_1', param_2='value_2')
148 | data = client.put('/my/method', param_1='value_1', param_2='value_2')
149 | data = client.delete('/my/method')
150 |
151 | # Advanced, low level call
152 | data = client.rawCall('GET', '/my/method?my_filter=my_value', data=None)
153 |
154 |
--------------------------------------------------------------------------------
/ovh/config.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | """
30 | The straightforward way to use OVH's API keys is to embed them directly in the
31 | application code. While this is very convenient, it lacks of elegance and
32 | flexibility.
33 |
34 | Alternatively it is suggested to use configuration files or environment
35 | variables so that the same code may run seamlessly in multiple environments.
36 | Production and development for instance.
37 |
38 | This wrapper will first look for direct instanciation parameters then
39 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and
40 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not
41 | provided, it will look for a configuration file of the form:
42 |
43 | .. code:: ini
44 |
45 | [default]
46 | ; general configuration: default endpoint
47 | endpoint=ovh-eu
48 |
49 | [ovh-eu]
50 | ; configuration specific to 'ovh-eu' endpoint
51 | application_key=my_app_key
52 | application_secret=my_application_secret
53 | consumer_key=my_consumer_key
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 | import os
66 |
67 | try:
68 | from ConfigParser import RawConfigParser, NoSectionError, NoOptionError
69 | except ImportError: # pragma: no cover
70 | # Python 3
71 | from configparser import RawConfigParser, NoSectionError, NoOptionError
72 |
73 | __all__ = ['config']
74 |
75 | #: Locations where to look for configuration file by *increasing* priority
76 | CONFIG_PATH = [
77 | '/etc/ovh.conf',
78 | os.path.expanduser('~/.ovh.conf'),
79 | os.path.realpath('./ovh.conf'),
80 | ]
81 |
82 | class ConfigurationManager(object):
83 | '''
84 | Application wide configuration manager
85 | '''
86 | def __init__(self):
87 | '''
88 | Create a config parser and load config from environment.
89 | '''
90 | # create config parser
91 | self.config = RawConfigParser()
92 | self.config.read(CONFIG_PATH)
93 |
94 | def get(self, section, name):
95 | '''
96 | Load parameter ``name`` from configuration, respecting priority order.
97 | Most of the time, ``section`` will correspond to the current api
98 | ``endpoint``. ``default`` section only contains ``endpoint`` and general
99 | configuration.
100 |
101 | :param str section: configuration section or region name. Ignored when
102 | looking in environment
103 | :param str name: configuration parameter to lookup
104 | '''
105 | # 1/ try env
106 | try:
107 | return os.environ['OVH_'+name.upper()]
108 | except KeyError:
109 | pass
110 |
111 | # 2/ try from specified section/endpoint
112 | try:
113 | return self.config.get(section, name)
114 | except (NoSectionError, NoOptionError):
115 | pass
116 |
117 | # not found, sorry
118 | return None
119 |
120 | #: System wide instance :py:class:`ConfigurationManager` instance
121 | config = ConfigurationManager()
122 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | import sys
16 | import os
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | #sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be
29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
30 | # ones.
31 | extensions = [
32 | 'sphinx.ext.autodoc',
33 | 'sphinx.ext.doctest',
34 | 'sphinx.ext.coverage',
35 | 'sphinx.ext.viewcode',
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # The suffix of source filenames.
42 | source_suffix = '.rst'
43 |
44 | # The encoding of source files.
45 | #source_encoding = 'utf-8-sig'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 |
50 | # General information about the project.
51 | project = u'Python-OVH'
52 | copyright = u'2013-2014, OVH SAS'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | version = '0.3'
60 | # The full version, including alpha/beta/rc tags.
61 | release = '0.3.4'
62 |
63 | # The language for content autogenerated by Sphinx. Refer to documentation
64 | # for a list of supported languages.
65 | #language = None
66 |
67 | # There are two options for replacing |today|: either, you set today to some
68 | # non-false value, then it is used:
69 | #today = ''
70 | # Else, today_fmt is used as the format for a strftime call.
71 | #today_fmt = '%B %d, %Y'
72 |
73 | # List of patterns, relative to source directory, that match files and
74 | # directories to ignore when looking for source files.
75 | exclude_patterns = ['_build']
76 |
77 | # The reST default role (used for this markup: `text`) to use for all
78 | # documents.
79 | #default_role = None
80 |
81 | # If true, '()' will be appended to :func: etc. cross-reference text.
82 | #add_function_parentheses = True
83 |
84 | # If true, the current module name will be prepended to all description
85 | # unit titles (such as .. function::).
86 | #add_module_names = True
87 |
88 | # If true, sectionauthor and moduleauthor directives will be shown in the
89 | # output. They are ignored by default.
90 | #show_authors = False
91 |
92 | # The name of the Pygments (syntax highlighting) style to use.
93 | pygments_style = 'sphinx'
94 |
95 | # A list of ignored prefixes for module index sorting.
96 | #modindex_common_prefix = []
97 |
98 | # If true, keep warnings as "system message" paragraphs in the built documents.
99 | #keep_warnings = False
100 |
101 |
102 | # -- Options for HTML output ----------------------------------------------
103 |
104 | # The theme to use for HTML and HTML Help pages. See the documentation for
105 | # a list of builtin themes.
106 | html_theme = 'default'
107 |
108 | # Theme options are theme-specific and customize the look and feel of a theme
109 | # further. For a list of options available for each theme, see the
110 | # documentation.
111 | #html_theme_options = {}
112 |
113 | # Add any paths that contain custom themes here, relative to this directory.
114 | #html_theme_path = []
115 |
116 | # The name for this set of Sphinx documents. If None, it defaults to
117 | # " v documentation".
118 | #html_title = None
119 |
120 | # A shorter title for the navigation bar. Default is the same as html_title.
121 | #html_short_title = None
122 |
123 | # The name of an image file (relative to this directory) to place at the top
124 | # of the sidebar.
125 | #html_logo = None
126 |
127 | # The name of an image file (within the static path) to use as favicon of the
128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
129 | # pixels large.
130 | #html_favicon = None
131 |
132 | # Add any paths that contain custom static files (such as style sheets) here,
133 | # relative to this directory. They are copied after the builtin static files,
134 | # so a file named "default.css" will overwrite the builtin "default.css".
135 | html_static_path = ['_static']
136 |
137 | # Add any extra paths that contain custom files (such as robots.txt or
138 | # .htaccess) here, relative to this directory. These files are copied
139 | # directly to the root of the documentation.
140 | #html_extra_path = []
141 |
142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
143 | # using the given strftime format.
144 | #html_last_updated_fmt = '%b %d, %Y'
145 |
146 | # If true, SmartyPants will be used to convert quotes and dashes to
147 | # typographically correct entities.
148 | #html_use_smartypants = True
149 |
150 | # Custom sidebar templates, maps document names to template names.
151 | #html_sidebars = {}
152 |
153 | # Additional templates that should be rendered to pages, maps page names to
154 | # template names.
155 | #html_additional_pages = {}
156 |
157 | # If false, no module index is generated.
158 | #html_domain_indices = True
159 |
160 | # If false, no index is generated.
161 | #html_use_index = True
162 |
163 | # If true, the index is split into individual pages for each letter.
164 | #html_split_index = False
165 |
166 | # If true, links to the reST sources are added to the pages.
167 | #html_show_sourcelink = True
168 |
169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
170 | #html_show_sphinx = True
171 |
172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
173 | #html_show_copyright = True
174 |
175 | # If true, an OpenSearch description file will be output, and all pages will
176 | # contain a tag referring to it. The value of this option must be the
177 | # base URL from which the finished HTML is served.
178 | #html_use_opensearch = ''
179 |
180 | # This is the file name suffix for HTML files (e.g. ".xhtml").
181 | #html_file_suffix = None
182 |
183 | # Output file base name for HTML help builder.
184 | htmlhelp_basename = 'python-ovh-doc'
185 |
186 |
187 | # -- Options for LaTeX output ---------------------------------------------
188 |
189 | latex_elements = {
190 | # The paper size ('letterpaper' or 'a4paper').
191 | #'papersize': 'letterpaper',
192 |
193 | # The font size ('10pt', '11pt' or '12pt').
194 | #'pointsize': '10pt',
195 |
196 | # Additional stuff for the LaTeX preamble.
197 | #'preamble': '',
198 | }
199 |
200 | # Grouping the document tree into LaTeX files. List of tuples
201 | # (source start file, target name, title,
202 | # author, documentclass [howto, manual, or own class]).
203 | latex_documents = [
204 | ('index', 'Python-OVH.tex', u'Python-OVH Documentation',
205 | u'Jean-Tiare Le Bigot', 'manual'),
206 | ]
207 |
208 | # The name of an image file (relative to this directory) to place at the top of
209 | # the title page.
210 | #latex_logo = None
211 |
212 | # For "manual" documents, if this is true, then toplevel headings are parts,
213 | # not chapters.
214 | #latex_use_parts = False
215 |
216 | # If true, show page references after internal links.
217 | #latex_show_pagerefs = False
218 |
219 | # If true, show URL addresses after external links.
220 | #latex_show_urls = False
221 |
222 | # Documents to append as an appendix to all manuals.
223 | #latex_appendices = []
224 |
225 | # If false, no module index is generated.
226 | #latex_domain_indices = True
227 |
228 |
229 | # -- Options for manual page output ---------------------------------------
230 |
231 | # One entry per manual page. List of tuples
232 | # (source start file, name, description, authors, manual section).
233 | man_pages = [
234 | ('index', 'python-ovh', u'Python-OVH Documentation',
235 | [u'Jean-Tiare Le Bigot'], 1)
236 | ]
237 |
238 | # If true, show URL addresses after external links.
239 | #man_show_urls = False
240 |
241 |
242 | # -- Options for Texinfo output -------------------------------------------
243 |
244 | # Grouping the document tree into Texinfo files. List of tuples
245 | # (source start file, target name, title, author,
246 | # dir menu entry, description, category)
247 | texinfo_documents = [
248 | ('index', 'Python-OVH', u'Python-OVH Documentation',
249 | u'Jean-Tiare Le Bigot', 'Python-OVH', 'OVH Rest API wrapper.',
250 | 'API'),
251 | ]
252 |
253 | # Documents to append as an appendix to all manuals.
254 | #texinfo_appendices = []
255 |
256 | # If false, no module index is generated.
257 | #texinfo_domain_indices = True
258 |
259 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
260 | #texinfo_show_urls = 'footnote'
261 |
262 | # If true, do not generate a @detailmenu in the "Top" node's menu.
263 | #texinfo_no_detailmenu = False
264 |
--------------------------------------------------------------------------------
/tests/test_client.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | import unittest
30 | import requests
31 | import mock
32 | import json
33 |
34 | try:
35 | from collections import OrderedDict
36 | except ImportError:
37 | # Python 2.6
38 | from ordereddict import OrderedDict
39 |
40 | from ovh.client import Client, ENDPOINTS
41 | from ovh.exceptions import (
42 | APIError, NetworkError, InvalidResponse, InvalidRegion, ReadOnlyError,
43 | ResourceNotFoundError, BadParametersError, ResourceConflictError, HTTPError,
44 | InvalidKey, InvalidCredential, NotGrantedCall, NotCredential, Forbidden,
45 | )
46 |
47 | M_ENVIRON = {
48 | 'OVH_ENDPOINT': 'runabove-ca',
49 | 'OVH_APPLICATION_KEY': 'application key from environ',
50 | 'OVH_APPLICATION_SECRET': 'application secret from environ',
51 | 'OVH_CONSUMER_KEY': 'consumer key from from environ',
52 | }
53 |
54 | APPLICATION_KEY = 'fake application key'
55 | APPLICATION_SECRET = 'fake application secret'
56 | CONSUMER_KEY = 'fake consumer key'
57 | ENDPOINT = 'ovh-eu'
58 | ENDPOINT_BAD = 'laponie'
59 | BASE_URL = 'https://eu.api.ovh.com/1.0'
60 | FAKE_URL = 'http://gopher.ovh.net/'
61 | FAKE_TIME = 1404395889.467238
62 |
63 | FAKE_METHOD = 'MeThOd'
64 | FAKE_PATH = '/unit/test'
65 |
66 | class testClient(unittest.TestCase):
67 | def setUp(self):
68 | self.time_patch = mock.patch('time.time', return_value=FAKE_TIME)
69 | self.time_patch.start()
70 |
71 | def tearDown(self):
72 | self.time_patch.stop()
73 |
74 | ## test helpers
75 |
76 | def test_init(self):
77 | # nominal
78 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
79 | self.assertEqual(APPLICATION_KEY, api._application_key)
80 | self.assertEqual(APPLICATION_SECRET, api._application_secret)
81 | self.assertEqual(CONSUMER_KEY, api._consumer_key)
82 | self.assertTrue(api._time_delta is None)
83 |
84 | # invalid region
85 | self.assertRaises(InvalidRegion, Client, ENDPOINT_BAD, '', '', '')
86 |
87 | def test_init_from_config(self):
88 | with mock.patch.dict('os.environ', M_ENVIRON):
89 | api = Client()
90 |
91 | self.assertEqual('https://api.runabove.com/1.0', api._endpoint)
92 | self.assertEqual(M_ENVIRON['OVH_APPLICATION_KEY'], api._application_key)
93 | self.assertEqual(M_ENVIRON['OVH_APPLICATION_SECRET'], api._application_secret)
94 | self.assertEqual(M_ENVIRON['OVH_CONSUMER_KEY'], api._consumer_key)
95 |
96 | @mock.patch.object(Client, 'call')
97 | def test_time_delta(self, m_call):
98 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
99 | m_call.return_value = 1404395895
100 | api._time_delta = None
101 |
102 | # nominal
103 | time_delta = api.time_delta
104 | m_call.assert_called_once_with('GET', '/auth/time', None, False)
105 | self.assertEqual(time_delta, 6)
106 | self.assertEqual(api._time_delta, 6)
107 |
108 | # ensure cache
109 | m_call.return_value = 0
110 | m_call.reset_mock()
111 | self.assertEqual(api.time_delta, 6)
112 | self.assertFalse(m_call.called)
113 |
114 | @mock.patch.object(Client, 'call')
115 | def test_request_consumerkey(self, m_call):
116 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
117 |
118 | # nominal
119 | FAKE_RULES = object()
120 | FAKE_CK = object()
121 | RET = {'consumerKey': FAKE_CK}
122 | m_call.return_value = RET
123 |
124 | ret = api.request_consumerkey(FAKE_RULES, FAKE_URL)
125 |
126 | self.assertEqual(RET, ret)
127 | m_call.assert_called_once_with('POST', '/auth/credential', {
128 | 'redirection': FAKE_URL,
129 | 'accessRules': FAKE_RULES,
130 | }, False)
131 |
132 | ## test wrappers
133 |
134 | def test__canonicalize_kwargs(self):
135 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
136 |
137 | self.assertEqual({}, api._canonicalize_kwargs({}))
138 | self.assertEqual({'from': 'value'}, api._canonicalize_kwargs({'from': 'value'}))
139 | self.assertEqual({'_to': 'value'}, api._canonicalize_kwargs({'_to': 'value'}))
140 | self.assertEqual({'from': 'value'}, api._canonicalize_kwargs({'_from': 'value'}))
141 |
142 | @mock.patch.object(Client, 'call')
143 | def test_get(self, m_call):
144 | # basic test
145 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
146 | self.assertEqual(m_call.return_value, api.get(FAKE_URL))
147 | m_call.assert_called_once_with('GET', FAKE_URL, None, True)
148 |
149 | # append query string
150 | m_call.reset_mock()
151 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
152 | self.assertEqual(m_call.return_value, api.get(FAKE_URL, param="test"))
153 | m_call.assert_called_once_with('GET', FAKE_URL+'?param=test', None, True)
154 |
155 | # append to existing query string
156 | m_call.reset_mock()
157 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
158 | self.assertEqual(m_call.return_value, api.get(FAKE_URL+'?query=string', param="test"))
159 | m_call.assert_called_once_with('GET', FAKE_URL+'?query=string¶m=test', None, True)
160 |
161 | # keyword calling convention
162 | m_call.reset_mock()
163 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
164 | self.assertEqual(m_call.return_value, api.get(FAKE_URL, _from="start", to="end"))
165 | try:
166 | m_call.assert_called_once_with('GET', FAKE_URL+'?to=end&from=start', None, True)
167 | except:
168 | m_call.assert_called_once_with('GET', FAKE_URL+'?from=start&to=end', None, True)
169 |
170 |
171 | @mock.patch.object(Client, 'call')
172 | def test_delete(self, m_call):
173 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
174 | self.assertEqual(m_call.return_value, api.delete(FAKE_URL))
175 | m_call.assert_called_once_with('DELETE', FAKE_URL, None, True)
176 |
177 | @mock.patch.object(Client, 'call')
178 | def test_post(self, m_call):
179 | PAYLOAD = {
180 | 'arg1': object(),
181 | 'arg2': object(),
182 | 'arg3': object(),
183 | }
184 |
185 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
186 | self.assertEqual(m_call.return_value, api.post(FAKE_URL, **PAYLOAD))
187 | m_call.assert_called_once_with('POST', FAKE_URL, PAYLOAD, True)
188 |
189 | @mock.patch.object(Client, 'call')
190 | def test_put(self, m_call):
191 | PAYLOAD = {
192 | 'arg1': object(),
193 | 'arg2': object(),
194 | 'arg3': object(),
195 | }
196 |
197 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
198 | self.assertEqual(m_call.return_value, api.put(FAKE_URL, **PAYLOAD))
199 | m_call.assert_called_once_with('PUT', FAKE_URL, PAYLOAD, True)
200 |
201 | ## test core function
202 |
203 | @mock.patch('ovh.client.Session.request')
204 | def test_call_no_sign(self, m_req):
205 | m_res = m_req.return_value
206 | m_json = m_res.json.return_value
207 |
208 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET)
209 |
210 | # nominal
211 | m_res.status_code = 200
212 | self.assertEqual(m_json, api.call(FAKE_METHOD, FAKE_PATH, None, False))
213 | m_req.assert_called_once_with(
214 | FAKE_METHOD, BASE_URL+'/unit/test',
215 | headers={'X-Ovh-Application': APPLICATION_KEY}, data=''
216 | )
217 | m_req.reset_mock()
218 |
219 | # data, nominal
220 | m_res.status_code = 200
221 | data = {'key': 'value'}
222 | j_data = json.dumps(data)
223 | self.assertEqual(m_json, api.call(FAKE_METHOD, FAKE_PATH, data, False))
224 | m_req.assert_called_once_with(
225 | FAKE_METHOD, BASE_URL+'/unit/test',
226 | headers={
227 | 'X-Ovh-Application': APPLICATION_KEY,
228 | 'Content-type': 'application/json',
229 | }, data=j_data
230 | )
231 | m_req.reset_mock()
232 |
233 | # request fails, somehow
234 | m_req.side_effect = requests.RequestException
235 | self.assertRaises(HTTPError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
236 | m_req.side_effect = None
237 |
238 | # response decoding fails
239 | m_res.json.side_effect = ValueError
240 | self.assertRaises(InvalidResponse, api.call, FAKE_METHOD, FAKE_PATH, None, False)
241 | m_res.json.side_effect = None
242 |
243 | # HTTP errors
244 | m_res.status_code = 404
245 | self.assertRaises(ResourceNotFoundError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
246 | m_res.status_code = 403
247 | m_res.json.return_value = {'errorCode': "NOT_GRANTED_CALL"}
248 | self.assertRaises(NotGrantedCall, api.call, FAKE_METHOD, FAKE_PATH, None, False)
249 | m_res.status_code = 403
250 | m_res.json.return_value = {'errorCode': "NOT_CREDENTIAL"}
251 | self.assertRaises(NotCredential, api.call, FAKE_METHOD, FAKE_PATH, None, False)
252 | m_res.status_code = 403
253 | m_res.json.return_value = {'errorCode': "INVALID_KEY"}
254 | self.assertRaises(InvalidKey, api.call, FAKE_METHOD, FAKE_PATH, None, False)
255 | m_res.status_code = 403
256 | m_res.json.return_value = {'errorCode': "INVALID_CREDENTIAL"}
257 | self.assertRaises(InvalidCredential, api.call, FAKE_METHOD, FAKE_PATH, None, False)
258 | m_res.status_code = 403
259 | m_res.json.return_value = {'errorCode': "FORBIDDEN"}
260 | self.assertRaises(Forbidden, api.call, FAKE_METHOD, FAKE_PATH, None, False)
261 | m_res.status_code = 400
262 | self.assertRaises(BadParametersError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
263 | m_res.status_code = 409
264 | self.assertRaises(ResourceConflictError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
265 | m_res.status_code = 0
266 | self.assertRaises(NetworkError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
267 | m_res.status_code = 99
268 | self.assertRaises(APIError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
269 | m_res.status_code = 306
270 | self.assertRaises(APIError, api.call, FAKE_METHOD, FAKE_PATH, None, False)
271 |
272 | @mock.patch('ovh.client.Session.request')
273 | @mock.patch('ovh.client.Client.time_delta', new_callable=mock.PropertyMock)
274 | def test_call_signature(self, m_time_delta, m_req):
275 | m_res = m_req.return_value
276 | m_json = m_res.json.return_value
277 | m_time_delta.return_value = 42
278 |
279 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)
280 |
281 | # nominal
282 | m_res.status_code = 200
283 | self.assertEqual(m_json, api.call(FAKE_METHOD, FAKE_PATH, None, True))
284 | m_time_delta.assert_called_once_with()
285 | m_req.assert_called_once_with(
286 | FAKE_METHOD, BASE_URL+'/unit/test',
287 | headers={
288 | 'X-Ovh-Consumer': CONSUMER_KEY,
289 | 'X-Ovh-Application': APPLICATION_KEY,
290 | 'X-Ovh-Signature': '$1$16ae5ba8c63841b1951575be905867991d5f49dc',
291 | 'X-Ovh-Timestamp': '1404395931',
292 | }, data=''
293 | )
294 | m_time_delta.reset_mock()
295 | m_req.reset_mock()
296 |
297 |
298 | # data, nominal
299 | data = OrderedDict([('some', 'random'), ('data', 42)])
300 | m_res.status_code = 200
301 | self.assertEqual(m_json, api.call(FAKE_METHOD, FAKE_PATH, data, True))
302 | m_time_delta.assert_called_once_with()
303 | m_req.assert_called_once_with(
304 | FAKE_METHOD, BASE_URL+'/unit/test',
305 | headers={
306 | 'X-Ovh-Consumer': CONSUMER_KEY,
307 | 'X-Ovh-Application': APPLICATION_KEY,
308 | 'Content-type': 'application/json',
309 | 'X-Ovh-Timestamp': '1404395931',
310 | 'X-Ovh-Signature': '$1$9acb1ac0120006d16261a635aed788e83ab172d2',
311 | }, data=json.dumps(data)
312 | )
313 | m_time_delta.reset_mock()
314 | m_req.reset_mock()
315 |
316 | # Overwrite configuration to avoid interfering with any local config
317 | from ovh.client import config
318 | try:
319 | from ConfigParser import RawConfigParser
320 | except ImportError:
321 | # Python 3
322 | from configparser import RawConfigParser
323 |
324 | self._orig_config = config.config
325 | config.config = RawConfigParser()
326 |
327 | # errors
328 | try:
329 | api = Client(ENDPOINT, APPLICATION_KEY, None, CONSUMER_KEY)
330 | self.assertRaises(InvalidKey, api.call, FAKE_METHOD, FAKE_PATH, None, True)
331 | api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, None)
332 | self.assertRaises(InvalidKey, api.call, FAKE_METHOD, FAKE_PATH, None, True)
333 | finally:
334 | # Restore configuration
335 | config.config = self._orig_config
336 |
337 | # Perform real API tests.
338 | def test_endpoints(self):
339 | for endpoint in ENDPOINTS.keys():
340 | auth_time = Client(endpoint).get('/auth/time', _need_auth=False)
341 | self.assertTrue(auth_time > 0)
342 |
343 |
--------------------------------------------------------------------------------
/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 | # -*- encoding: utf-8 -*-
15 |
16 | import ovh
17 |
18 | # Instantiate. Visit https://api.ovh.com/createToken/index.cgi?GET=/me
19 | # to get your credentials
20 | client = ovh.Client(
21 | endpoint='ovh-eu',
22 | application_key='',
23 | application_secret='',
24 | consumer_key='',
25 | )
26 |
27 | # Print nice welcome message
28 | print "Welcome", client.get('/me')['firstname']
29 |
30 | Installation
31 | ============
32 |
33 | The easiest way to get the latest stable release is to grab it from `pypi
34 | `_ using ``pip``.
35 |
36 | .. code:: bash
37 |
38 | pip install ovh
39 |
40 | Alternatively, you may get latest development version directly from Git.
41 |
42 | .. code:: bash
43 |
44 | pip install -e git+https://github.com/ovh/python-ovh.git#egg=ovh
45 |
46 | API Documentation
47 | =================
48 |
49 | .. toctree::
50 | :maxdepth: 2
51 | :glob:
52 |
53 | api/ovh/*
54 |
55 | Example Usage
56 | =============
57 |
58 | Use the API on behalf of a user
59 | -------------------------------
60 |
61 | 1. Create an application
62 | ************************
63 |
64 | To interact with the APIs, the SDK needs to identify itself using an
65 | ``application_key`` and an ``application_secret``. To get them, you need
66 | to register your application. Depending the API you plan yo use, visit:
67 |
68 | - `OVH Europe `_
69 | - `OVH North-America `_
70 | - `RunAbove `_
71 |
72 | Once created, you will obtain an **application key (AK)** and an **application
73 | secret (AS)**.
74 |
75 | 2. Configure your application
76 | *****************************
77 |
78 | The easiest and safest way to use your application's credentials is create an
79 | ``ovh.conf`` configuration file in application's working directory. Here is how
80 | it looks like:
81 |
82 | .. code:: ini
83 |
84 | [default]
85 | ; general configuration: default endpoint
86 | endpoint=ovh-eu
87 |
88 | [ovh-eu]
89 | ; configuration specific to 'ovh-eu' endpoint
90 | application_key=my_app_key
91 | application_secret=my_application_secret
92 | ; uncomment following line when writing a script application
93 | ; with a single consumer key.
94 | ;consumer_key=my_consumer_key
95 |
96 | Depending on the API you want to use, you may set the ``endpoint`` to:
97 |
98 | * ``ovh-eu`` for OVH Europe API
99 | * ``ovh-ca`` for OVH North-America API
100 | * ``runabove-ca`` for RunAbove API
101 |
102 | See Configuration_ for more inforamtions on available configuration mechanisms.
103 |
104 | .. note:: When using a versioning system, make sure to add ``ovh.conf`` to ignored
105 | files. It contains confidential/security-sensitive informations!
106 |
107 | 3. Authorize your application to access a customer account
108 | **********************************************************
109 |
110 | To allow your application to access a customer account using the API on your
111 | behalf, you need a **consumer key (CK)**.
112 |
113 | .. code:: python
114 |
115 | # -*- encoding: utf-8 -*-
116 |
117 | import ovh
118 |
119 | # create a client using configuration
120 | client = ovh.Client()
121 |
122 | # Request RO, /me API access
123 | access_rules = [
124 | {'method': 'GET', 'path': '/me'},
125 | ]
126 |
127 | # Request token
128 | validation = client.request_consumerkey(access_rules)
129 |
130 | print "Please visit %s to authenticate" % validation['validationUrl']
131 | raw_input("and press Enter to continue...")
132 |
133 | # Print nice welcome message
134 | print "Welcome", client.get('/me')['firstname']
135 | print "Btw, your 'consumerKey' is '%s'" % validation['consumerKey']
136 |
137 |
138 | Returned ``consumerKey`` should then be kept to avoid re-authenticating your
139 | end-user on each use.
140 |
141 | .. note:: To request full and unlimited access to the API, you may use wildcards:
142 |
143 | .. code:: python
144 |
145 | access_rules = [
146 | {'method': 'GET', 'path': '/*'},
147 | {'method': 'POST', 'path': '/*'},
148 | {'method': 'PUT', 'path': '/*'},
149 | {'method': 'DELETE', 'path': '/*'}
150 | ]
151 |
152 | Install a new mail redirection
153 | ------------------------------
154 |
155 | e-mail redirections may be freely configured on domains and DNS zones hosted by
156 | OVH to an arbitrary destination e-mail using API call
157 | ``POST /email/domain/{domain}/redirection``.
158 |
159 | For this call, the api specifies that the source adress shall be given under the
160 | ``from`` keyword. Which is a problem as this is also a reserved Python keyword.
161 | In this case, simply prefix it with a '_', the wrapper will automatically detect
162 | it as being a prefixed reserved keyword and will subsitute it. Such aliasing
163 | is only supported with reserved keywords.
164 |
165 | .. code:: python
166 |
167 | # -*- encoding: utf-8 -*-
168 |
169 | import ovh
170 |
171 | DOMAIN = "example.com"
172 | SOURCE = "sales@example.com"
173 | DESTINATION = "contact@example.com"
174 |
175 | # create a client
176 | client = ovh.Client()
177 |
178 | # Create a new alias
179 | client.post('/email/domain/%s/redirection' % DOMAIN,
180 | _from=SOURCE,
181 | to=DESTINATION
182 | localCopy=False
183 | )
184 | print "Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION)
185 |
186 | Grab bill list
187 | --------------
188 |
189 | Let's say you want to integrate OVH bills into your own billing system, you
190 | could just script around the ``/me/bills`` endpoints and even get the details
191 | of each bill lines using ``/me/bill/{billId}/details/{billDetailId}``.
192 |
193 | This example assumes an existing Configuration_ with valid ``application_key``,
194 | ``application_secret`` and ``consumer_key``.
195 |
196 | .. code:: python
197 |
198 | # -*- encoding: utf-8 -*-
199 |
200 | import ovh
201 |
202 | # create a client without a consumerKey
203 | client = ovh.Client()
204 |
205 | # Grab bill list
206 | bills = client.get('/me/bill')
207 | for bill in bills:
208 | details = client.get('/me/bill/%s' % bill)
209 | print "%12s (%s): %10s --> %s" % (
210 | bill,
211 | details['date'],
212 | details['priceWithTax']['text'],
213 | details['pdfUrl'],
214 | )
215 |
216 | Enable network burst in SBG1
217 | ----------------------------
218 |
219 | 'Network burst' is a free service but is opt-in. What if you have, say, 10
220 | servers in ``SBG-1`` datacenter? You certainely don't want to activate it
221 | manually for each servers. You could take advantage of a code like this.
222 |
223 | This example assumes an existing Configuration_ with valid ``application_key``,
224 | ``application_secret`` and ``consumer_key``.
225 |
226 | .. code:: python
227 |
228 | # -*- encoding: utf-8 -*-
229 |
230 | import ovh
231 |
232 | # create a client
233 | client = ovh.Client()
234 |
235 | # get list of all server names
236 | servers = client.get('/dedicated/server/')
237 |
238 | # find all servers in SBG-1 datacenter
239 | for server in servers:
240 | details = client.get('/dedicated/server/%s' % server)
241 | if details['datacenter'] == 'sbg1':
242 | # enable burst on server
243 | client.put('/dedicated/server/%s/burst' % server, status='active')
244 | print "Enabled burst for %s server located in SBG-1" % server
245 |
246 | List application authorized to access your account
247 | --------------------------------------------------
248 |
249 | Thanks to the application key / consumer key mechanism, it is possible to
250 | finely track applications having access to your data and revoke this access.
251 | This examples lists validated applications. It could easily be adapted to
252 | manage revocation too.
253 |
254 | This example assumes an existing Configuration_ with valid ``application_key``,
255 | ``application_secret`` and ``consumer_key``.
256 |
257 | .. code:: python
258 |
259 | # -*- encoding: utf-8 -*-
260 |
261 | import ovh
262 | from tabulate import tabulate
263 |
264 | # create a client
265 | client = ovh.Client()
266 |
267 | credentials = client.get('/me/api/credential', status='validated')
268 |
269 | # pretty print credentials status
270 | table = []
271 | for credential_id in credentials:
272 | credential_method = '/me/api/credential/'+str(credential_id)
273 | credential = client.get(credential_method)
274 | application = client.get(credential_method+'/application')
275 |
276 | table.append([
277 | credential_id,
278 | '[%s] %s' % (application['status'], application['name']),
279 | application['description'],
280 | credential['creation'],
281 | credential['expiration'],
282 | credential['lastUse'],
283 | ])
284 | print tabulate(table, headers=['ID', 'App Name', 'Description',
285 | 'Token Creation', 'Token Expiration', 'Token Last Use'])
286 |
287 | Before running this example, make sure you have the
288 | `tabulate `_ library installed. It's a
289 | pretty cool library to pretty print tabular data in a clean and easy way.
290 |
291 | >>> pip install tabulate
292 |
293 | List Runabove's instance
294 | ------------------------
295 |
296 | This example assumes an existing Configuration_ with valid ``application_key``,
297 | ``application_secret`` and ``consumer_key``.
298 |
299 | .. code:: python
300 |
301 | # -*- encoding: utf-8 -*-
302 |
303 | import ovh
304 | from tabulate import tabulate
305 |
306 | # visit https://api.runabove.com/createApp/ to create your application's credentials
307 | client = ovh.Client(endpoint='runabove-ca')
308 |
309 | # get list of all instances
310 | instances = client.get('/instance')
311 |
312 | # pretty print instances status
313 | table = []
314 | for instance in instances:
315 | table.append([
316 | instance['name'],
317 | instance['ip'],
318 | instance['region'],
319 | instance['status'],
320 | ])
321 | print tabulate(table, headers=['Name', 'IP', 'Region', 'Status'])
322 |
323 | Before running this example, make sure you have the
324 | `tabulate `_ library installed. It's a
325 | pretty cool library to pretty print tabular data in a clean and easy way.
326 |
327 | >>> pip install tabulate
328 |
329 | Configuration
330 | =============
331 |
332 | The straightforward way to use OVH's API keys is to embed them directly in the
333 | application code. While this is very convenient, it lacks of elegance and
334 | flexibility.
335 |
336 | Alternatively it is suggested to use configuration files or environment
337 | variables so that the same code may run seamlessly in multiple environments.
338 | Production and development for instance.
339 |
340 | This wrapper will first look for direct instanciation parameters then
341 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and
342 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not
343 | provided, it will look for a configuration file of the form:
344 |
345 | .. code:: ini
346 |
347 | [default]
348 | ; general configuration: default endpoint
349 | endpoint=ovh-eu
350 |
351 | [ovh-eu]
352 | ; configuration specific to 'ovh-eu' endpoint
353 | application_key=my_app_key
354 | application_secret=my_application_secret
355 | consumer_key=my_consumer_key
356 |
357 | The client will successively attempt to locate this configuration file in
358 |
359 | 1. Current working directory: ``./ovh.conf``
360 | 2. Current user's home directory ``~/.ovh.conf``
361 | 3. System wide configuration ``/etc/ovh.conf``
362 |
363 | This lookup mechanism makes it easy to overload credentials for a specific
364 | project or user.
365 |
366 | Passing parameters
367 | =============
368 |
369 | You can call all the methods of the API with the necessary arguments.
370 |
371 | If an API needs an argument colliding with a Python reserved keyword, it
372 | can be prefixed with an underscore. For example, ``from`` argument of
373 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``.
374 |
375 | With characters invalid in python argument name like a dot, you can:
376 |
377 | .. code:: python
378 |
379 | # -*- encoding: utf-8 -*-
380 |
381 | import ovh
382 |
383 | params = {}
384 | params['date.from'] = '2014-01-01'
385 | params['date.to'] = '2015-01-01'
386 |
387 | # create a client
388 | client = ovh.Client()
389 |
390 | # pass parameters using **
391 | client.post('/me/bills', **params)
392 |
393 | Hacking
394 | =======
395 |
396 | This wrapper uses standard Python tools, so you should feel at home with it.
397 | Here is a quick outline of what it may look like. A good practice is to run
398 | this from a ``virtualenv``.
399 |
400 | Get the sources
401 | ---------------
402 |
403 | .. code:: bash
404 |
405 | git clone https://github.com/ovh/python-ovh.git
406 | cd python-ovh
407 | python setup.py develop
408 |
409 | You've developed a new cool feature ? Fixed an annoying bug ? We'd be happy
410 | to hear from you !
411 |
412 | Run the tests
413 | -------------
414 |
415 | Simply run ``nosetests``. It will automatically load its configuration from
416 | ``setup.cfg`` and output full coverage status. Since we all love quality, please
417 | note that we do not accept contributions with test coverage under 100%.
418 |
419 | .. code:: bash
420 |
421 | pip install -r requirements-dev.txt
422 | nosetests # 100% coverage is a hard minimum
423 |
424 |
425 | Build the documentation
426 | -----------------------
427 |
428 | Documentation is managed using the excellent ``Sphinx`` system. For example, to
429 | build HTML documentation:
430 |
431 | .. code:: bash
432 |
433 | cd python-ovh/docs
434 | make html
435 |
436 | Supported APIs
437 | ==============
438 |
439 | OVH Europe
440 | ----------
441 |
442 | - **Documentation**: https://eu.api.ovh.com/
443 | - **Community support**: api-subscribe@ml.ovh.net
444 | - **Console**: https://eu.api.ovh.com/console
445 | - **Create application credentials**: https://eu.api.ovh.com/createApp/
446 |
447 | OVH North America
448 | -----------------
449 |
450 | - **Documentation**: https://ca.api.ovh.com/
451 | - **Community support**: api-subscribe@ml.ovh.net
452 | - **Console**: https://ca.api.ovh.com/console
453 | - **Create application credentials**: https://ca.api.ovh.com/createApp/
454 |
455 | Runabove
456 | --------
457 |
458 | - **console**: https://api.runabove.com/console/
459 | - **get application credentials**: https://api.runabove.com/createApp/
460 | - **high level SDK**: https://github.com/runabove/python-runabove
461 |
462 | Related links
463 | =============
464 |
465 | - **contribute**: https://github.com/ovh/python-ovh
466 | - **Report bugs**: https://github.com/ovh/python-ovh/issues
467 | - **Download**: http://pypi.python.org/pypi/ovh
468 |
--------------------------------------------------------------------------------
/ovh/client.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | #
3 | # Copyright (c) 2013-2014, OVH SAS.
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions are met:
8 | #
9 | # * Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | # * Redistributions in binary form must reproduce the above copyright
12 | # notice, this list of conditions and the following disclaimer in the
13 | # documentation and/or other materials provided with the distribution.
14 | # * Neither the name of OVH SAS nor the
15 | # names of its contributors may be used to endorse or promote products
16 | # derived from this software without specific prior written permission.
17 | #
18 | # THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | # DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | """
30 | This module provides a simple python wrapper over the OVH REST API.
31 | It handles requesting credential, signing queries...
32 |
33 | - To get your API keys: https://eu.api.ovh.com/createApp/
34 | - To get started with API: https://api.ovh.com/g934.first_step_with_api
35 | """
36 |
37 | import hashlib
38 | import urllib
39 | import keyword
40 | import time
41 | import json
42 |
43 | try:
44 | from urllib import urlencode
45 | except ImportError: # pragma: no cover
46 | # Python 3
47 | from urllib.parse import urlencode
48 |
49 | from requests import request, Session
50 | from requests.exceptions import RequestException
51 |
52 | from .config import config
53 | from .exceptions import (
54 | APIError, NetworkError, InvalidResponse, InvalidRegion, InvalidKey,
55 | ResourceNotFoundError, BadParametersError, ResourceConflictError, HTTPError,
56 | NotGrantedCall, NotCredential, Forbidden, InvalidCredential,
57 | )
58 |
59 | #: Mapping between OVH API region names and corresponding endpoints
60 | ENDPOINTS = {
61 | 'ovh-eu': 'https://eu.api.ovh.com/1.0',
62 | 'ovh-ca': 'https://ca.api.ovh.com/1.0',
63 | 'kimsufi-eu': 'https://eu.api.kimsufi.com/1.0',
64 | 'kimsufi-ca': 'https://ca.api.kimsufi.com/1.0',
65 | 'soyoustart-eu': 'https://eu.api.soyoustart.com/1.0',
66 | 'soyoustart-ca': 'https://ca.api.soyoustart.com/1.0',
67 | 'runabove-ca': 'https://api.runabove.com/1.0',
68 | }
69 |
70 |
71 | class Client(object):
72 | """
73 | Low level OVH Client. It abstracts all the authentication and request
74 | signing logic along with some nice tools helping with key generation.
75 |
76 | All low level request logic including signing and error handling takes place
77 | in :py:func:`Client.call` function. Convenient wrappers
78 | :py:func:`Client.get` :py:func:`Client.post`, :py:func:`Client.put`,
79 | :py:func:`Client.delete` should be used instead. :py:func:`Client.post`,
80 | :py:func:`Client.put` both accept arbitrary list of keyword arguments
81 | mapped to ``data`` param of :py:func:`Client.call`.
82 |
83 | Example usage:
84 |
85 | .. code:: python
86 |
87 | from ovh import Client, APIError
88 |
89 | REGION = 'ovh-eu'
90 | APP_KEY=""
91 | APP_SECRET=""
92 | CONSUMER_KEY=""
93 |
94 | client = Client(REGION, APP_KEY, APP_SECRET, CONSUMER_KEY)
95 |
96 | try:
97 | print client.get('/me')
98 | except APIError as e:
99 | print "Ooops, failed to get my info:", e.msg
100 |
101 | """
102 |
103 | def __init__(self, endpoint=None, application_key=None,
104 | application_secret=None, consumer_key=None):
105 | """
106 | Creates a new Client. No credential check is done at this point.
107 |
108 | The ``application_key`` identifies your application while
109 | ``application_secret`` authenticates it. On the other hand, the
110 | ``consumer_key`` uniquely identifies your application's end user without
111 | requiring his personal password.
112 |
113 | If any of ``endpoint``, ``application_key``, ``application_secret``
114 | or ``consumer_key`` is not provided, this client will attempt to locate
115 | from them from environment, ~/.ovh.cfg or /etc/ovh.cfg.
116 |
117 | See :py:mod:`ovh.config` for more informations on supported
118 | configuration mechanisms.
119 |
120 | :param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS``
121 | :param str application_key: Application key as provided by OVH
122 | :param str application_secret: Application secret key as provided by OVH
123 | :param str consumer_key: uniquely identifies
124 | :raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``.
125 | """
126 | # load endpoint
127 | if endpoint is None:
128 | endpoint = config.get('default', 'endpoint')
129 |
130 | try:
131 | self._endpoint = ENDPOINTS[endpoint]
132 | except KeyError:
133 | raise InvalidRegion("Unknow endpoint %s. Valid endpoints: %s",
134 | endpoint, ENDPOINTS.keys())
135 |
136 | # load keys
137 | if application_key is None:
138 | application_key = config.get(endpoint, 'application_key')
139 | self._application_key = application_key
140 |
141 | if application_secret is None:
142 | application_secret = config.get(endpoint, 'application_secret')
143 | self._application_secret = application_secret
144 |
145 | if consumer_key is None:
146 | consumer_key = config.get(endpoint, 'consumer_key')
147 | self._consumer_key = consumer_key
148 |
149 | # lazy load time delta
150 | self._time_delta = None
151 |
152 | # use a requests session to reuse HTTPS connections between requests
153 | self._session = Session()
154 |
155 | ## high level API
156 |
157 | @property
158 | def time_delta(self):
159 | """
160 | Request signatures are valid only for a short amount of time to mitigate
161 | risk of attack replay scenarii which requires to use a common time
162 | reference. This function queries endpoint's time and computes the delta.
163 | This entrypoint does not require authentication.
164 |
165 | This method is *lazy*. It will only load it once even though it is used
166 | for each request.
167 |
168 | .. note:: You should not need to use this property directly
169 |
170 | :returns: time distance between local and server time in seconds.
171 | :rtype: int
172 | """
173 | if self._time_delta is None:
174 | server_time = self.get('/auth/time', _need_auth=False)
175 | self._time_delta = server_time - int(time.time())
176 | return self._time_delta
177 |
178 |
179 | def request_consumerkey(self, access_rules, redirect_url=None):
180 | """
181 | Create a new "consumer key" identifying this application's end user. API
182 | will return a ``consumerKey`` and a ``validationUrl``. The end user must
183 | visit the ``validationUrl``, authenticate and validate the requested
184 | ``access_rules`` to link his account to the ``consumerKey``. Once this
185 | is done, he may optionaly be redirected to ``redirect_url`` and the
186 | application can start using the ``consumerKey``.
187 |
188 | The new ``consumerKey`` is automatically loaded into
189 | ``self._consumer_key`` and is ready to used as soon as validated.
190 |
191 | As signing requires a valid ``consumerKey``, the method does not require
192 | authentication, only a valid ``applicationKey``
193 |
194 | ``access_rules`` is a list of the form:
195 |
196 | .. code:: python
197 |
198 | # Grant full, unrestricted API access
199 | access_rules = [
200 | {'method': 'GET', 'path': '/*'},
201 | {'method': 'POST', 'path': '/*'},
202 | {'method': 'PUT', 'path': '/*'},
203 | {'method': 'DELETE', 'path': '/*'}
204 | ]
205 |
206 | To request a new consumer key, you may use a code like:
207 |
208 | .. code:: python
209 |
210 | # Request RO, /me API access
211 | access_rules = [
212 | {'method': 'GET', 'path': '/me'},
213 | ]
214 |
215 | # Request token
216 | validation = client.request_consumerkey(access_rules)
217 |
218 | print "Please visit", validation['validationUrl'], "to authenticate"
219 | raw_input("and press Enter to continue...")
220 |
221 | # Print nice welcome message
222 | print "Welcome", client.get('/me')['firstname']
223 |
224 |
225 | :param list access_rules: Mapping specifying requested privileges.
226 | :param str redirect_url: Where to redirect end user upon validation.
227 | :raises APIError: When ``self.call`` fails.
228 | :returns: dict with ``consumerKey`` and ``validationUrl`` keys
229 | :rtype: dict
230 | """
231 | res = self.post('/auth/credential', _need_auth=False,
232 | accessRules=access_rules, redirection=redirect_url)
233 | self._consumer_key = res['consumerKey']
234 | return res
235 |
236 | ## API shortcuts
237 |
238 | def _canonicalize_kwargs(self, kwargs):
239 | """
240 | If an API needs an argument colliding with a Python reserved keyword, it
241 | can be prefixed with an underscore. For example, ``from`` argument of
242 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``
243 |
244 | :param dict kwargs: input kwargs
245 | :return dict: filtered kawrgs
246 | """
247 | arguments = {}
248 |
249 | for k, v in kwargs.items():
250 | if k[0] == '_' and k[1:] in keyword.kwlist:
251 | k = k[1:]
252 | arguments[k] = v
253 |
254 | return arguments
255 |
256 | def get(self, _target, _need_auth=True, **kwargs):
257 | """
258 | 'GET' :py:func:`Client.call` wrapper.
259 |
260 | Query string parameters can be set either directly in ``_target`` or as
261 | keywork arguments. If an argument collides with a Python reserved
262 | keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``.
263 |
264 | :param string _target: API method to call
265 | :param string _need_auth: If True, send authentication headers. This is
266 | the default
267 | """
268 | if kwargs:
269 | kwargs = self._canonicalize_kwargs(kwargs)
270 | query_string = urlencode(kwargs)
271 | if '?' in _target:
272 | _target = '%s&%s' % (_target, query_string)
273 | else:
274 | _target = '%s?%s' % (_target, query_string)
275 |
276 | return self.call('GET', _target, None, _need_auth)
277 |
278 | def put(self, _target, _need_auth=True, **kwargs):
279 | """
280 | 'PUT' :py:func:`Client.call` wrapper
281 |
282 | Body parameters can be set either directly in ``_target`` or as keywork
283 | arguments. If an argument collides with a Python reserved keyword,
284 | prefix it with a '_'. For instance, ``from`` becomes ``_from``.
285 |
286 | :param string _target: API method to call
287 | :param string _need_auth: If True, send authentication headers. This is
288 | the default
289 | """
290 | kwargs = self._canonicalize_kwargs(kwargs)
291 | return self.call('PUT', _target, kwargs, _need_auth)
292 |
293 | def post(self, _target, _need_auth=True, **kwargs):
294 | """
295 | 'POST' :py:func:`Client.call` wrapper
296 |
297 | Body parameters can be set either directly in ``_target`` or as keywork
298 | arguments. If an argument collides with a Python reserved keyword,
299 | prefix it with a '_'. For instance, ``from`` becomes ``_from``.
300 |
301 | :param string _target: API method to call
302 | :param string _need_auth: If True, send authentication headers. This is
303 | the default
304 | """
305 | kwargs = self._canonicalize_kwargs(kwargs)
306 | return self.call('POST', _target, kwargs, _need_auth)
307 |
308 | def delete(self, _target, _need_auth=True):
309 | """
310 | 'DELETE' :py:func:`Client.call` wrapper
311 |
312 | :param string _target: API method to call
313 | :param string _need_auth: If True, send authentication headers. This is
314 | the default
315 | """
316 | return self.call('DELETE', _target, None, _need_auth)
317 |
318 | ## low level helpers
319 |
320 | def call(self, method, path, data=None, need_auth=True):
321 | """
322 | Lowest level call helper. If ``consumer_key`` is not ``None``, inject
323 | authentication headers and sign the request.
324 |
325 | Request signature is a sha1 hash on following fields, joined by '+'
326 | - application_secret
327 | - consumer_key
328 | - METHOD
329 | - full request url
330 | - body
331 | - server current time (takes time delta into account)
332 |
333 | :param str method: HTTP verb. Usualy one of GET, POST, PUT, DELETE
334 | :param str path: api entrypoint to call, relative to endpoint base path
335 | :param data: any json serializable data to send as request's body
336 | :param boolean need_auth: if False, bypass signature
337 | :raises HTTPError: when underlying request failed for network reason
338 | :raises InvalidResponse: when API response could not be decoded
339 | """
340 | body = ''
341 | target = self._endpoint + path
342 | headers = {
343 | 'X-Ovh-Application': self._application_key
344 | }
345 |
346 | # include payload
347 | if data is not None:
348 | headers['Content-type'] = 'application/json'
349 | body = json.dumps(data)
350 |
351 | # sign request. Never sign 'time' or will recuse infinitely
352 | if need_auth:
353 | if not self._application_secret:
354 | raise InvalidKey("Invalid ApplicationSecret '%s'" %
355 | self._application_secret)
356 |
357 | if not self._consumer_key:
358 | raise InvalidKey("Invalid ConsumerKey '%s'" %
359 | self._consumer_key)
360 |
361 | now = str(int(time.time()) + self.time_delta)
362 | signature = hashlib.sha1()
363 | signature.update("+".join([
364 | self._application_secret, self._consumer_key,
365 | method.upper(), target,
366 | body,
367 | now
368 | ]).encode('utf-8'))
369 |
370 | headers['X-Ovh-Consumer'] = self._consumer_key
371 | headers['X-Ovh-Timestamp'] = now
372 | headers['X-Ovh-Signature'] = "$1$" + signature.hexdigest()
373 |
374 | # attempt request
375 | try:
376 | result = self._session.request(method, target, headers=headers,
377 | data=body)
378 | except RequestException as error:
379 | raise HTTPError("Low HTTP request failed error", error)
380 |
381 | status = result.status_code
382 |
383 | # attempt to decode and return the response
384 | try:
385 | json_result = result.json()
386 | except ValueError as error:
387 | raise InvalidResponse("Failed to decode API response", error)
388 |
389 | # error check
390 | if status >= 100 and status < 300:
391 | return json_result
392 | elif status == 403 and json_result.get('errorCode') == 'NOT_GRANTED_CALL':
393 | raise NotGrantedCall(json_result.get('message'))
394 | elif status == 403 and json_result.get('errorCode') == 'NOT_CREDENTIAL':
395 | raise NotCredential(json_result.get('message'))
396 | elif status == 403 and json_result.get('errorCode') == 'INVALID_KEY':
397 | raise InvalidKey(json_result.get('message'))
398 | elif status == 403 and json_result.get('errorCode') == 'INVALID_CREDENTIAL':
399 | raise InvalidCredential(json_result.get('message'))
400 | elif status == 403 and json_result.get('errorCode') == 'FORBIDDEN':
401 | raise Forbidden(json_result.get('message'))
402 | elif status == 404:
403 | raise ResourceNotFoundError(json_result.get('message'))
404 | elif status == 400:
405 | raise BadParametersError(json_result.get('message'))
406 | elif status == 409:
407 | raise ResourceConflictError(json_result.get('message'))
408 | elif status == 0:
409 | raise NetworkError()
410 | else:
411 | raise APIError(json_result.get('message'))
412 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://github.com/ovh/python-ovh/raw/master/docs/img/logo.png
2 | :alt: Python & OVH APIs
3 | :target: https://pypi.python.org/pypi/ovh
4 |
5 | Lightweight wrapper around OVH'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://travis-ci.org/ovh/python-ovh.svg?branch=master
12 | :alt: Build Status
13 | :target: https://travis-ci.org/ovh/python-ovh
14 | .. image:: https://coveralls.io/repos/ovh/python-ovh/badge.png
15 | :alt: Coverage Status
16 | :target: https://coveralls.io/r/ovh/python-ovh
17 |
18 | .. code:: python
19 |
20 | # -*- encoding: utf-8 -*-
21 |
22 | import ovh
23 |
24 | # Instantiate. Visit https://api.ovh.com/createToken/index.cgi?GET=/me
25 | # to get your credentials
26 | client = ovh.Client(
27 | endpoint='ovh-eu',
28 | application_key='',
29 | application_secret='',
30 | consumer_key='',
31 | )
32 |
33 | # Print nice welcome message
34 | print "Welcome", client.get('/me')['firstname']
35 |
36 | Installation
37 | ============
38 |
39 | The easiest way to get the latest stable release is to grab it from `pypi
40 | `_ using ``pip``.
41 |
42 | .. code:: bash
43 |
44 | pip install ovh
45 |
46 | Alternatively, you may get latest development version directly from Git.
47 |
48 | .. code:: bash
49 |
50 | pip install -e git+https://github.com/ovh/python-ovh.git#egg=ovh
51 |
52 | Example Usage
53 | =============
54 |
55 | Use the API on behalf of a user
56 | -------------------------------
57 |
58 | 1. Create an application
59 | ************************
60 |
61 | To interact with the APIs, the SDK needs to identify itself using an
62 | ``application_key`` and an ``application_secret``. To get them, you need
63 | to register your application. Depending the API you plan yo use, visit:
64 |
65 | - `OVH Europe `_
66 | - `OVH North-America `_
67 | - `So you Start Europe `_
68 | - `So you Start North America `_
69 | - `Kimsufi Europe `_
70 | - `Kimsufi North America `_
71 | - `RunAbove `_
72 |
73 | Once created, you will obtain an **application key (AK)** and an **application
74 | secret (AS)**.
75 |
76 | 2. Configure your application
77 | *****************************
78 |
79 | The easiest and safest way to use your application's credentials is create an
80 | ``ovh.conf`` configuration file in application's working directory. Here is how
81 | it looks like:
82 |
83 | .. code:: ini
84 |
85 | [default]
86 | ; general configuration: default endpoint
87 | endpoint=ovh-eu
88 |
89 | [ovh-eu]
90 | ; configuration specific to 'ovh-eu' endpoint
91 | application_key=my_app_key
92 | application_secret=my_application_secret
93 | ; uncomment following line when writing a script application
94 | ; with a single consumer key.
95 | ;consumer_key=my_consumer_key
96 |
97 | Depending on the API you want to use, you may set the ``endpoint`` to:
98 |
99 | * ``ovh-eu`` for OVH Europe API
100 | * ``ovh-ca`` for OVH North-America API
101 | * ``soyoustart-eu`` for So you Start Europe API
102 | * ``soyoustart-ca`` for So you Start North America API
103 | * ``kimsufi-eu`` for Kimsufi Europe API
104 | * ``kimsufi-ca`` for Kimsufi North America API
105 | * ``runabove-ca`` for RunAbove API
106 |
107 | See Configuration_ for more inforamtions on available configuration mechanisms.
108 |
109 | .. note:: When using a versioning system, make sure to add ``ovh.conf`` to ignored
110 | files. It contains confidential/security-sensitive informations!
111 |
112 | 3. Authorize your application to access a customer account
113 | **********************************************************
114 |
115 | To allow your application to access a customer account using the API on your
116 | behalf, you need a **consumer key (CK)**.
117 |
118 | Here is a sample code you can use to allow your application to access a
119 | customer's informations:
120 |
121 | .. code:: python
122 |
123 | # -*- encoding: utf-8 -*-
124 |
125 | import ovh
126 |
127 | # create a client using configuration
128 | client = ovh.Client()
129 |
130 | # Request RO, /me API access
131 | access_rules = [
132 | {'method': 'GET', 'path': '/me'},
133 | ]
134 |
135 | # Request token
136 | validation = client.request_consumerkey(access_rules)
137 |
138 | print "Please visit %s to authenticate" % validation['validationUrl']
139 | raw_input("and press Enter to continue...")
140 |
141 | # Print nice welcome message
142 | print "Welcome", client.get('/me')['firstname']
143 | print "Btw, your 'consumerKey' is '%s'" % validation['consumerKey']
144 |
145 |
146 | Returned ``consumerKey`` should then be kept to avoid re-authenticating your
147 | end-user on each use.
148 |
149 | .. note:: To request full and unlimited access to the API, you may use wildcards:
150 |
151 | .. code:: python
152 |
153 | access_rules = [
154 | {'method': 'GET', 'path': '/*'},
155 | {'method': 'POST', 'path': '/*'},
156 | {'method': 'PUT', 'path': '/*'},
157 | {'method': 'DELETE', 'path': '/*'}
158 | ]
159 |
160 | Install a new mail redirection
161 | ------------------------------
162 |
163 | e-mail redirections may be freely configured on domains and DNS zones hosted by
164 | OVH to an arbitrary destination e-mail using API call
165 | ``POST /email/domain/{domain}/redirection``.
166 |
167 | For this call, the api specifies that the source adress shall be given under the
168 | ``from`` keyword. Which is a problem as this is also a reserved Python keyword.
169 | In this case, simply prefix it with a '_', the wrapper will automatically detect
170 | it as being a prefixed reserved keyword and will subsitute it. Such aliasing
171 | is only supported with reserved keywords.
172 |
173 | .. code:: python
174 |
175 | # -*- encoding: utf-8 -*-
176 |
177 | import ovh
178 |
179 | DOMAIN = "example.com"
180 | SOURCE = "sales@example.com"
181 | DESTINATION = "contact@example.com"
182 |
183 | # create a client
184 | client = ovh.Client()
185 |
186 | # Create a new alias
187 | client.post('/email/domain/%s/redirection' % DOMAIN,
188 | _from=SOURCE,
189 | to=DESTINATION,
190 | localCopy=False
191 | )
192 | print "Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION)
193 |
194 | Grab bill list
195 | --------------
196 |
197 | Let's say you want to integrate OVH bills into your own billing system, you
198 | could just script around the ``/me/bills`` endpoints and even get the details
199 | of each bill lines using ``/me/bill/{billId}/details/{billDetailId}``.
200 |
201 | This example assumes an existing Configuration_ with valid ``application_key``,
202 | ``application_secret`` and ``consumer_key``.
203 |
204 | .. code:: python
205 |
206 | # -*- encoding: utf-8 -*-
207 |
208 | import ovh
209 |
210 | # create a client
211 | client = ovh.Client()
212 |
213 | # Grab bill list
214 | bills = client.get('/me/bill')
215 | for bill in bills:
216 | details = client.get('/me/bill/%s' % bill)
217 | print "%12s (%s): %10s --> %s" % (
218 | bill,
219 | details['date'],
220 | details['priceWithTax']['text'],
221 | details['pdfUrl'],
222 | )
223 |
224 | Enable network burst in SBG1
225 | ----------------------------
226 |
227 | 'Network burst' is a free service but is opt-in. What if you have, say, 10
228 | servers in ``SBG-1`` datacenter? You certainely don't want to activate it
229 | manually for each servers. You could take advantage of a code like this.
230 |
231 | This example assumes an existing Configuration_ with valid ``application_key``,
232 | ``application_secret`` and ``consumer_key``.
233 |
234 | .. code:: python
235 |
236 | # -*- encoding: utf-8 -*-
237 |
238 | import ovh
239 |
240 | # create a client
241 | client = ovh.Client()
242 |
243 | # get list of all server names
244 | servers = client.get('/dedicated/server/')
245 |
246 | # find all servers in SBG-1 datacenter
247 | for server in servers:
248 | details = client.get('/dedicated/server/%s' % server)
249 | if details['datacenter'] == 'sbg1':
250 | # enable burst on server
251 | client.put('/dedicated/server/%s/burst' % server, status='active')
252 | print "Enabled burst for %s server located in SBG-1" % server
253 |
254 | List application authorized to access your account
255 | --------------------------------------------------
256 |
257 | Thanks to the application key / consumer key mechanism, it is possible to
258 | finely track applications having access to your data and revoke this access.
259 | This examples lists validated applications. It could easily be adapted to
260 | manage revocation too.
261 |
262 | This example assumes an existing Configuration_ with valid ``application_key``,
263 | ``application_secret`` and ``consumer_key``.
264 |
265 | .. code:: python
266 |
267 | # -*- encoding: utf-8 -*-
268 |
269 | import ovh
270 | from tabulate import tabulate
271 |
272 | # create a client
273 | client = ovh.Client()
274 |
275 | credentials = client.get('/me/api/credential', status='validated')
276 |
277 | # pretty print credentials status
278 | table = []
279 | for credential_id in credentials:
280 | credential_method = '/me/api/credential/'+str(credential_id)
281 | credential = client.get(credential_method)
282 | application = client.get(credential_method+'/application')
283 |
284 | table.append([
285 | credential_id,
286 | '[%s] %s' % (application['status'], application['name']),
287 | application['description'],
288 | credential['creation'],
289 | credential['expiration'],
290 | credential['lastUse'],
291 | ])
292 | print tabulate(table, headers=['ID', 'App Name', 'Description',
293 | 'Token Creation', 'Token Expiration', 'Token Last Use'])
294 |
295 | Before running this example, make sure you have the
296 | `tabulate `_ library installed. It's a
297 | pretty cool library to pretty print tabular data in a clean and easy way.
298 |
299 | >>> pip install tabulate
300 |
301 | List Runabove's instance
302 | ------------------------
303 |
304 | This example assumes an existing Configuration_ with valid ``application_key``,
305 | ``application_secret`` and ``consumer_key``.
306 |
307 | .. code:: python
308 |
309 | # -*- encoding: utf-8 -*-
310 |
311 | import ovh
312 | from tabulate import tabulate
313 |
314 | # visit https://api.runabove.com/createApp/ to create your application's credentials
315 | client = ovh.Client(endpoint='runabove-ca')
316 |
317 | # get list of all instances
318 | instances = client.get('/instance')
319 |
320 | # pretty print instances status
321 | table = []
322 | for instance in instances:
323 | table.append([
324 | instance['name'],
325 | instance['ip'],
326 | instance['region'],
327 | instance['status'],
328 | ])
329 | print tabulate(table, headers=['Name', 'IP', 'Region', 'Status'])
330 |
331 | Before running this example, make sure you have the
332 | `tabulate `_ library installed. It's a
333 | pretty cool library to pretty print tabular data in a clean and easy way.
334 |
335 | >>> pip install tabulate
336 |
337 | Configuration
338 | =============
339 |
340 | The straightforward way to use OVH's API keys is to embed them directly in the
341 | application code. While this is very convenient, it lacks of elegance and
342 | flexibility.
343 |
344 | Alternatively it is suggested to use configuration files or environment
345 | variables so that the same code may run seamlessly in multiple environments.
346 | Production and development for instance.
347 |
348 | This wrapper will first look for direct instanciation parameters then
349 | ``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and
350 | ``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not
351 | provided, it will look for a configuration file of the form:
352 |
353 | .. code:: ini
354 |
355 | [default]
356 | ; general configuration: default endpoint
357 | endpoint=ovh-eu
358 |
359 | [ovh-eu]
360 | ; configuration specific to 'ovh-eu' endpoint
361 | application_key=my_app_key
362 | application_secret=my_application_secret
363 | consumer_key=my_consumer_key
364 |
365 | The client will successively attempt to locate this configuration file in
366 |
367 | 1. Current working directory: ``./ovh.conf``
368 | 2. Current user's home directory ``~/.ovh.conf``
369 | 3. System wide configuration ``/etc/ovh.conf``
370 |
371 | This lookup mechanism makes it easy to overload credentials for a specific
372 | project or user.
373 |
374 | Passing parameters
375 | =============
376 |
377 | You can call all the methods of the API with the necessary arguments.
378 |
379 | If an API needs an argument colliding with a Python reserved keyword, it
380 | can be prefixed with an underscore. For example, ``from`` argument of
381 | ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``.
382 |
383 | With characters invalid in python argument name like a dot, you can:
384 |
385 | .. code:: python
386 |
387 | # -*- encoding: utf-8 -*-
388 |
389 | import ovh
390 |
391 | params = {}
392 | params['date.from'] = '2014-01-01'
393 | params['date.to'] = '2015-01-01'
394 |
395 | # create a client
396 | client = ovh.Client()
397 |
398 | # pass parameters using **
399 | client.post('/me/bills', **params)
400 |
401 | Hacking
402 | =======
403 |
404 | This wrapper uses standard Python tools, so you should feel at home with it.
405 | Here is a quick outline of what it may look like. A good practice is to run
406 | this from a ``virtualenv``.
407 |
408 | Get the sources
409 | ---------------
410 |
411 | .. code:: bash
412 |
413 | git clone https://github.com/ovh/python-ovh.git
414 | cd python-ovh
415 | python setup.py develop
416 |
417 | You've developed a new cool feature ? Fixed an annoying bug ? We'd be happy
418 | to hear from you !
419 |
420 | Run the tests
421 | -------------
422 |
423 | Simply run ``nosetests``. It will automatically load its configuration from
424 | ``setup.cfg`` and output full coverage status. Since we all love quality, please
425 | note that we do not accept contributions with test coverage under 100%.
426 |
427 | .. code:: bash
428 |
429 | pip install -r requirements-dev.txt
430 | nosetests # 100% coverage is a hard minimum
431 |
432 |
433 | Build the documentation
434 | -----------------------
435 |
436 | Documentation is managed using the excellent ``Sphinx`` system. For example, to
437 | build HTML documentation:
438 |
439 | .. code:: bash
440 |
441 | cd python-ovh/docs
442 | make html
443 |
444 | Supported APIs
445 | ==============
446 |
447 | OVH Europe
448 | ----------
449 |
450 | - **Documentation**: https://eu.api.ovh.com/
451 | - **Community support**: api-subscribe@ml.ovh.net
452 | - **Console**: https://eu.api.ovh.com/console
453 | - **Create application credentials**: https://eu.api.ovh.com/createApp/
454 | - **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/
455 |
456 | OVH North America
457 | -----------------
458 |
459 | - **Documentation**: https://ca.api.ovh.com/
460 | - **Community support**: api-subscribe@ml.ovh.net
461 | - **Console**: https://ca.api.ovh.com/console
462 | - **Create application credentials**: https://ca.api.ovh.com/createApp/
463 | - **Create script credentials** (all keys at once): https://ca.api.ovh.com/createToken/
464 |
465 | So you Start Europe
466 | -------------------
467 |
468 | - **Documentation**: https://eu.api.soyoustart.com/
469 | - **Community support**: api-subscribe@ml.ovh.net
470 | - **Console**: https://eu.api.soyoustart.com/console/
471 | - **Create application credentials**: https://eu.api.soyoustart.com/createApp/
472 | - **Create script credentials** (all keys at once): https://eu.api.soyoustart.com/createToken/
473 |
474 | So you Start North America
475 | --------------------------
476 |
477 | - **Documentation**: https://ca.api.soyoustart.com/
478 | - **Community support**: api-subscribe@ml.ovh.net
479 | - **Console**: https://ca.api.soyoustart.com/console/
480 | - **Create application credentials**: https://ca.api.soyoustart.com/createApp/
481 | - **Create script credentials** (all keys at once): https://ca.api.soyoustart.com/createToken/
482 |
483 | Kimsufi Europe
484 | --------------
485 |
486 | - **Documentation**: https://eu.api.kimsufi.com/
487 | - **Community support**: api-subscribe@ml.ovh.net
488 | - **Console**: https://eu.api.kimsufi.com/console/
489 | - **Create application credentials**: https://eu.api.kimsufi.com/createApp/
490 | - **Create script credentials** (all keys at once): https://eu.api.kimsufi.com/createToken/
491 |
492 | Kimsufi North America
493 | ---------------------
494 |
495 | - **Documentation**: https://ca.api.kimsufi.com/
496 | - **Community support**: api-subscribe@ml.ovh.net
497 | - **Console**: https://ca.api.kimsufi.com/console/
498 | - **Create application credentials**: https://ca.api.kimsufi.com/createApp/
499 | - **Create script credentials** (all keys at once): https://ca.api.kimsufi.com/createToken/
500 |
501 | Runabove
502 | --------
503 |
504 | - **Community support**: https://community.runabove.com/
505 | - **Console**: https://api.runabove.com/console/
506 | - **Create application credentials**: https://api.runabove.com/createApp/
507 | - **High level SDK**: https://github.com/runabove/python-runabove
508 |
509 | Related links
510 | =============
511 |
512 | - **Contribute**: https://github.com/ovh/python-ovh
513 | - **Report bugs**: https://github.com/ovh/python-ovh/issues
514 | - **Download**: http://pypi.python.org/pypi/ovh
515 |
516 |
--------------------------------------------------------------------------------