diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 516ae6f..0000000 --- a/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -# Format is: -# -# diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index 71f9c17..0000000 --- a/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -python-almanachclient Style Commandments -======================================== - -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/README.rst b/README.rst index 2ac531b..3a7c7b3 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ =================== -Almanach API client +Almanach API Client =================== This is a client library for Almanach built on the Almanch REST API. @@ -9,3 +9,4 @@ tool (almanach-client). * Free software: Apache license * Source: http://git.openstack.org/cgit/openstack/python-almanachclient * Bugs: http://bugs.launchpad.net/almanach +* Documentation: https://almanach-client.readthedocs.io/ diff --git a/almanachclient/commands/__init__.py b/almanachclient/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/almanachclient/commands/endpoint.py b/almanachclient/commands/endpoint.py new file mode 100644 index 0000000..04855b2 --- /dev/null +++ b/almanachclient/commands/endpoint.py @@ -0,0 +1,22 @@ +# Copyright 2017 INAP +# +# 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 cliff.command import Command + + +class EndpointCommand(Command): + """Show the Almanach Endpoint URL""" + + def take_action(self, parsed_args): + self.app.stdout.write('{}\n'.format(self.app.get_client().get_url())) diff --git a/almanachclient/commands/version.py b/almanachclient/commands/version.py new file mode 100644 index 0000000..25c1261 --- /dev/null +++ b/almanachclient/commands/version.py @@ -0,0 +1,23 @@ +# Copyright 2017 INAP +# +# 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 cliff.command import Command + + +class VersionCommand(Command): + """Show the Almanach version number""" + + def take_action(self, parsed_args): + info = self.app.get_client().get_info() + self.app.stdout.write('{}\n'.format(info.get('info', {}).get('version'))) diff --git a/almanachclient/exceptions.py b/almanachclient/exceptions.py new file mode 100644 index 0000000..48cf56a --- /dev/null +++ b/almanachclient/exceptions.py @@ -0,0 +1,25 @@ +# Copyright 2017 INAP +# +# 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. + + +class ClientError(Exception): + pass + + +class EndpointNotFound(ClientError): + pass + + +class HTTPError(ClientError): + pass diff --git a/almanachclient/http_client.py b/almanachclient/http_client.py new file mode 100644 index 0000000..6bdb544 --- /dev/null +++ b/almanachclient/http_client.py @@ -0,0 +1,41 @@ +# Copyright 2017 INAP +# +# 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 abc +import logging + +import requests + +from almanachclient import exceptions +from almanachclient import version as client_version + +logger = logging.getLogger(__name__) + + +class HttpClient(metaclass=abc.ABCMeta): + def _get(self, url): + logger.debug(url) + response = requests.get(url, headers=self._get_headers()) + + if response.status_code != 200: + raise exceptions.HTTPError('HTTP Error ({})'.format(response.status_code)) + + return response.json() + + def _get_headers(self): + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-almanachclient/{}'.format(client_version.__version__), + } diff --git a/almanachclient/keystone_client.py b/almanachclient/keystone_client.py new file mode 100644 index 0000000..a23a5b6 --- /dev/null +++ b/almanachclient/keystone_client.py @@ -0,0 +1,51 @@ +# Copyright 2017 INAP +# +# 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 keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient.v3 import client as keystone_client + +from almanachclient import exceptions + + +class KeystoneClient(object): + def __init__(self, auth_url, username, password, service, region_name, + domain_name='default', user_domain_id='default'): + self.auth_url = auth_url + self.username = username + self.password = password + self.service = service + self.region_name = region_name + self.domain_name = domain_name + self.user_domain_id = user_domain_id + + def get_endpoint_url(self, visibility='admin'): + keystone = self._get_keystone_client() + endpoints = keystone.endpoints.list(service=self.service, region=self.region_name) + + for endpoint in endpoints: + if endpoint.interface == visibility: + return endpoint.url + + raise exceptions.EndpointNotFound('Endpoint URL Not Found') + + def _get_keystone_client(self): + auth = v3.Password(auth_url=self.auth_url, + username=self.username, + password=self.password, + domain_name=self.domain_name, + user_domain_id=self.user_domain_id) + + sess = session.Session(auth=auth) + return keystone_client.Client(session=sess) diff --git a/almanachclient/shell.py b/almanachclient/shell.py index baf9a88..d545d31 100644 --- a/almanachclient/shell.py +++ b/almanachclient/shell.py @@ -1,33 +1,34 @@ +# Copyright 2017 INAP # -# 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 +# 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. +# 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. -""" -Command-line interface to the OpenStack Almanach API. -""" - +import os import sys from cliff import app from cliff import commandmanager -from almanachclient import version -from almanachclient.v1 import version_cli +from almanachclient.commands.endpoint import EndpointCommand +from almanachclient.commands.version import VersionCommand +from almanachclient.keystone_client import KeystoneClient +from almanachclient.v1.client import Client +from almanachclient import version as client_version class AlmanachCommandManager(commandmanager.CommandManager): SHELL_COMMANDS = { - "version": version_cli.CliVersionShow, + 'version': VersionCommand, + 'endpoint': EndpointCommand, } def load_commands(self, namespace): @@ -38,18 +39,49 @@ class AlmanachCommandManager(commandmanager.CommandManager): class AlmanachApp(app.App): def __init__(self): - super(AlmanachApp, self).__init__( - description='Almanach command line client', - version=version.__version__, + super().__init__( + description='Almanach Command Line Client', + version=client_version.__version__, command_manager=AlmanachCommandManager(None), deferred_help=True, ) + def build_option_parser(self, description, version, argparse_kwargs=None): + parser = super().build_option_parser(description, version, argparse_kwargs) -def main(args=None): - if args is None: - args = sys.argv[1:] - return AlmanachApp().run(args) + parser.add_argument('--os-auth-url', + default=os.environ.get('OS_AUTH_URL'), + help='Keystone V3 URL (Env: OS_AUTH_URL).') + + parser.add_argument('--os-region-name', + default=os.environ.get('OS_REGION_NAME'), + help='OpenStack region name (Env: OS_REGION_NAME).') + + parser.add_argument('--os-password', + default=os.environ.get('OS_PASSWORD'), + help='OpenStack password (Env: OS_PASSWORD).') + + parser.add_argument('--os-username', + default=os.environ.get('OS_USERNAME'), + help='OpenStack username (Env: OS_USERNAME).') + + parser.add_argument('--almanach-service', + default=os.environ.get('ALMANACH_SERVICE'), + help='Almanach keystone service name (Env: ALMANACH_SERVICE).') + return parser + + def get_client(self): + keystone = KeystoneClient(auth_url=self.options.os_auth_url, + username=self.options.os_username, + password=self.options.os_password, + service=self.options.almanach_service, + region_name=self.options.os_region_name) + + return Client(keystone.get_endpoint_url()) + + +def main(argv=sys.argv[1:]): + return AlmanachApp().run(argv) if __name__ == '__main__': diff --git a/almanachclient/tests/base.py b/almanachclient/tests/base.py index 1c30cdb..126d57b 100644 --- a/almanachclient/tests/base.py +++ b/almanachclient/tests/base.py @@ -1,23 +1,19 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# Copyright 2017 INAP # -# 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 +# 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 +# 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. +# 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 oslotest import base class TestCase(base.BaseTestCase): - """Test case base class for all unit tests.""" diff --git a/almanachclient/tests/test_almanachclient.py b/almanachclient/tests/test_almanachclient.py deleted file mode 100644 index 48b16f6..0000000 --- a/almanachclient/tests/test_almanachclient.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# 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. - -""" -test_almanachclient ----------------------------------- - -Tests for `almanachclient` module. -""" - -from almanachclient.tests import base - - -class TestAlmanachclient(base.TestCase): - - def test_something(self): - pass diff --git a/almanachclient/tests/test_keystone_client.py b/almanachclient/tests/test_keystone_client.py new file mode 100644 index 0000000..c12daf5 --- /dev/null +++ b/almanachclient/tests/test_keystone_client.py @@ -0,0 +1,46 @@ +# Copyright 2017 INAP +# +# 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 unittest import mock + +from almanachclient import exceptions +from almanachclient.keystone_client import KeystoneClient +from almanachclient.tests import base + + +class TestKeystoneClient(base.TestCase): + def setUp(self): + super().setUp() + self.almanach_url = 'http://almanach_url' + self.auth_url = 'http://keystone_url' + self.username = 'username' + self.password = 'password' + self.service = 'almanach' + self.region_name = 'some region' + self.client = KeystoneClient(self.auth_url, self.username, self.password, self.service, self.region_name) + + @mock.patch('keystoneclient.v3.client.Client') + def test_get_endpoint_url(self, keystone): + endpoint_manager = mock.Mock() + keystone.return_value = endpoint_manager + endpoint_manager.endpoints.list.return_value = [mock.Mock(interface='admin', url=self.almanach_url)] + + self.assertEqual(self.almanach_url, self.client.get_endpoint_url()) + + endpoint_manager.endpoints.list.assert_called_once_with(service=self.service, region=self.region_name) + + @mock.patch('keystoneclient.v3.client.Client') + def test_get_endpoint_url_not_found(self, keystone): + keystone.return_value.endpoints.list.return_value = [] + self.assertRaises(exceptions.EndpointNotFound, self.client.get_endpoint_url) diff --git a/almanachclient/tests/v1/__init__.py b/almanachclient/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/almanachclient/tests/v1/test_client.py b/almanachclient/tests/v1/test_client.py new file mode 100644 index 0000000..ed3a6f6 --- /dev/null +++ b/almanachclient/tests/v1/test_client.py @@ -0,0 +1,39 @@ +# Copyright 2017 INAP +# +# 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 unittest import mock + +from almanachclient.tests import base +from almanachclient.v1.client import Client + + +class TestClient(base.TestCase): + def setUp(self): + super().setUp() + self.almanach_url = 'http://almanach_url' + self.client = Client(self.almanach_url) + + @mock.patch('requests.get') + def test_get_info(self, requests): + response = mock.Mock() + expected = { + 'info': {'version': '1.2.3'}, + "database": {'all_entities': 2, 'active_entities': 1} + } + + requests.return_value = response + response.json.return_value = expected + response.status_code = 200 + + self.assertEqual(expected, self.client.get_info()) diff --git a/almanachclient/v1/client.py b/almanachclient/v1/client.py new file mode 100644 index 0000000..187e119 --- /dev/null +++ b/almanachclient/v1/client.py @@ -0,0 +1,28 @@ +# Copyright 2017 INAP +# +# 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 almanachclient.http_client import HttpClient + + +class Client(HttpClient): + api_version = 'v1' + + def __init__(self, url): + self.url = url + + def get_url(self): + return self.url + + def get_info(self): + return self._get('{}/{}/info'.format(self.url, self.api_version)) diff --git a/almanachclient/v1/version_cli.py b/almanachclient/v1/version_cli.py deleted file mode 100644 index c1dfe68..0000000 --- a/almanachclient/v1/version_cli.py +++ /dev/null @@ -1,25 +0,0 @@ -# -# 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 cliff.command import Command - - -from almanachclient import version - - -class CliVersionShow(Command): - """Show the client version number""" - - def take_action(self, parsed_args): - self.app.stdout.write(version.__version__ + '\n') diff --git a/almanachclient/version.py b/almanachclient/version.py index e58667d..2d0bb50 100644 --- a/almanachclient/version.py +++ b/almanachclient/version.py @@ -1,16 +1,16 @@ +# Copyright 2017 INAP # -# 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 +# 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. +# 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 pbr.version diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index 15cd6cb..0000000 --- a/babel.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[python: **.py] - diff --git a/requirements.txt b/requirements.txt index 09a66dd..b50f9f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -pbr>=1.6 # Apache-2.0 -cliff>1.16.0 # Apache-2.0 +pbr>=2.0.0,!=2.1.0 # Apache-2.0 +cliff>=2.6.0 # Apache-2.0 +requests>=2.10.0,!=2.12.2,!=2.13.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 3240ae9..b50023a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,11 +13,9 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 [files] packages = @@ -25,7 +23,7 @@ packages = [entry_points] console_scripts = - almanach = almanachclient.shell:main + almanach-client = almanachclient.shell:main [build_sphinx] source-dir = doc/source diff --git a/setup.py b/setup.py index 056c16c..155c200 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,6 @@ # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass setuptools.setup( setup_requires=['pbr'], diff --git a/test-requirements.txt b/test-requirements.txt index a3fcd81..044347a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,3 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - hacking<0.12,>=0.11.0 # Apache-2.0 coverage>=3.6 # Apache-2.0 diff --git a/tox.ini b/tox.ini index d9e6688..d493a44 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.0 -envlist = py34,py27,pep8 +envlist = py34,py35,pep8 skipsdist = True [testenv] @@ -12,14 +12,12 @@ deps = -r{toxinidir}/test-requirements.txt commands = python setup.py test --slowest --testr-args='{posargs}' [testenv:pep8] +basepython = python3 commands = flake8 {posargs} [testenv:venv] commands = {posargs} -[testenv:cover] -commands = python setup.py test --coverage --testr-args='{posargs}' - [testenv:docs] commands = python setup.py build_sphinx @@ -27,13 +25,9 @@ commands = python setup.py build_sphinx commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:debug] -commands = oslo_debug_helper {posargs} - [flake8] -# E123, E125 skipped as they are invalid PEP-8. - show-source = True ignore = E123,E125 builtins = _ +max-line-length = 120 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build