From ffb4bc0f215436ca5e8e7eec1b0bc77f69b063b1 Mon Sep 17 00:00:00 2001 From: kolatz <36399892+kolatz@users.noreply.github.com> Date: Fri, 9 Mar 2018 16:03:53 +0300 Subject: [PATCH 1/3] add appveyor ci [ci skip] --- .appveyor.yml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 7 ++++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..7033c7b --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,46 @@ +# https://www.appveyor.com/docs/appveyor-yml/ + +version: '{build}' +max_jobs: 6 +build: off +shallow_clone: true +# clone_depth: 1 +environment: + fast_finish: true + matrix: + + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.4.4" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.4" + PYTHON_ARCH: "64" + + - PYTHON: "C:\\Python35" + PYTHON_VERSION: "3.5.3" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.3" + PYTHON_ARCH: "64" + + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.4" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.4" + PYTHON_ARCH: "64" + +init: + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" +install: + - "pip install --disable-pip-version-check --user --upgrade pip" + - "pip install -e ." + - "pip install -e .[dev]" +test_script: + - "%CMD_IN_ENV% python setup.py test" diff --git a/README.rst b/README.rst index 73cf92d..9990633 100644 --- a/README.rst +++ b/README.rst @@ -9,14 +9,19 @@ README - | |docs| * - Tests - | |build1| |requires| + | |appveyor| |coveralls| | |codacy| |codeclimate| - | |coveralls| * - Package - | |supported-versions| |supported-implementations| | |dev-status| |pypi-version| |license| * - GitHub - | |gh-release| |gh-tag| |gh-issues| + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/hptdwfa7mcsu5tla/branch/master?svg=true + :target: https://ci.appveyor.com/project/kolatz/umbr-api/ + :alt: Appveyor Build Status + .. |coveralls| image:: https://coveralls.io/repos/github/kolatz/umbr_api/badge.svg?branch=release%2F0.3 :target: https://coveralls.io/github/kolatz/umbr_api?branch=release%2F0.3 :alt: coveralls From 936c6e384dd903d59617f50e86a4be3bdbeb1a34 Mon Sep 17 00:00:00 2001 From: kolatz <36399892+kolatz@users.noreply.github.com> Date: Sat, 10 Mar 2018 01:48:59 +0300 Subject: [PATCH 2/3] remove duplicated code from _http_requests --- tests/test_offline_add.py | 6 ++--- tests/test_offline_get.py | 6 ++--- tests/test_offline_remove.py | 6 ++--- tests/test_offline_umbrella.py | 2 +- umbr_api/_http_requests.py | 42 ++++++++++------------------------ 5 files changed, 22 insertions(+), 40 deletions(-) diff --git a/tests/test_offline_add.py b/tests/test_offline_add.py index e2cb0a0..c5aade5 100644 --- a/tests/test_offline_add.py +++ b/tests/test_offline_add.py @@ -46,7 +46,7 @@ def test_add_main(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.post') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response main(test_key=FAKE_KEY) @@ -73,7 +73,7 @@ def test_add(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.post') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.add(domain='example.com', url='example.com', key=FAKE_KEY, @@ -103,7 +103,7 @@ def test_add_fail(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.post') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.add(domain='example.com', url='2example.com', key=FAKE_KEY, diff --git a/tests/test_offline_get.py b/tests/test_offline_get.py index d60eb1a..59bfc69 100644 --- a/tests/test_offline_get.py +++ b/tests/test_offline_get.py @@ -46,7 +46,7 @@ def test_get_main(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.get') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response main(test_key=FAKE_KEY) @@ -73,7 +73,7 @@ def test_get(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.get') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.get_list(page=1, limit=10, key=FAKE_KEY) assert response.status_code == status_code @@ -101,7 +101,7 @@ def test_get_fail(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.get') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.get_list(page=1, limit=201, key=FAKE_KEY) assert response.status_code == status_code diff --git a/tests/test_offline_remove.py b/tests/test_offline_remove.py index 6a9ed4b..3e28672 100644 --- a/tests/test_offline_remove.py +++ b/tests/test_offline_remove.py @@ -46,7 +46,7 @@ def test_remove_main(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.delete') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response main(test_key=FAKE_KEY) @@ -73,7 +73,7 @@ def test_remove(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.delete') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.remove(record_id='29765170', domain_name='www.example.com', @@ -103,7 +103,7 @@ def test_remove_fail(self): my_response = FakeResponse(status_code, headers, body_text) - with mock.patch('requests.delete') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.remove(domain_name='www.example.com', key=FAKE_KEY) diff --git a/tests/test_offline_umbrella.py b/tests/test_offline_umbrella.py index 4e1e17d..d9e3b77 100644 --- a/tests/test_offline_umbrella.py +++ b/tests/test_offline_umbrella.py @@ -52,7 +52,7 @@ def test_umbrella_main(self): keyring_add=None, remove_domain=None, remove_id=None, verbose=2, version=False) - with mock.patch('requests.get') as mock_requests_post: + with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response with self.assertRaises(SystemExit) as expected_exc: diff --git a/umbr_api/_http_requests.py b/umbr_api/_http_requests.py index 67ffea0..89952aa 100644 --- a/umbr_api/_http_requests.py +++ b/umbr_api/_http_requests.py @@ -7,12 +7,21 @@ def send_get(url): """Send HTTP GET request via 'requests' module.""" + return send_any('GET', url) + + +def send_any(method, url, headers=None, data=None): + """Send HTTP request via 'requests' module.""" assert url assert isinstance(url, str) logger.info('Requesting: %s', url) + if headers: + logger.debug('Headers to send: %s', str(headers)) + if data: + logger.debug('Data to send: %s', str(data)) try: - response = requests.get(url) + response = requests.request(method, url, headers=headers, data=data) except requests.exceptions.RequestException as err_msg: logger.exception(err_msg) response = None @@ -24,39 +33,12 @@ def send_get(url): def send_post(url, data=None, headers=None): """Send HTTP POST request via 'requests' module.""" - assert url - assert isinstance(url, str) - logger.info('Requesting: %s', url) - logger.debug('Data to send: %s', str(data)) - logger.debug('Headers to send: %s', str(headers)) - try: - response = requests.post(url, - data=data, - headers=headers) - except requests.exceptions.RequestException as err_msg: - logger.exception(err_msg) - response = None - else: - response_logging(response) - - return response + return send_any('POST', url, data=data, headers=headers) def send_delete(url, headers=None): """Send HTTP DELETE request via 'requests' module.""" - assert url - assert isinstance(url, str) - logger.info('Requesting: %s', url) - logger.debug('Headers to send: %s', str(headers)) - try: - response = requests.delete(url, headers=headers) - except requests.exceptions.RequestException as err_msg: - logger.exception(err_msg) - response = None - else: - response_logging(response) - - return response + return send_any('DELETE', url, headers=headers) def response_logging(response): From 048432bf0685d6f2f3088bb13681f85b4f7b219a Mon Sep 17 00:00:00 2001 From: kolatz <36399892+kolatz@users.noreply.github.com> Date: Fri, 9 Mar 2018 18:56:59 +0300 Subject: [PATCH 3/3] clean up --- Makefile | 9 +- setup.py | 68 +++++----- tests/offline_utils.py | 34 +++++ tests/test_offline_add.py | 72 +--------- tests/test_offline_get.py | 72 +--------- tests/test_offline_remove.py | 75 +---------- tests/test_offline_umbrella.py | 64 ++++----- tests/test_umbrella.py | 40 ++---- umbr_api/__about__.py | 20 +-- umbr_api/__init__.py | 4 +- umbr_api/_http_requests.py | 15 ++- umbr_api/add.py | 39 +++--- umbr_api/get.py | 15 +-- umbr_api/remove.py | 15 +-- umbr_api/umbrella.py | 238 ++++++++++++++++++++------------- 15 files changed, 334 insertions(+), 446 deletions(-) create mode 100644 tests/offline_utils.py diff --git a/Makefile b/Makefile index 1708129..533076c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: clear-all install-dev test-offline docs built upload coverage-offline .PHONY: test test: clear-all install-dev - pytest -q --cache-clear tests/ + pytest -k '' -q --cache-clear tests/ .PHONY: test-online test-online: clear-all install-dev @@ -20,12 +20,17 @@ lint: pep257 -s -e tests/ pep257 -s -e examples/ pep257 -s -e setup.py + pylint umbr_api/ tests/*.py examples/ setup.py # flake8 --statistics --count --exclude venv .PHONY: install-dev install-dev: pip install -q -e .[dev] +.PHONY: install-doc +install-doc: + pip install -q -e .[doc] + .PHONY: coverage-offline coverage-offline: clear-pyc clear-cov coverage run -m pytest -k 'not Online' -q --cache-clear tests/ @@ -46,7 +51,7 @@ upload: clear-all built twine upload dist/* .PHONY: docs -docs: clear-pyc install-dev +docs: clear-pyc install-dev install-doc $(MAKE) -C docs html .PHONY: built diff --git a/setup.py b/setup.py index e1e61fd..a232165 100644 --- a/setup.py +++ b/setup.py @@ -5,24 +5,28 @@ from setuptools import setup with open(path.join(path.dirname(__file__), "README.rst")) as read_file: - long_description = read_file.read() + LONG_DESCRIPTION = read_file.read() -base_dir = path.dirname(__file__) -about = {} -with open(path.join(base_dir, "umbr_api", "__about__.py")) as py_file: +ABOUT = {} +# pylint: disable=C0330 +with open(path.join( + path.dirname(__file__), + "umbr_api", + "__about__.py", + )) as py_file: # pylint: disable=W0122 - exec(py_file.read(), about) + exec(py_file.read(), ABOUT) setup( - name=about["__title__"], - version=about["__version__"], - description=about["__summary__"], - long_description=long_description, - url=about["__uri__"], - author=about["__author__"], - author_email=about["__email__"], + name=ABOUT["__title__"], + version=ABOUT["__version__"], + description=ABOUT["__summary__"], + long_description=LONG_DESCRIPTION, + url=ABOUT["__uri__"], + author=ABOUT["__author__"], + author_email=ABOUT["__email__"], platforms="Darwin", - license=about["__license__"], + license=ABOUT["__license__"], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Education", @@ -40,45 +44,47 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: Implementation :: CPython" + "Programming Language :: Python :: Implementation :: CPython", ], - keywords='cisco umbrella opendns security', - packages=['umbr_api'], + keywords="cisco umbrella opendns security", + packages=["umbr_api"], install_requires=[ - 'requests >= 2.18', - 'logzero >= 1.3.1', - 'keyring >= 11.0.0', + "requests >= 2.18", + "logzero >= 1.3.1", + "keyring >= 11.0.0", ], extras_require={ - 'dev': [ + "dev": [ "coverage>=4.5.1", "pytest>=3.4.1", "setuptools>=38.5.1", + "twine>=1.9.1", + ], + "doc": [ "Sphinx>=1.7.1", "sphinx_rtd_theme>=0.2.4", - "twine>=1.9.1" ], - 'dev_lint': [ + "dev_lint": [ "autopep8>=1.3.4", "pep257>=0.7.0", "pycodestyle>=2.3.1", "pydocstyle>=2.1.1", - "pylint>=1.8.2" + "pylint>=1.8.2", ], }, package_data={ - 'umbr_api': ['data/customer_key_example.json'], + "umbr_api": ["data/customer_key_example.json"], }, entry_points={ - 'console_scripts': [ - 'umbrella=umbr_api.umbrella:main', + "console_scripts": [ + "umbrella=umbr_api.umbrella:main", ], }, project_urls={ - 'Cisco Umbrella': 'https://umbrella.cisco.com/', - 'Cisco Umbrella Enforcement API': 'https://docs.umbrella.com/' - 'developer/enforcement-api/' + "Cisco Umbrella": "https://umbrella.cisco.com/", + "Cisco Umbrella Enforcement API": "https://docs.umbrella.com/" + "developer/enforcement-api/", }, - setup_requires=['pytest-runner'], - tests_require=['pytest'], + setup_requires=["pytest-runner"], + tests_require=["pytest"], ) diff --git a/tests/offline_utils.py b/tests/offline_utils.py new file mode 100644 index 0000000..99247ba --- /dev/null +++ b/tests/offline_utils.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# pylint: disable=R0201 +"""Test unit.""" + +import unittest +from ast import literal_eval +import os + + +# pylint: disable=R0903 +class FakeResponse(): + """To mimic ``requests`` response obj.""" + + def __init__(self, case_dir): + """Create fake class.""" + status_code_file = os.path.join(os.path.dirname(__file__), + case_dir, + 'code.txt') + body_file = os.path.join(os.path.dirname(__file__), + case_dir, + 'body.json') + headers_file = os.path.join(os.path.dirname(__file__), + case_dir, + 'headers.json') + with open(status_code_file) as file: + self.status_code = int(file.read()) + with open(headers_file) as file: + self.headers = literal_eval(file.read()) + with open(body_file) as file: + self.text = file.read() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_offline_add.py b/tests/test_offline_add.py index c5aade5..3035465 100644 --- a/tests/test_offline_add.py +++ b/tests/test_offline_add.py @@ -3,23 +3,11 @@ """Test unit.""" import unittest -from ast import literal_eval -import os +from offline_utils import FakeResponse FAKE_KEY = 'YOUR-CUSTOMER-KEY-IS-HERE-0123456789' -# pylint: disable=R0903 -class FakeResponse(): - """To mimic ``requests`` response obj.""" - - def __init__(self, code, headers, body_txt): - """Create fake class.""" - self.status_code = code - self.headers = headers - self.text = body_txt - - class TestCaseMocking(unittest.TestCase): """Main class.""" @@ -28,23 +16,7 @@ def test_add_main(self): from unittest import mock from umbr_api.add import main - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/add/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response @@ -55,60 +27,28 @@ def test_add(self): from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/add/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.add(domain='example.com', url='example.com', key=FAKE_KEY, bypass=False) - assert response.status_code == status_code + assert response.status_code == my_response.status_code def test_add_fail(self): """Call add to fail.""" from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case2', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case2', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/add/case2', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/add/case2') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.add(domain='example.com', url='2example.com', key=FAKE_KEY, bypass=True) - assert response.status_code == status_code + assert response.status_code == my_response.status_code if __name__ == '__main__': diff --git a/tests/test_offline_get.py b/tests/test_offline_get.py index 59bfc69..bfbcd11 100644 --- a/tests/test_offline_get.py +++ b/tests/test_offline_get.py @@ -3,23 +3,11 @@ """Test unit.""" import unittest -from ast import literal_eval -import os +from offline_utils import FakeResponse FAKE_KEY = 'YOUR-CUSTOMER-KEY-IS-HERE-0123456789' -# pylint: disable=R0903 -class FakeResponse(): - """To mimic ``requests`` response obj.""" - - def __init__(self, code, headers, body_txt): - """Create fake class.""" - self.status_code = code - self.headers = headers - self.text = body_txt - - class TestCaseMocking(unittest.TestCase): """Main class.""" @@ -28,23 +16,7 @@ def test_get_main(self): from unittest import mock from umbr_api.get import main - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/get/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response @@ -55,56 +27,24 @@ def test_get(self): from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/get/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.get_list(page=1, limit=10, key=FAKE_KEY) - assert response.status_code == status_code + assert response.status_code == my_response.status_code def test_get_fail(self): """Call add to fail.""" from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case2', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case2', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case2', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/get/case2') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.get_list(page=1, limit=201, key=FAKE_KEY) - assert response.status_code == status_code + assert response.status_code == my_response.status_code if __name__ == '__main__': diff --git a/tests/test_offline_remove.py b/tests/test_offline_remove.py index 3e28672..3d99101 100644 --- a/tests/test_offline_remove.py +++ b/tests/test_offline_remove.py @@ -3,23 +3,11 @@ """Test unit.""" import unittest -from ast import literal_eval -import os +from offline_utils import FakeResponse FAKE_KEY = 'YOUR-CUSTOMER-KEY-IS-HERE-0123456789' -# pylint: disable=R0903 -class FakeResponse(): - """To mimic ``requests`` response obj.""" - - def __init__(self, code, headers, body_txt): - """Create fake class.""" - self.status_code = code - self.headers = headers - self.text = body_txt - - class TestCaseMocking(unittest.TestCase): """Main class.""" @@ -28,23 +16,7 @@ def test_remove_main(self): from unittest import mock from umbr_api.remove import main - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/remove/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response @@ -55,59 +27,26 @@ def test_remove(self): from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/remove/case1') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response response = umbr_api.remove(record_id='29765170', - domain_name='www.example.com', key=FAKE_KEY) - assert response.status_code == status_code + assert response.status_code == my_response.status_code def test_remove_fail(self): """Call add to fail.""" from unittest import mock import umbr_api - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case2', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case2', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/remove/case2', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() - - my_response = FakeResponse(status_code, headers, body_text) + my_response = FakeResponse('data/templates/remove/case2') with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response - response = umbr_api.remove(domain_name='www.example.com', + response = umbr_api.remove(record_id='www.example.com', key=FAKE_KEY) - assert response.status_code == status_code + assert response.status_code == my_response.status_code if __name__ == '__main__': diff --git a/tests/test_offline_umbrella.py b/tests/test_offline_umbrella.py index d9e3b77..44e8d4c 100644 --- a/tests/test_offline_umbrella.py +++ b/tests/test_offline_umbrella.py @@ -2,55 +2,28 @@ """Test unit.""" import unittest -from ast import literal_eval -import os +from offline_utils import FakeResponse FAKE_KEY = 'YOUR-CUSTOMER-KEY-IS-HERE-0123456789' -# pylint: disable=R0903 -class FakeResponse(): - """To mimic ``requests`` response obj.""" - - def __init__(self, code, headers, body_txt): - """Create fake class.""" - self.status_code = code - self.headers = headers - self.text = body_txt - - class TestCaseMocking(unittest.TestCase): """Main class.""" - # pylint: disable=R0914 - def test_umbrella_main(self): + def test_umbrella_main_get(self): """Call main.""" import argparse from unittest import mock from umbr_api.umbrella import main - status_code_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'code.txt') - body_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'body.json') - headers_file = os.path.join(os.path.dirname(__file__), - 'data/templates/get/case1', - 'headers.json') - with open(status_code_file) as file: - status_code = int(file.read()) - with open(headers_file) as file: - headers = literal_eval(file.read()) - with open(body_file) as file: - body_text = file.read() + my_response = FakeResponse('data/templates/get/case1') - my_response = FakeResponse(status_code, headers, body_text) - - args = argparse.Namespace(add=None, get_list=2, - key=['YOUR-CUSTOMER-KEY-IS-HERE-0123456789'], - keyring_add=None, remove_domain=None, - remove_id=None, verbose=2, version=False) + args = argparse.Namespace( + command='get', + key=FAKE_KEY, + max_records=2, + verbose=2, + ) with mock.patch('requests.request') as mock_requests_post: mock_requests_post.return_value = my_response @@ -59,6 +32,25 @@ def test_umbrella_main(self): main(args) self.assertEqual(expected_exc.exception.code, 200) + def test_umbrella_main_keyring(self): + """Call main.""" + import argparse + from umbr_api.umbrella import main + + args = argparse.Namespace( + command='keyring', + key_to_add=None, + show=True, + verbose=0, + ) + + with self.assertRaises(SystemExit) as expected_exc: + main(args) + self.assertTrue( + expected_exc.exception.code == 0 or + expected_exc.exception.code == 1 + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_umbrella.py b/tests/test_umbrella.py index ffc8939..c6ad2e1 100644 --- a/tests/test_umbrella.py +++ b/tests/test_umbrella.py @@ -18,33 +18,13 @@ def test_with_empty_args(self): # cannot use for tests under diff environments self.assertIsNotNone(expected_exc.exception.code) - def test_version(self): - """User passes no args, should exit with SystemExit.""" - import argparse - from unittest import mock - from umbr_api.umbrella import main - - args = argparse.Namespace(add=None, get_list=None, key=None, - keyring_add=None, remove_domain=None, - remove_id=None, verbose=2, version=1) - - with mock.patch('umbr_api.umbrella.create_parser') as \ - mock_create_parser: - - mock_create_parser.return_value = args - with self.assertRaises(SystemExit) as expected_exc: - main() - self.assertEqual(expected_exc.exception.code, 0) - def test_verbose_level(self): """User passes no args, should exit with SystemExit.""" import argparse from unittest import mock from umbr_api.umbrella import main - args = argparse.Namespace(add=None, get_list=None, key=None, - keyring_add=None, remove_domain=None, - remove_id=None, verbose=1, version=1) + args = argparse.Namespace(command=None, verbose=1) with mock.patch('umbr_api.umbrella.create_parser') as \ mock_create_parser: @@ -81,9 +61,12 @@ def test_get(self): from unittest import mock from umbr_api.umbrella import main - args = argparse.Namespace(add=None, get_list=10, key=None, - keyring_add=None, remove_domain=None, - remove_id=None, verbose=2, version=None) + args = argparse.Namespace( + command='get', + key=None, + max_records=5, + verbose=2, + ) with mock.patch('umbr_api.umbrella.create_parser') as \ mock_create_parser: @@ -99,9 +82,12 @@ def test_get_fail(self): from unittest import mock from umbr_api.umbrella import main - args = argparse.Namespace(add=None, get_list=201, key=None, - keyring_add=None, remove_domain=None, - remove_id=None, verbose=1, version=None) + args = argparse.Namespace( + command='get', + key=None, + max_records=201, + verbose=2, + ) with mock.patch('umbr_api.umbrella.create_parser') as \ mock_create_parser: diff --git a/umbr_api/__about__.py b/umbr_api/__about__.py index 222cf87..2c6a84e 100644 --- a/umbr_api/__about__.py +++ b/umbr_api/__about__.py @@ -2,18 +2,18 @@ """Configure package wide attributes.""" __all__ = ( - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + '__title__', '__summary__', '__uri__', '__version__', '__author__', + '__email__', '__license__', '__copyright__', ) -__title__ = "umbr_api" -__summary__ = "Cisco Umbrella Enforcement API wrapper and command-line utility" -__uri__ = "https://github.com/kolatz/umbr_api" +__title__ = 'umbr_api' +__summary__ = 'Cisco Umbrella Enforcement API wrapper and command-line utility' +__uri__ = 'https://github.com/kolatz/umbr_api' -__version__ = '0.3.post1' +__version__ = '0.4' -__author__ = "kolatz" -__email__ = "private@example.com" +__author__ = 'kolatz' +__email__ = 'private@example.com' -__license__ = "MIT" -__copyright__ = "Copyright 2018 %s" % __author__ +__license__ = 'MIT' +__copyright__ = 'Copyright 2018 %s' % __author__ diff --git a/umbr_api/__init__.py b/umbr_api/__init__.py index 3a81d6e..b92fabe 100644 --- a/umbr_api/__init__.py +++ b/umbr_api/__init__.py @@ -10,6 +10,6 @@ from .remove import remove __all__ = ( - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + '__title__', '__summary__', '__uri__', '__version__', '__author__', + '__email__', '__license__', '__copyright__', ) diff --git a/umbr_api/_http_requests.py b/umbr_api/_http_requests.py index 89952aa..bc460df 100644 --- a/umbr_api/_http_requests.py +++ b/umbr_api/_http_requests.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Wrapper for request module calls.""" +import json import requests + from logzero import logger @@ -45,9 +47,18 @@ def response_logging(response): """Log responses.""" logger.info('Response code: %d', response.status_code) logger.info('Response headers: %s', str(response.headers)[:100]) - logger.debug('Response headers:\n%s', str(response.headers)) + logger.debug( + 'Response headers:\n%s', + json.dumps(dict(response.headers), indent=4) + ) logger.info('Response: %s', response.text[:100]) - logger.debug('Response:\n%s', response.text) + if response.text: + logger.debug( + 'Response:\n%s', + json.dumps(json.loads(response.text), indent=4) + ) + else: + logger.debug('Response: ') if __name__ == '__main__': diff --git a/umbr_api/add.py b/umbr_api/add.py index d9495dd..b7fa012 100644 --- a/umbr_api/add.py +++ b/umbr_api/add.py @@ -77,9 +77,9 @@ def add(domain=None, url=None, key=None, bypass=False): key if bypass: - bypass_str = "true" + bypass_str = 'true' else: - bypass_str = "false" + bypass_str = 'false' block_request_txt = """ {{ @@ -122,21 +122,26 @@ def format_response(response): def main(test_key=None): """Test if executed directly.""" - response = add(domain='www.example.com', - url='https://www.example.com/test', - key=test_key) - response = add(domain='www.example.com', - url='www.example.com', - key=test_key) - response = add(domain='www.example.com', - url='www.example.com/test', - key=test_key) - response = add(domain='www.example.com', - url='https://www.example.com/test', - key=test_key) - print(response.status_code, - json.dumps(dict(response.headers), indent=4), - json.dumps(json.loads(response.text), indent=4), sep='\n\n') + add( + domain='www.example.com', + url='https://www.example.com/test', + key=test_key, + ) + add( + domain='www.example.com', + url='www.example.com', + key=test_key, + ) + add( + domain='www.example.com', + url='www.example.com/test', + key=test_key, + ) + add( + domain='www.example.com', + url='https://www.example.com/test', + key=test_key, + ) if __name__ == '__main__': diff --git a/umbr_api/get.py b/umbr_api/get.py index db8fd65..7eff0b5 100644 --- a/umbr_api/get.py +++ b/umbr_api/get.py @@ -87,22 +87,13 @@ def format_response(code, json_response): def main(test_key=None): """Test if executed directly.""" # Standard request - response = get_list(key=test_key) - print(response.status_code, - json.dumps(dict(response.headers), indent=4), - json.dumps(json.loads(response.text), indent=4), sep='\n\n') + get_list(key=test_key) # Request with pagination - response = get_list(page=2, limit=2, key=test_key) - print(response.status_code, - json.dumps(dict(response.headers), indent=4), - json.dumps(json.loads(response.text), indent=4), sep='\n\n') + get_list(page=2, limit=2, key=test_key) # Request with pagination - response = get_list(page=1, limit=201, key=test_key) - print(response.status_code, - json.dumps(dict(response.headers), indent=4), - json.dumps(json.loads(response.text), indent=4), sep='\n\n') + get_list(page=1, limit=201, key=test_key) if __name__ == '__main__': diff --git a/umbr_api/remove.py b/umbr_api/remove.py index 63e9257..b92405d 100644 --- a/umbr_api/remove.py +++ b/umbr_api/remove.py @@ -7,7 +7,6 @@ """ import json -from urllib.parse import quote from logzero import logger from umbr_api._key import get_key from umbr_api._http_requests import send_delete @@ -15,24 +14,15 @@ APP_JSON = {'Content-Type': 'application/json'} -def remove(record_id=None, domain_name=None, key=None): +def remove(record_id=None, key=None): """Remove a record from the policy.""" key = get_key(key=key) response = None - if record_id and domain_name: - logger.warning('Both arguments are used. "id" is preferred.') - if record_id: api_uri = """https://s-platform.api.opendns.com/1.0/domains/""" + \ """{0}?customerKey={1}""".format(record_id, key) response = send_delete(api_uri, headers=APP_JSON) - elif domain_name and not record_id: - safe_url = quote(domain_name, safe='') - - api_uri = """https://s-platform.api.opendns.com/1.0/domains/""" + \ - """{0}?customerKey={1}""".format(safe_url, key) - response = send_delete(api_uri, headers=APP_JSON) format_response(response) @@ -65,12 +55,11 @@ def format_response(response): def main(test_key=None): """Test if executed directly.""" response = remove(record_id='29765170', - domain_name='www.example.com', key=test_key) print(response.status_code, json.dumps(dict(response.headers), indent=4), sep='\n\n') - response = remove(domain_name='www.example.com', + response = remove(record_id='www.example.com', key=test_key) print(response.status_code, json.dumps(dict(response.headers), indent=4), sep='\n\n') diff --git a/umbr_api/umbrella.py b/umbr_api/umbrella.py index b36a24d..a96e324 100644 --- a/umbr_api/umbrella.py +++ b/umbr_api/umbrella.py @@ -4,12 +4,12 @@ Examples: .. code:: bash - umbrella --add www.example.com http://www.example.com/images - umbrella --remove-domain www.example.com - umbrella --remove-id 297XXXXX --k YOUR-CUSTOMER-KEY-IS-HERE-0123456789 - umbrella --get-list 100 - umbrella --get-list --key YOUR-CUSTOMER-KEY-IS-HERE-0123456789 - umbrella --keyring-add YOUR-CUSTOMER-KEY-IS-HERE-0123456789 + umbrella add www.example.com http://www.example.com/images + umbrella del www.example.com + umbrella del 297XXXXX --key YOUR-CUSTOMER-KEY-IS-HERE-0123456789 + umbrella -vv get 50 + umbrella get --key YOUR-CUSTOMER-KEY-IS-HERE-0123456789 + umbrella keyring --add YOUR-CUSTOMER-KEY-IS-HERE-0123456789 References: https://docs.umbrella.com/developer/enforcement-api/ @@ -27,9 +27,9 @@ from umbr_api.add import add from umbr_api.remove import remove -LOG_FORMAT = '%(asctime)s.%(msecs)03d %(module)13s[%(lineno)4d] ' + \ - '%(threadName)10s %(color)s%(levelname)8s%(end_color)s ' + \ - '%(message)s' +LOG_FORMAT = '%(asctime)s.%(msecs)03d %(module)13s[%(lineno)4d] ' \ + + '%(threadName)10s %(color)s%(levelname)8s%(end_color)s ' \ + + '%(message)s' FORMATTER = logzero.LogFormatter(fmt=LOG_FORMAT, datefmt='%H:%M:%S') logzero.setup_default_logger(formatter=FORMATTER, level=logging.WARNING) @@ -37,60 +37,112 @@ def create_parser(): """Create argparse parser, return args.""" - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse. - RawDescriptionHelpFormatter, - epilog='') - - group = parser.add_mutually_exclusive_group() - - group.add_argument('--add', - help='Add domain to the block list; The first ' - 'argument represent DNS domain name, the second ' - 'URL address; Two arguments are required.', - nargs=2, - type=str) - - group.add_argument('--remove-domain', - help='Remove a record from the block list ' - 'by DNS domain name', - nargs=1, - type=str) - - group.add_argument('--remove-id', - help='Remove a record from the block list ' - 'by record id', - nargs=1, - type=str) - - group.add_argument('--get-list', - help='Get the block list', - nargs='?', - const=10, - type=int) - - parser.add_argument('--key', - help='Specify API key', - nargs=1, - type=str) - - parser.add_argument('--force', - help='Bypass Domain Acceptance Process while adding', - action='store_true') - - group.add_argument('--keyring-add', - help='Add API key to the keyring (MacOS)', - nargs=1, - type=str) - - parser.add_argument('-v', '--verbose', - help='Enable detailed logging; Option is additive, ' - 'can be used up to 2 times', - action='count') - - parser.add_argument('-V', '--version', - help='Show version', - action='store_true') + parser = argparse.ArgumentParser( + description=__doc__, + prog='umbrella', + formatter_class=argparse. + RawDescriptionHelpFormatter, + epilog='', + ) + + # optional arguments + parser.add_argument( + '-V', '--version', + help='Show version', + action='version', + version=umbr_api.__version__, + ) + parser.add_argument( + '-v', '--verbose', + help='Enable detailed logging; Option is additive, ' + 'can be used up to 2 times', + action='count', + ) + subparsers = parser.add_subparsers( + title='commands', + dest='command', + ) + + # add command + parser_add = subparsers.add_parser( + 'add', + help='Add domain to the block list', + ) + parser_add.add_argument( + 'dns_name', + help='DNS address to block', + type=str, + ) + parser_add.add_argument( + 'url', + help='URL address to block', + type=str, + ) + parser_add.add_argument( + '--force', + help='Bypass Domain Acceptance Process ' + 'while adding', + action='store_true' + ) + parser_add.add_argument( + '--key', + help='Specify API key', + type=str, + ) + # get command + parser_get = subparsers.add_parser( + 'get', + help='Show the current block list', + ) + parser_get.add_argument( + '--key', + help='Specify API key', + type=str, + default=None, + ) + parser_get.add_argument( + 'max_records', + help='Limit maximum number records to return', + type=int, + nargs='?', + default=10, + ) + # del command + parser_del = subparsers.add_parser( + 'del', + help='Remove a record from the block ' + 'list', + ) + parser_del.add_argument( + '--key', + help='Specify API key', + type=str, + ) + parser_del.add_argument( + 'record_id', + help='DNS domain name or record ID to remove ' + 'from the block list', + type=str, + ) + # keyring command + parser_keyring = subparsers.add_parser( + 'keyring', + help='Remove a record from the ' + 'block list', + ) + group_keyring = parser_keyring.add_mutually_exclusive_group() + group_keyring.add_argument( + '--add', + dest='key_to_add', + help='Add API key to the keyring', + type=str, + ) + group_keyring.add_argument( + '--show', + help='Read and show API key from the keyring', + action='store_true', + default=True, + ) # print short usage description if no args was provided if len(sys.argv) == 1: @@ -105,16 +157,29 @@ def save_key(key): logger.debug('Saving API key to keyring') keyring.set_password('python', umbr_api.__title__, key) read_key = keyring.get_password('python', umbr_api.__title__) - print('Note: Any python program may have an access to this record\n' + print('Note: Any python program may have an access to this record ' 'in the keychain under your credentials.') if read_key == key: - code = 0 print('OK') else: - code = 1 print('Error: Provided key doesn''t match to saved key.') logging.error('Provided key doesn''t match to saved key.') - return code + return 0 if read_key == key else 1 + + +def show_key(): + """Read and show API key from the keyring.""" + logger.debug('Reading API key from keyring') + try: + read_key = keyring.get_password('python', umbr_api.__title__) + except RuntimeError: + read_key = None + + if read_key: + print('API key:', read_key) + else: + logging.warning('No API key is accessible.') + return 0 if read_key else 1 def setup_logging_level(verbose_level): @@ -134,7 +199,6 @@ def setup_logging_level(verbose_level): def main(args=None): """Execute main body, console_script entry point.""" - key = None response = None exit_code = 0 if not args: @@ -147,38 +211,24 @@ def main(args=None): logger.debug('Debug turned on') logger.debug('Run with arguments: %s', str(args)) - if args.version: - print('Version: {}'.format(umbr_api.__version__)) - raise SystemExit(0) - - if args.keyring_add: - exit_code = save_key(args.keyring_add[0]) + if args.command == 'keyring': + if args.key_to_add: + exit_code = save_key(args.key_to_add) + elif args.show: + exit_code = show_key() raise SystemExit(exit_code) - if args.key: - key = args.key[0] - logger.debug('Use API key from command-line arguments') - else: - logger.debug('Reading API key from a keyring') - key = keyring.get_password('python', umbr_api.__title__) - if not key: - logger.debug('No API key found in a keyring') - - if args.get_list: - response = get_list(page=1, limit=args.get_list, key=key) - exit_code = response.status_code - - if args.add: - response = add(domain=args.add[0], url=args.add[1], - key=key, bypass=args.force) + if args.command == 'get': + response = get_list(page=1, limit=args.max_records, key=args.key) exit_code = response.status_code - if args.remove_domain: - response = remove(domain_name=args.remove_domain[0], key=key) + if args.command == 'add': + response = add(domain=args.dns_name, url=args.url, + key=args.key, bypass=args.force) exit_code = response.status_code - if args.remove_id: - response = remove(record_id=args.remove_id[0], key=key) + if args.command == 'del': + response = remove(record_id=args.record_id, key=args.key) exit_code = response.status_code logger.debug('Exit code: %d', exit_code)