├── 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 | --------------------------------------------------------------------------------