diff --git a/.github/workflows/pulls.yml b/.github/workflows/pulls.yml new file mode 100644 index 0000000..658629f --- /dev/null +++ b/.github/workflows/pulls.yml @@ -0,0 +1,60 @@ +name: Pulls + +on: pull_request +jobs: + build: + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + python-version: ['2.7', '3.4', '3.5', '3.6', '3.7', '3.8', '3.9'] + include: + - tox_env: 'py27' + python-version: '2.7' + - tox_env: 'py34' + python-version: '3.4' + - tox_env: 'py35' + python-version: '3.5' + - tox_env: 'py36' + python-version: '3.6' + - tox_env: 'latest' + python-version: '3.7' + - tox_env: 'latest' + python-version: '3.8' + - tox_env: 'latest' + python-version: '3.9' + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install pbr + pip install tox + tox -e ${{ matrix.tox_env }} --notest + - name: Run + run: | + tox -e ${{ matrix.tox_env }} -- --nocapture + lint: + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + python-version: ['2.7', '3.4', '3.5', '3.6', '3.7', '3.8', '3.9'] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install pbr + pip install --upgrade pip + pip install tox + tox -r -e pep8 --notest + - name: lint + run: | + tox -e pep8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4b12d74 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: release + +on: + release: + types: [released, prereleased] + +jobs: + build-and-publish-pypi: + permissions: + contents: read + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python_version: ["3.9"] + tox_env: ['latest'] + steps: + - uses: actions/checkout@v3 + - name: "Set up Python ${{ matrix.python_version }}" + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python_version }} + - name: Install dependencies + run: python -m pip install --upgrade pip + - name: Install building dependencies + run: pip install wheel tox + - name: Build venv + run: | + tox -e ${{ matrix.tox_env }} --notest + - name: Run + run: | + rm -rf dist || true + .tox/${{ matrix.tox_env }}/bin/python setup.py sdist bdist_wheel + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event_name == 'release' + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c50f9c0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: python -python: -- '2.7' -- '3.4' -install: -- pip install -r requirements.txt -- pip install -r test-requirements.txt -script: nosetests -deploy: - provider: pypi - user: internaphosting - password: - secure: fZxfMePJQroeiLQb+lmpNiEEMvRONOVt4yTQY8CmemyVMOe4TVZUfsBzCMa7eZkUaFZuHfNaUo/4Jmhn15EruzGhqBLRe5fpuFxFqJqDJ7ZmPMTRBIMKUsRuYrG6vc9YmG8CLfLvxwG8OUoLzND+rz+QwWaFruFeDlkCN3xMpzRHcdopcKlKoKwUJCkPzqEm0qeziTsRNixLbrBHzfr+FY96FDclE2GDywUBCacPzwP+R9QwPKU74NevlD+MazP/bYcZHY8DKDQ2X4L7NSr/8osJswVvCCoOqqqFPsPLaVzhDLU1hvTSW44K3hUSlRmKWzmdfFutkPix4Zrge9JYzTOodV921WgFLSsSPcpwEoRMZM21hoFhO6PThd9ag33z1rUxXzAJxBYfjM5OP+2g6gqWDptyJp8OgdqjzoCKFsXBrINf+vb+Ossdtl2+W1fzBjHlV9D8t6gf7ekJZmTAKzfYcvrRc++gnZwlwmPWrr5qBUSxT9SEqln7L/u4RbHxZdAOzYivPBvrL1ncdwUx9kvzbelr1wolc0aszodRgx8S7aWZ7V4CbuE6LpNkRREJ+EY71MYInpCRO81XabAIEG7T7i11ZkNexxQAe5Mafy/nSw5cGWirIAcLEO7WU3tSd5rteP7hgp28VJ9XfGZPwH9Q1FqiNMpKk+Db6PwplDE= - on: - tags: true - python: 3.4 - distributions: sdist bdist_wheel - repo: internap/python-ubersmithclient 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/constraints.txt b/constraints.txt new file mode 100644 index 0000000..fcb695b --- /dev/null +++ b/constraints.txt @@ -0,0 +1,29 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --no-emit-trusted-host --no-index --output-file=constraints.txt setup.py test-requirements.txt +# +certifi==2021.10.8 # via requests +chardet==3.0.4 # via requests +click==7.0 # via pip-tools +flake8==3.8.4 +idna==2.8 # via requests +importlib-metadata==1.1.3 # via flake8 +mccabe==0.6.1 # via flake8 +mock==3.0.5 +nose==1.3.7 +pathlib2==2.3.7.post1 # via importlib-metadata +pip-tools==3.9.0 +pycodestyle==2.6.0 # via flake8 +pyflakes==2.2.0 # via flake8 +pyhamcrest==1.9.0 +requests==2.21.0 +scandir==1.10.0 # via pathlib2 +six==1.16.0 # via mock, pathlib2, pip-tools, pyhamcrest +typing==3.10.0.0 # via flake8, pathlib2 +urllib3==1.24.3 # via requests +zipp==1.2.0 # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools==43.0.0 # via pyhamcrest diff --git a/requirements.txt b/requirements.txt index c7317e0..7aa47e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests<=2.9.1 +requests==2.21.0;python_version<'3.6' +requests;python_version>='3.6' \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 0987682..91fc041 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 @@ -11,6 +11,12 @@ classifier = Operating System :: POSIX Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 +description-file = README.rst +description-content-type = text/x-rst; charset=UTF-8 [files] packages = 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..7178b47 100755 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,5 @@ -nose==1.2.1 -requests-mock==0.7.0 -pyhamcrest==1.8.1 \ No newline at end of file +nose +pyhamcrest<2.0.0 +mock +flake8 +pip-tools 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..b19a17b 100755 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,33 @@ [tox] -envlist = py34,py27 +envlist = py34,py35,py36,py37,py38,py27,pep8 [testenv] deps = -r{toxinidir}/test-requirements.txt +install_command = python -m pip install -c constraints.txt {opts} {packages} commands = - nosetests + nosetests {posargs} + +[testenv:latest] +deps = -r{toxinidir}/test-requirements.txt +basepython = python3 +install_command = python -m pip install {opts} {packages} +commands = + nosetests {posargs} + +[testenv:pep8] +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} + +[testenv:bump-dependencies] +basepython = python3.4 +skip_install = true +deps = + pip-tools +commands = + pip-compile --upgrade --no-index --no-emit-trusted-host --output-file constraints.txt setup.py test-requirements.txt + + +[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..6752777 --- /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 != '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)