diff --git a/.travis.yml b/.travis.yml index c50f9c0..64146d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,17 @@ language: python + python: -- '2.7' -- '3.4' + - '2.7' + - '3.4' + install: -- pip install -r requirements.txt -- pip install -r test-requirements.txt -script: nosetests + - pip install -r requirements.txt + - pip install -r test-requirements.txt + +script: + - nosetests + - flake8 + deploy: provider: pypi user: internaphosting diff --git a/README.rst b/README.rst index 987c9bf..b9bb651 100755 --- a/README.rst +++ b/README.rst @@ -1,16 +1,40 @@ -# Ubersmith API Client for Python +Ubersmith API Client for Python +=============================== .. image:: https://travis-ci.org/internap/python-ubersmithclient.svg?branch=master :target: https://travis-ci.org/internap/python-ubersmithclient -.. image:: https://img.shields.io/pypi/v/ubersmith_client.svg?style=flat - :target: https://pypi.python.org/pypi/ubersmith_client +.. image:: https://img.shields.io/pypi/v/python-ubersmithclient.svg?style=flat + :target: https://pypi.python.org/pypi/python-ubersmithclient -# Usage +Usage +----- - >>> import ubersmith_client - >>> api = ubersmith_client.api.init('http://ubersmith.com/api/2.0/', 'username', 'password') - >>> api.client.count() - u'264' - >>> api.client.latest_client() - 1265 +.. code:: python + + import ubersmith_client + + api = ubersmith_client.api.init(url='http://ubersmith.com/api/2.0/', user='username', password='password') + api.client.count() + >>> u'264' + api.client.latest_client() + >>> 1265 + +API +--- + +**ubersmith_client.api.init(url, user, password, timeout, use_http_get)** + :url: + URL of your API + + *Example:* ``http://ubersmith.example.org/api/2.0/`` + + :user: API username + :password: API Password or token + :timeout: api timeout given to requests (type: float) + + *Default:* ``60`` + :use_http_get: + Use `GET` requests instead of `POST` + + *Default:* ``False`` diff --git a/requirements.txt b/requirements.txt index c7317e0..39d4f77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests<=2.9.1 +pbr>=1.8 +requests diff --git a/setup.cfg b/setup.cfg index 0987682..83a5503 100755 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] -name = ubersmith_client -url = https://github.com/Marx314/python-ubersmithclient +name = python-ubersmithclient +url = https://github.com/internap/python-ubersmithclient author = Internap author-email = opensource@internap.com summary = Another ubersmith lib diff --git a/setup.py b/setup.py index b2e43a5..823b9f8 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ - -# Copyright 2016 Internap +# Copyright 2017 Internap. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,10 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -#!/usr/bin/env python from setuptools import setup setup( - setup_requires=["pbr>=1.8"], + setup_requires=['pbr'], pbr=True, ) diff --git a/test-requirements.txt b/test-requirements.txt index ab4c471..bb0dfc5 100755 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ nose==1.2.1 -requests-mock==0.7.0 -pyhamcrest==1.8.1 \ No newline at end of file +pyhamcrest==1.8.1 +mock==1.3.0 +flake8 diff --git a/tests/__init__.py b/tests/__init__.py index 2f12658..a5b5ca9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,10 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -def apply_kwargs(kwargs, default_kwargs): - for k, v in kwargs.items(): - if isinstance(v, dict): - default_kwargs[k] = apply_kwargs(v, default_kwargs[k]) - else: - default_kwargs[k] = v - return default_kwargs diff --git a/tests/api_test.py b/tests/api_test.py deleted file mode 100644 index a54281c..0000000 --- a/tests/api_test.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright 2016 Internap. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import base64 -import unittest - -from hamcrest import assert_that, equal_to, raises, calling -import requests_mock -from requests_mock.exceptions import NoMockAddress -from ubersmith_client import api -from ubersmith_client.exceptions import UbersmithException, BadRequest, UnknownError, Forbidden, NotFound, Unauthorized -from tests.ubersmith_json.response_data_structure import a_response_data - - -class UbersmithIWebTest(unittest.TestCase): - def setUp(self): - self.url = 'http://ubersmith.example.org/' - self.username = 'admin' - self.password = 'test' - - @requests_mock.mock() - def test_api_method_returns_without_arguments(self, request_mock): - json_data = [ - { - 'client_id': '1', - 'first': 'John', - 'last': 'Snow', - 'company': 'The Night Watch' - } - ] - data = a_response_data(data=json_data) - self.expect_a_ubersmith_call(request_mock, "client.list", data=data) - - ubersmith_api = api.init(self.url, self.username, self.password) - response = ubersmith_api.client.list() - - assert_that(response, equal_to(json_data)) - - @requests_mock.mock() - def test_api_method_returns_with_arguments(self, request_mock): - json_data = { - 'group_id': '1', - 'client_id': '30001', - 'assignment_count': '1' - } - data = a_response_data(data=json_data) - self.expect_a_ubersmith_call(request_mock, method="device.ip_group_list", fac_id='1', client_id='30001', - data=data) - - ubersmith_api = api.init(self.url, self.username, self.password) - response = ubersmith_api.device.ip_group_list(fac_id=1, client_id=30001) - - assert_that(response, equal_to(json_data)) - - @requests_mock.mock() - def test_api_raises_exception_with_if_data_status_is_false(self, request_mock): - data = a_response_data(status=False, error_code=1, error_message="invalid method specified: client.miss", - data=None) - ubersmith_api = api.init(self.url, self.username, self.password) - - self.expect_a_ubersmith_call(request_mock, method="client.miss", data=data) - assert_that(calling(ubersmith_api.client.miss), raises(UbersmithException)) - - @requests_mock.mock() - def test_api_raises_exception_for_invalid_status_code(self, request_mock): - method = "client.list" - ubersmith_api = api.init(self.url, self.username, self.password) - - self.expect_a_ubersmith_call(request_mock, method=method, status_code=400) - - assert_that(calling(ubersmith_api.client.list), raises(BadRequest)) - - self.expect_a_ubersmith_call(request_mock, method=method, status_code=401) - assert_that(calling(ubersmith_api.client.list), raises(Unauthorized)) - - self.expect_a_ubersmith_call(request_mock, method=method, status_code=403) - assert_that(calling(ubersmith_api.client.list), raises(Forbidden)) - - self.expect_a_ubersmith_call(request_mock, method=method, status_code=404) - assert_that(calling(ubersmith_api.client.list), raises(NotFound)) - - self.expect_a_ubersmith_call(request_mock, method=method, status_code=500) - assert_that(calling(ubersmith_api.client.list), raises(UnknownError)) - - @requests_mock.mock() - def test_api_with_a_false_identifier(self, request_mock): - method = "client.list" - self.expect_a_ubersmith_call(request_mock, method=method) - ubersmith_api = api.init(self.url, 'not_hapi', 'lol') - - with self.assertRaises(NoMockAddress) as ube: - ubersmith_api.client.list() - - assert_that(str(ube.exception), equal_to("No mock address: GET " + self.url + "?method=" + method)) - - @requests_mock.mock() - def test_api_http_get_method(self, request_mock): - json_data = { - 'group_id': '666', - 'client_id': '30666', - 'assignment_count': '1' - } - data = a_response_data(data=json_data) - self.expect_a_ubersmith_call(request_mock, method="device.ip_group_list", fac_id='666', client_id='30666', - data=data) - - ubersmith_api = api.init(self.url, self.username, self.password) - response = ubersmith_api.device.ip_group_list.http_get(fac_id=666, client_id=30666) - - assert_that(response, equal_to(json_data)) - - @requests_mock.mock() - def test_api_http_get_method_default(self, request_mock): - json_data = { - 'group_id': '666', - 'client_id': '30666', - 'assignment_count': '1' - } - data = a_response_data(data=json_data) - self.expect_a_ubersmith_call(request_mock, method="device.ip_group_list", fac_id='666', client_id='30666', - data=data) - - ubersmith_api = api.init(self.url, self.username, self.password) - response = ubersmith_api.device.ip_group_list(fac_id=666, client_id=30666) - - assert_that(response, equal_to(json_data)) - - @requests_mock.mock() - def test_api_http_post_method_result_200(self, request_mock): - json_data = { - 'data': '778', - 'error_code': None, - 'error_message': '', - 'status': True - } - - self.expect_a_ubersmith_call_post( - request_mock, - response_body=json_data, - ) - - ubersmith_api = api.init(self.url, self.username, self.password) - response = ubersmith_api.support.ticket_submit.http_post(body='ticket body', subject='ticket subject') - - assert_that(response, equal_to(json_data.get('data'))) - - @requests_mock.mock() - def test_api_http_post_method_raises_on_result_414(self, request_mock): - json_data = { - 'data': '778', - 'error_code': None, - 'error_message': '', - 'status': True - } - - self.expect_a_ubersmith_call_post( - request_mock, - response_body=json_data, - status_code=414 - ) - - ubersmith_api = api.init(self.url, self.username, self.password) - - assert_that(calling(ubersmith_api.support.ticket_submit.http_post), raises(UnknownError)) - - @requests_mock.mock() - def test_api_http_post_method_raises_on_result_500(self, request_mock): - json_data = { - 'data': '778', - 'error_code': None, - 'error_message': '', - 'status': False - } - - self.expect_a_ubersmith_call_post( - request_mock, - response_body=json_data, - ) - - ubersmith_api = api.init(self.url, self.username, self.password) - - assert_that(calling(ubersmith_api.support.ticket_submit.http_post), raises(UbersmithException)) - - def expect_a_ubersmith_call(self, request_mock, method, data=None, status_code=200, **kwargs): - url = self.url + '?method=' + method - if kwargs: - for key, value in kwargs.items(): - url += '&' + key + '=' + value - headers = { - 'Content-Type': 'application/json', - } - request_mock.get(url, json=data, headers=headers, request_headers={ - 'Authorization': self.get_auth_header() - }, status_code=status_code) - - def expect_a_ubersmith_call_post(self, request_mock, response_body=None, status_code=200): - request_mock.post(self.url, json=response_body, status_code=status_code) - - def get_auth_header(self): - auth = base64.b64encode((self.username + ':' + self.password).encode('utf-8')) - return 'Basic ' + auth.decode('utf-8') diff --git a/tests/http_utils_test.py b/tests/http_utils_test.py new file mode 100644 index 0000000..8f1d959 --- /dev/null +++ b/tests/http_utils_test.py @@ -0,0 +1,74 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from ubersmith_client import _http_utils + + +class HttpUtilsTest(unittest.TestCase): + def test_form_encode_with_list(self): + result = _http_utils.form_encode(dict(test=['a', 'b'])) + self.assertDictEqual({ + 'test[0]': 'a', + 'test[1]': 'b', + }, result) + + def test_with_tuples(self): + result = _http_utils.form_encode(dict(test=('a', 'b'))) + + self.assertDictEqual({ + 'test[0]': 'a', + 'test[1]': 'b', + }, result) + + def test_with_dict(self): + result = _http_utils.form_encode(dict(test={'a': '1', 'b': '2'})) + + self.assertDictEqual({ + 'test[a]': '1', + 'test[b]': '2' + }, result) + + def test_with_empty_dict(self): + result = _http_utils.form_encode(dict(test_dict={}, test_list=[])) + + self.assertDictEqual({ + 'test_dict': {}, + 'test_list': [] + }, result) + + def test_with_nested_lists_and_dicts(self): + result = _http_utils.form_encode(dict(test=[['a', 'b'], {'c': '1', 'd': '2'}])) + + self.assertDictEqual({ + 'test[0][0]': 'a', + 'test[0][1]': 'b', + 'test[1][c]': '1', + 'test[1][d]': '2' + }, result) + + def test_with_bools(self): + result = _http_utils.form_encode(dict(true=True, false=False)) + + self.assertDictEqual({ + 'true': True, + 'false': False + }, result) + + def test_filtering_files(self): + result = _http_utils.form_encode_without_files(dict(true=True, files=dict(attach='some_binary_data'))) + self.assertDictEqual({ + 'true': True, + }, result) diff --git a/tests/ubersmith_json/__init__.py b/tests/ubersmith_json/__init__.py index f44d680..a5b5ca9 100644 --- a/tests/ubersmith_json/__init__.py +++ b/tests/ubersmith_json/__init__.py @@ -10,4 +10,4 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/tests/ubersmith_json/response_data_structure.py b/tests/ubersmith_json/response_data_structure.py index 56ea561..216fe13 100644 --- a/tests/ubersmith_json/response_data_structure.py +++ b/tests/ubersmith_json/response_data_structure.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from tests import apply_kwargs + def a_response_data(**overrides): return apply_kwargs(overrides, { @@ -20,3 +20,12 @@ def a_response_data(**overrides): "error_message": "", "data": {}, }) + + +def apply_kwargs(kwargs, default_kwargs): + for k, v in kwargs.items(): + if isinstance(v, dict): + default_kwargs[k] = apply_kwargs(v, default_kwargs[k]) + else: + default_kwargs[k] = v + return default_kwargs diff --git a/tests/ubersmith_request_form_encoding_test.py b/tests/ubersmith_request_form_encoding_test.py new file mode 100644 index 0000000..1f5d747 --- /dev/null +++ b/tests/ubersmith_request_form_encoding_test.py @@ -0,0 +1,58 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from mock import sentinel, patch, MagicMock + +from ubersmith_client.ubersmith_request_get import UbersmithRequestGet +from ubersmith_client.ubersmith_request_post import UbersmithRequestPost + + +class UbersmithRequestFormEncodingTest(unittest.TestCase): + def setUp(self): + self.ubersmith_constructor_params = (sentinel.url, sentinel.username, sentinel.password, + sentinel.module, sentinel.timeout) + self._standard_kwargs = dict(auth=(sentinel.username, sentinel.password), + timeout=sentinel.timeout, + url=sentinel.url, + headers={'user-agent': 'python-ubersmithclient'}) + + @patch('ubersmith_client.ubersmith_request_get.requests') + def test_get_with_list(self, request_mock): + request_mock.get.return_value = MagicMock(status_code=200) + + self.client = UbersmithRequestGet(*self.ubersmith_constructor_params) + self.client.call(test=['a']) + + expected_args = self._standard_kwargs + expected_args.update(dict(params={ + 'method': 'sentinel.module.call', + 'test[0]': 'a', + })) + request_mock.get.assert_called_with(**expected_args) + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_post_with_list(self, request_mock): + request_mock.post.return_value = MagicMock(status_code=200) + + self.client = UbersmithRequestPost(*self.ubersmith_constructor_params) + self.client.call(test=['a']) + + expected_args = self._standard_kwargs + expected_args.update(dict(data={ + 'method': 'sentinel.module.call', + 'test[0]': 'a', + })) + request_mock.post.assert_called_with(**expected_args) diff --git a/tests/ubersmith_request_get_test.py b/tests/ubersmith_request_get_test.py new file mode 100644 index 0000000..72b66a6 --- /dev/null +++ b/tests/ubersmith_request_get_test.py @@ -0,0 +1,111 @@ +# Copyright 2016 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from hamcrest import assert_that, equal_to +from mock import patch, MagicMock + +import ubersmith_client +from tests.ubersmith_json.response_data_structure import a_response_data + + +class UbersmithRequestGetTest(unittest.TestCase): + def setUp(self): + self.url = 'http://ubersmith.example.com/' + self.username = 'admin' + self.password = 'test' + + self.auth = (self.username, self.password) + self.timeout = 60 + + @patch('ubersmith_client.ubersmith_request_get.requests') + def test_api_get_method_returns_without_arguments(self, requests_mock): + json_data = { + 'company': 'council of ricks' + } + expected_call = self.expect_a_ubersmith_call(requests_mock=requests_mock, + method='client.list', + returning=a_response_data(data=json_data)) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password, use_http_get=True) + response = ubersmith_api.client.list() + + assert_that(response, equal_to(json_data)) + + expected_call() + + @patch('ubersmith_client.ubersmith_request_get.requests') + def test_api_get_method_returns_with_arguments(self, request_mock): + json_data = { + 'group_id': '1', + 'client_id': '30001', + 'assignment_count': '1' + } + expected_call = self.expect_a_ubersmith_call(requests_mock=request_mock, + method='device.ip_group_list', + fac_id=1, + client_id=30001, + returning=a_response_data(data=json_data)) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password, use_http_get=True) + response = ubersmith_api.device.ip_group_list(fac_id=1, client_id=30001) + + assert_that(response, equal_to(json_data)) + + expected_call() + + @patch('ubersmith_client.ubersmith_request_get.requests') + def test_api_get_support_ticket_submit_allow_file_upload(self, request_mock): + expected_files = {'attach[0]': ('filename.pdf', b'filecontent')} + expected_call = self.expect_a_ubersmith_call_with_files(requests_mock=request_mock, + method='support.ticket_submit', + subject='that I used to know', + body='some body', + returning=a_response_data(data='42'), + files=expected_files) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password, use_http_get=True) + + response = ubersmith_api.support.ticket_submit(subject='that I used to know', + body='some body', + files=expected_files) + + assert_that(response, equal_to('42')) + + expected_call() + + def expect_a_ubersmith_call(self, requests_mock, returning=None, **kwargs): + response = MagicMock(status_code=200, headers={'content-type': 'application/json'}) + requests_mock.get = MagicMock(return_value=response) + response.json = MagicMock(return_value=returning) + + def assert_called_with(): + requests_mock.get.assert_called_with(auth=self.auth, params=kwargs, timeout=self.timeout, url=self.url, + headers={'user-agent': 'python-ubersmithclient'}) + response.json.assert_called_with() + + return assert_called_with + + def expect_a_ubersmith_call_with_files(self, requests_mock, returning=None, files=None, **kwargs): + response = MagicMock(status_code=200, headers={'content-type': 'application/json'}) + requests_mock.get = MagicMock(return_value=response) + response.json = MagicMock(return_value=returning) + + def assert_called_with(): + requests_mock.get.assert_called_with(auth=self.auth, params=kwargs, timeout=self.timeout, url=self.url, + files=files, + headers={'user-agent': 'python-ubersmithclient'}) + response.json.assert_called_with() + + return assert_called_with diff --git a/tests/ubersmith_request_post_test.py b/tests/ubersmith_request_post_test.py new file mode 100644 index 0000000..fdbe1b6 --- /dev/null +++ b/tests/ubersmith_request_post_test.py @@ -0,0 +1,122 @@ +# Copyright 2016 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from hamcrest import assert_that, equal_to, calling, raises +from mock import patch, MagicMock + +import ubersmith_client +from tests.ubersmith_json.response_data_structure import a_response_data + + +class UbersmithRequestPostTest(unittest.TestCase): + def setUp(self): + self.url = 'http://ubersmith.example.com/' + self.username = 'admin' + self.password = 'test' + + self.auth = (self.username, self.password) + self.timeout = 60 + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_post_method_returns_with_arguments(self, request_mock): + json_data = { + 'group_id': '1', + 'client_id': '30001', + 'assignment_count': '1' + } + expected_call = self.expect_a_ubersmith_call_post(requests_mock=request_mock, + method='device.ip_group_list', + fac_id=1, + client_id=30001, + returning=a_response_data(data=json_data)) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + response = ubersmith_api.device.ip_group_list(fac_id=1, client_id=30001) + + assert_that(response, equal_to(json_data)) + + expected_call() + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_post_method_returns_without_arguments(self, requests_mock): + json_data = { + 'company': 'schwifty' + } + expected_call = self.expect_a_ubersmith_call_post(requests_mock=requests_mock, + method='client.list', + returning=a_response_data(data=json_data)) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + response = ubersmith_api.client.list() + + assert_that(response, equal_to(json_data)) + + expected_call() + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_raises_exception_with_if_data_status_is_false(self, requests_mock): + data = a_response_data(status=False, + error_code=1, + error_message='invalid method specified: client.miss', + data='schwifty') + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + + self.expect_a_ubersmith_call_post(requests_mock, method='client.miss', returning=data) + assert_that(calling(ubersmith_api.client.miss), raises(ubersmith_client.exceptions.UbersmithException)) + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_post_support_ticket_submit_allow_file_upload(self, request_mock): + expected_files = {'attach[0]': ('filename.pdf', b'filecontent')} + expected_call = self.expect_a_ubersmith_call_post_with_files(requests_mock=request_mock, + method='support.ticket_submit', + subject='that I used to know', + body='some body', + returning=a_response_data(data='42'), + files=expected_files) + + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + + response = ubersmith_api.support.ticket_submit(subject='that I used to know', + body='some body', + files=expected_files) + + assert_that(response, equal_to('42')) + + expected_call() + + def expect_a_ubersmith_call_post(self, requests_mock, returning=None, **kwargs): + response = MagicMock(status_code=200, headers={'content-type': 'application/json'}) + requests_mock.post = MagicMock(return_value=response) + response.json = MagicMock(return_value=returning) + + def assert_called_with(): + requests_mock.post.assert_called_with(auth=self.auth, timeout=self.timeout, url=self.url, data=kwargs, + headers={'user-agent': 'python-ubersmithclient'}) + response.json.assert_called_with() + + return assert_called_with + + def expect_a_ubersmith_call_post_with_files(self, requests_mock, returning=None, files=None, **kwargs): + response = MagicMock(status_code=200, headers={'content-type': 'application/json'}) + requests_mock.post = MagicMock(return_value=response) + response.json = MagicMock(return_value=returning) + + def assert_called_with(): + requests_mock.post.assert_called_with(auth=self.auth, timeout=self.timeout, url=self.url, data=kwargs, + files=files, + headers={'user-agent': 'python-ubersmithclient'}) + response.json.assert_called_with() + + return assert_called_with diff --git a/tests/ubersmith_request_test.py b/tests/ubersmith_request_test.py new file mode 100644 index 0000000..6d026af --- /dev/null +++ b/tests/ubersmith_request_test.py @@ -0,0 +1,90 @@ +# Copyright 2016 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import ubersmith_client +from mock import Mock, patch +from hamcrest import assert_that, raises, calling, equal_to +from requests.exceptions import ConnectionError, Timeout + +from ubersmith_client import exceptions +from ubersmith_client.ubersmith_request import UbersmithRequest + +from tests.ubersmith_json.response_data_structure import a_response_data + + +class UbersmithRequestTest(unittest.TestCase): + def setUp(self): + self.url = 'http://ubersmith.example.com/' + self.username = 'admin' + self.password = 'test' + + def test_process_ubersmith_response(self): + response = Mock(status_code=200, headers={'content-type': 'application/json'}) + + json_data = { + 'client_id': '1', + 'first': 'Rick', + 'last': 'Sanchez', + 'company': 'Wubba lubba dub dub!' + } + + response.json = Mock(return_value=a_response_data(data=json_data)) + + self.assertDictEqual(json_data, UbersmithRequest.process_ubersmith_response(response)) + + def test_process_ubersmith_response_not_application_json(self): + response = Mock(status_code=200, headers={'content-type': 'text/html'}, content='42') + assert_that(response.content, equal_to(UbersmithRequest.process_ubersmith_response(response))) + + def test_process_ubersmith_response_raise_exception(self): + response = Mock(status_code=400, headers={'content-type': 'application/json'}) + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.BadRequest)) + + response.status_code = 401 + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.Unauthorized)) + + response.status_code = 403 + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.Forbidden)) + + response.status_code = 404 + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.NotFound)) + + response.status_code = 500 + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.UnknownError)) + + response.status_code = 200 + response.json = Mock(return_value={'status': False, 'error_code': 42, 'error_message': 'come and watch tv'}) + assert_that(calling(UbersmithRequest.process_ubersmith_response).with_args(response), + raises(exceptions.UbersmithException, 'Error code 42 - message: come and watch tv')) + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_method_returns_handle_connection_error_exception(self, requests_mock): + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + requests_mock.post = Mock(side_effect=ConnectionError()) + + assert_that(calling(ubersmith_api.client.list), raises(exceptions.UbersmithConnectionError)) + + @patch('ubersmith_client.ubersmith_request_post.requests') + def test_api_method_returns_handle_timeout_exception(self, requests_mock): + ubersmith_api = ubersmith_client.api.init(self.url, self.username, self.password) + requests_mock.post = Mock(side_effect=Timeout()) + + assert_that(calling(ubersmith_api.client.list), raises(exceptions.UbersmithTimeout)) diff --git a/tox.ini b/tox.ini index 6f9d47d..ef011f1 100755 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,16 @@ [tox] -envlist = py34,py27 +envlist = py34,py27,pep8 [testenv] deps = -r{toxinidir}/test-requirements.txt commands = nosetests + +[testenv:pep8] +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} + +[flake8] +show-source = True +max-line-length = 120 +exclude = .venv,.git,.tox,dist,doc,*egg,build,ubersmith_client/__init__.py diff --git a/ubersmith_client/__init__.py b/ubersmith_client/__init__.py index f44d680..5921267 100644 --- a/ubersmith_client/__init__.py +++ b/ubersmith_client/__init__.py @@ -10,4 +10,7 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. + +from . import api +from . import exceptions diff --git a/ubersmith_client/_http_utils.py b/ubersmith_client/_http_utils.py new file mode 100644 index 0000000..0de20b7 --- /dev/null +++ b/ubersmith_client/_http_utils.py @@ -0,0 +1,47 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def form_encode(data): + exploded_data = {} + for k, v in data.items(): + items = _explode_enumerable(k, v) + for new_key, new_val in items: + exploded_data[new_key] = new_val + return exploded_data + + +def form_encode_without_files(data): + return form_encode({k: v for k, v in data.items() if k is not 'files'}) + + +def _explode_enumerable(k, v): + exploded_items = [] + if isinstance(v, list) or isinstance(v, tuple): + if len(v) == 0: + exploded_items.append((k, v)) + else: + for idx, item in enumerate(v): + current_key = '{}[{}]'.format(k, idx) + exploded_items.extend(_explode_enumerable(current_key, item)) + elif isinstance(v, dict): + if len(v) == 0: + exploded_items.append((k, v)) + else: + for idx, item in v.items(): + current_key = '{}[{}]'.format(k, idx) + exploded_items.extend(_explode_enumerable(current_key, item)) + else: + exploded_items.append((k, v)) + return exploded_items diff --git a/ubersmith_client/api.py b/ubersmith_client/api.py index 5796838..c6c39ba 100644 --- a/ubersmith_client/api.py +++ b/ubersmith_client/api.py @@ -11,71 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import requests -from ubersmith_client.exceptions import UbersmithException, get_exception_for +from ubersmith_client.ubersmith_api import UbersmithApi -def init(url, user, password): - return UbersmithApi(url, user, password) - - -class UbersmithApi(object): - def __init__(self, url, user, password): - self.url = url - self.user = user - self.password = password - - def __getattr__(self, module): - return UbersmithRequest(self.url, self.user, self.password, module) - - -class UbersmithRequest(object): - def __init__(self, url, user, password, module): - self.url = url - self.user = user - self.password = password - self.module = module - self.methods = [] - self.http_methods = {'GET': 'get', 'POST': 'post'} - - def __getattr__(self, function): - self.methods.append(function) - return self - - def __call__(self, **kwargs): - return self.http_get(**kwargs) - - def process_request(self, http_method, **kwargs): - callable_http_method = getattr(requests, http_method) - response = callable_http_method(self.url, auth=(self.user, self.password), **kwargs) - - if response.status_code < 200 or response.status_code >= 400: - raise get_exception_for(status_code=response.status_code) - - response_json = response.json() - if not response_json['status']: - raise UbersmithException( - 500, - "error {0}, {1}".format(response_json['error_code'], response_json['error_message']) - ) - - return response.json()["data"] - - def http_get(self, **kwargs): - self._build_request_params(kwargs) - - response = self.process_request(self.http_methods.get('GET'), params=kwargs) - - return response - - def http_post(self, **kwargs): - self._build_request_params(kwargs) - - response = self.process_request(self.http_methods.get('POST'), data=kwargs) - - return response - - def _build_request_params(self, kwargs): - _methods = ".".join(self.methods) - kwargs['method'] = "{0}.{1}".format(self.module, _methods) +def init(url, user, password, timeout=60, use_http_get=False): + return UbersmithApi(url, user, password, timeout, use_http_get) diff --git a/ubersmith_client/exceptions.py b/ubersmith_client/exceptions.py index 090ee43..1724c44 100644 --- a/ubersmith_client/exceptions.py +++ b/ubersmith_client/exceptions.py @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + + class UbersmithException(Exception): code = None message = None @@ -58,3 +60,15 @@ def __init__(self): class UnknownError(UbersmithException): def __init__(self, code): super(UnknownError, self).__init__(code=code, message='An unknown error occurred') + + +class UbersmithConnectionError(UbersmithException): + def __init__(self, url): + super(UbersmithConnectionError, self).__init__(message='Could not connect to {0}'.format(url)) + + +class UbersmithTimeout(UbersmithException): + def __init__(self, url, timeout): + super(UbersmithTimeout, self) \ + .__init__( + message='Trying to connect to {url} timed out after {timeout} seconds'.format(url=url, timeout=timeout)) diff --git a/ubersmith_client/ubersmith_api.py b/ubersmith_client/ubersmith_api.py new file mode 100644 index 0000000..2431281 --- /dev/null +++ b/ubersmith_client/ubersmith_api.py @@ -0,0 +1,28 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ubersmith_client.ubersmith_request_get import UbersmithRequestGet +from ubersmith_client.ubersmith_request_post import UbersmithRequestPost + + +class UbersmithApi(object): + def __init__(self, url, user, password, timeout, use_http_get): + self.url = url + self.user = user + self.password = password + self.timeout = float(timeout) + self.ubersmith_request = UbersmithRequestGet if use_http_get else UbersmithRequestPost + + def __getattr__(self, module): + return self.ubersmith_request(self.url, self.user, self.password, module, self.timeout) diff --git a/ubersmith_client/ubersmith_request.py b/ubersmith_client/ubersmith_request.py new file mode 100644 index 0000000..15abb32 --- /dev/null +++ b/ubersmith_client/ubersmith_request.py @@ -0,0 +1,62 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +from requests import Timeout, ConnectionError + +from ubersmith_client import exceptions + + +class UbersmithRequest(object): + def __init__(self, url, user, password, module, timeout): + self.url = url + self.user = user + self.password = password + self.module = module + self.methods = [] + self.timeout = timeout + + def __getattr__(self, function): + self.methods.append(function) + return self + + @abstractmethod + def __call__(self, **kwargs): + raise AttributeError + + def _process_request(self, method, **kwargs): + try: + return method(**kwargs) + except ConnectionError: + raise exceptions.UbersmithConnectionError(self.url) + except Timeout: + raise exceptions.UbersmithTimeout(self.url, self.timeout) + + def _build_request_params(self, kwargs): + _methods = '.'.join(self.methods) + kwargs['method'] = '{0}.{1}'.format(self.module, _methods) + + @staticmethod + def process_ubersmith_response(response): + if response.status_code < 200 or response.status_code >= 400: + raise exceptions.get_exception_for(status_code=response.status_code) + + if response.headers['content-type'] == 'application/json': + response_json = response.json() + if not response_json['status']: + raise exceptions.UbersmithException(response_json['error_code'], + response_json['error_message']) + return response_json['data'] + + return response.content diff --git a/ubersmith_client/ubersmith_request_get.py b/ubersmith_client/ubersmith_request_get.py new file mode 100644 index 0000000..e35c936 --- /dev/null +++ b/ubersmith_client/ubersmith_request_get.py @@ -0,0 +1,36 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + +from ubersmith_client import _http_utils +from ubersmith_client.ubersmith_request import UbersmithRequest + + +class UbersmithRequestGet(UbersmithRequest): + def __call__(self, **kwargs): + self._build_request_params(kwargs) + params = _http_utils.form_encode_without_files(kwargs) + requests_get_args = dict(method=requests.get, + url=self.url, + auth=(self.user, self.password), + timeout=self.timeout, + headers={'user-agent': 'python-ubersmithclient'}, + params=params) + if 'files' in kwargs: + requests_get_args['files'] = kwargs['files'] + + response = self._process_request(**requests_get_args) + + return UbersmithRequest.process_ubersmith_response(response) diff --git a/ubersmith_client/ubersmith_request_post.py b/ubersmith_client/ubersmith_request_post.py new file mode 100644 index 0000000..b2d6cab --- /dev/null +++ b/ubersmith_client/ubersmith_request_post.py @@ -0,0 +1,37 @@ +# Copyright 2017 Internap. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + +from ubersmith_client import _http_utils +from ubersmith_client.ubersmith_request import UbersmithRequest + + +class UbersmithRequestPost(UbersmithRequest): + def __call__(self, **kwargs): + self._build_request_params(kwargs) + params = _http_utils.form_encode_without_files(kwargs) + + requests_post_args = dict(method=requests.post, + url=self.url, + auth=(self.user, self.password), + timeout=self.timeout, + headers={'user-agent': 'python-ubersmithclient'}, + data=params) + if 'files' in kwargs: + requests_post_args['files'] = kwargs['files'] + + response = self._process_request(**requests_post_args) + + return UbersmithRequest.process_ubersmith_response(response)