From 3643dd40da0f0d0aefbba7355c640dd518778b1a Mon Sep 17 00:00:00 2001 From: Jean-Tiare LE BIGOT Date: Mon, 22 Dec 2014 12:20:36 +0100 Subject: [PATCH] Support '_' prefixed alias for Python reserved keywords in API calls Signed-off-by: Jean-Tiare LE BIGOT --- CHANGELOG.md | 7 ++++- README.rst | 34 +++++++++++++++++++++++ docs/index.rst | 34 +++++++++++++++++++++++ ovh/client.py | 66 +++++++++++++++++++++++++++++++++++++++++--- tests/test_client.py | 15 ++++++++++ 5 files changed, 151 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a2b1e..032e541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ Changelog ========= -## 0.3.0 (2014-09-23) +## 0.3.1 (2014-12-22) + + - [enhancement] support '_' prefixed keyword argument alias when colliding with Python reserved keywords + - [enhancement] add API documentation + +## 0.3.0 (2014-11-23) - [enhancement] add kimsufi API Europe/North-America - [enhancement] add soyoustart API Europe/North-America - [Q/A] add minimal integration test diff --git a/README.rst b/README.rst index 9ea9e9d..c669d98 100644 --- a/README.rst +++ b/README.rst @@ -157,6 +157,40 @@ end-user on each use. {'method': 'DELETE', 'path': '/*'} ] +Install a new mail redirection +------------------------------ + +e-mail redirections may be freely configured on domains and DNS zones hosted by +OVH to an arbitrary destination e-mail using API call +``POST /email/domain/{domain}/redirection``. + +For this call, the api specifies that the source adress shall be given under the +``from`` keyword. Which is a problem as this is also a reserved Python keyword. +In this case, simply prefix it with a '_', the wrapper will automatically detect +it as being a prefixed reserved keyword and will subsitute it. Such aliasing +is only supported with reserved keywords. + +.. code:: python + + # -*- encoding: utf-8 -*- + + import ovh + + DOMAIN = "example.com" + SOURCE = "sales@example.com" + DESTINATION = "contact@example.com" + + # create a client + client = ovh.Client() + + # Create a new alias + client.post('/email/domain/%s/redirection' % DOMAIN, + _from=SOURCE, + to=DESTINATION + localCopy=False + ) + print "Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION) + Grab bill list -------------- diff --git a/docs/index.rst b/docs/index.rst index 03b58ac..8cc6a8c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,6 +149,40 @@ end-user on each use. {'method': 'DELETE', 'path': '/*'} ] +Install a new mail redirection +------------------------------ + +e-mail redirections may be freely configured on domains and DNS zones hosted by +OVH to an arbitrary destination e-mail using API call +``POST /email/domain/{domain}/redirection``. + +For this call, the api specifies that the source adress shall be given under the +``from`` keyword. Which is a problem as this is also a reserved Python keyword. +In this case, simply prefix it with a '_', the wrapper will automatically detect +it as being a prefixed reserved keyword and will subsitute it. Such aliasing +is only supported with reserved keywords. + +.. code:: python + + # -*- encoding: utf-8 -*- + + import ovh + + DOMAIN = "example.com" + SOURCE = "sales@example.com" + DESTINATION = "contact@example.com" + + # create a client + client = ovh.Client() + + # Create a new alias + client.post('/email/domain/%s/redirection' % DOMAIN, + _from=SOURCE, + to=DESTINATION + localCopy=False + ) + print "Installed new mail redirection from %s to %s" % (SOURCE, DESTINATION) + Grab bill list -------------- diff --git a/ovh/client.py b/ovh/client.py index c8dab42..1e5b6c9 100644 --- a/ovh/client.py +++ b/ovh/client.py @@ -36,6 +36,7 @@ It handles requesting credential, signing queries... import hashlib import urllib +import keyword import time import json @@ -233,9 +234,38 @@ class Client(object): ## API shortcuts + def _canonicalize_kwargs(self, kwargs): + """ + If an API needs an argument colliding with a Python reserved keyword, it + can be prefixed with an underscore. For example, ``from`` argument of + ``POST /email/domain/{domain}/redirection`` may be replaced by ``_from`` + + :param dict kwargs: input kwargs + :return dict: filtered kawrgs + """ + arguments = {} + + for k, v in kwargs.iteritems(): + if k[0] == '_' and k[1:] in keyword.kwlist: + k = k[1:] + arguments[k] = v + + return arguments + def get(self, _target, _need_auth=True, **kwargs): - """'GET' :py:func:`Client.call` wrapper""" + """ + 'GET' :py:func:`Client.call` wrapper. + + Query string parameters can be set either directly in ``_target`` or as + keywork arguments. If an argument collides with a Python reserved + keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``. + + :param string _target: API method to call + :param string _need_auth: If True, send authentication headers. This is + the default + """ if kwargs: + kwargs = self._canonicalize_kwargs(kwargs) query_string = urlencode(kwargs) if '?' in _target: _target = '%s&%s' % (_target, query_string) @@ -245,15 +275,43 @@ class Client(object): return self.call('GET', _target, None, _need_auth) def put(self, _target, _need_auth=True, **kwargs): - """'PUT' :py:func:`Client.call` wrapper""" + """ + 'PUT' :py:func:`Client.call` wrapper + + Body parameters can be set either directly in ``_target`` or as keywork + arguments. If an argument collides with a Python reserved keyword, + prefix it with a '_'. For instance, ``from`` becomes ``_from``. + + :param string _target: API method to call + :param string _need_auth: If True, send authentication headers. This is + the default + """ + kwargs = self._canonicalize_kwargs(kwargs) return self.call('PUT', _target, kwargs, _need_auth) def post(self, _target, _need_auth=True, **kwargs): - """'POST' :py:func:`Client.call` wrapper""" + """ + 'POST' :py:func:`Client.call` wrapper + + Body parameters can be set either directly in ``_target`` or as keywork + arguments. If an argument collides with a Python reserved keyword, + prefix it with a '_'. For instance, ``from`` becomes ``_from``. + + :param string _target: API method to call + :param string _need_auth: If True, send authentication headers. This is + the default + """ + kwargs = self._canonicalize_kwargs(kwargs) return self.call('POST', _target, kwargs, _need_auth) def delete(self, _target, _need_auth=True): - """'DELETE' :py:func:`Client.call` wrapper""" + """ + 'DELETE' :py:func:`Client.call` wrapper + + :param string _target: API method to call + :param string _need_auth: If True, send authentication headers. This is + the default + """ return self.call('DELETE', _target, None, _need_auth) ## low level helpers diff --git a/tests/test_client.py b/tests/test_client.py index e156610..48130ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -131,6 +131,14 @@ class testClient(unittest.TestCase): ## test wrappers + def test__canonicalize_kwargs(self): + api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY) + + self.assertEqual({}, api._canonicalize_kwargs({})) + self.assertEqual({'from': 'value'}, api._canonicalize_kwargs({'from': 'value'})) + self.assertEqual({'_to': 'value'}, api._canonicalize_kwargs({'_to': 'value'})) + self.assertEqual({'from': 'value'}, api._canonicalize_kwargs({'_from': 'value'})) + @mock.patch.object(Client, 'call') def test_get(self, m_call): # basic test @@ -150,6 +158,13 @@ class testClient(unittest.TestCase): self.assertEqual(m_call.return_value, api.get(FAKE_URL+'?query=string', param="test")) m_call.assert_called_once_with('GET', FAKE_URL+'?query=string¶m=test', None, True) + # keyword calling convention + m_call.reset_mock() + api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY) + self.assertEqual(m_call.return_value, api.get(FAKE_URL, _from="start", to="end")) + m_call.assert_called_once_with('GET', FAKE_URL+'?to=end&from=start', None, True) + + @mock.patch.object(Client, 'call') def test_delete(self, m_call): api = Client(ENDPOINT, APPLICATION_KEY, APPLICATION_SECRET, CONSUMER_KEY)