Bare minimum to make HTTP calls

This commit is contained in:
Frédéric Guillot 2017-05-15 14:16:39 -04:00
parent c0b24963b3
commit 75b72d4e34
24 changed files with 361 additions and 140 deletions

View File

@ -1,3 +0,0 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>

View File

@ -1,4 +0,0 @@
python-almanachclient Style Commandments
========================================
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

View File

@ -1,5 +1,5 @@
=================== ===================
Almanach API client Almanach API Client
=================== ===================
This is a client library for Almanach built on the Almanch REST API. This is a client library for Almanach built on the Almanch REST API.
@ -9,3 +9,4 @@ tool (almanach-client).
* Free software: Apache license * Free software: Apache license
* Source: http://git.openstack.org/cgit/openstack/python-almanachclient * Source: http://git.openstack.org/cgit/openstack/python-almanachclient
* Bugs: http://bugs.launchpad.net/almanach * Bugs: http://bugs.launchpad.net/almanach
* Documentation: https://almanach-client.readthedocs.io/

View File

View File

@ -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()))

View File

@ -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')))

View File

@ -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

View File

@ -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__),
}

View File

@ -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)

View File

@ -1,33 +1,34 @@
# Copyright 2017 INAP
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License");
# not use this file except in compliance with the License. You may obtain # you may not use this file except in compliance with the License.
# a copy of the License at # 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.
# #
# 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 os
Command-line interface to the OpenStack Almanach API.
"""
import sys import sys
from cliff import app from cliff import app
from cliff import commandmanager from cliff import commandmanager
from almanachclient import version from almanachclient.commands.endpoint import EndpointCommand
from almanachclient.v1 import version_cli 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): class AlmanachCommandManager(commandmanager.CommandManager):
SHELL_COMMANDS = { SHELL_COMMANDS = {
"version": version_cli.CliVersionShow, 'version': VersionCommand,
'endpoint': EndpointCommand,
} }
def load_commands(self, namespace): def load_commands(self, namespace):
@ -38,18 +39,49 @@ class AlmanachCommandManager(commandmanager.CommandManager):
class AlmanachApp(app.App): class AlmanachApp(app.App):
def __init__(self): def __init__(self):
super(AlmanachApp, self).__init__( super().__init__(
description='Almanach command line client', description='Almanach Command Line Client',
version=version.__version__, version=client_version.__version__,
command_manager=AlmanachCommandManager(None), command_manager=AlmanachCommandManager(None),
deferred_help=True, 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): parser.add_argument('--os-auth-url',
if args is None: default=os.environ.get('OS_AUTH_URL'),
args = sys.argv[1:] help='Keystone V3 URL (Env: OS_AUTH_URL).')
return AlmanachApp().run(args)
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__': if __name__ == '__main__':

View File

@ -1,23 +1,19 @@
# -*- coding: utf-8 -*- # Copyright 2017 INAP
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License");
# not use this file except in compliance with the License. You may obtain # you may not use this file except in compliance with the License.
# a copy of the License at # 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 # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # distributed under the License is distributed on an "AS IS" BASIS,
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# License for the specific language governing permissions and limitations # See the License for the specific language governing permissions and
# under the License. # limitations under the License.
from oslotest import base from oslotest import base
class TestCase(base.BaseTestCase): class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests.""" """Test case base class for all unit tests."""

View File

@ -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

View File

@ -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)

View File

View File

@ -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())

View File

@ -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))

View File

@ -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')

View File

@ -1,16 +1,16 @@
# Copyright 2017 INAP
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License");
# not use this file except in compliance with the License. You may obtain # you may not use this file except in compliance with the License.
# a copy of the License at # 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.
# #
# 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 import pbr.version

View File

@ -1,2 +0,0 @@
[python: **.py]

View File

@ -1,6 +1,4 @@
# The order of packages is significant, because pip processes them in the order pbr>=2.0.0,!=2.1.0 # Apache-2.0
# of appearance. Changing the order has an impact on the overall integration cliff>=2.6.0 # Apache-2.0
# process, which may cause wedges in the gate later. requests>=2.10.0,!=2.12.2,!=2.13.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
pbr>=1.6 # Apache-2.0
cliff>1.16.0 # Apache-2.0

View File

@ -13,11 +13,9 @@ classifier =
License :: OSI Approved :: Apache Software License License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux Operating System :: POSIX :: Linux
Programming Language :: Python Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
[files] [files]
packages = packages =
@ -25,7 +23,7 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
almanach = almanachclient.shell:main almanach-client = almanachclient.shell:main
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source

View File

@ -16,13 +16,6 @@
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools 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( setuptools.setup(
setup_requires=['pbr'], setup_requires=['pbr'],

View File

@ -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 hacking<0.12,>=0.11.0 # Apache-2.0
coverage>=3.6 # Apache-2.0 coverage>=3.6 # Apache-2.0

12
tox.ini
View File

@ -1,6 +1,6 @@
[tox] [tox]
minversion = 2.0 minversion = 2.0
envlist = py34,py27,pep8 envlist = py34,py35,pep8
skipsdist = True skipsdist = True
[testenv] [testenv]
@ -12,14 +12,12 @@ deps = -r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}' commands = python setup.py test --slowest --testr-args='{posargs}'
[testenv:pep8] [testenv:pep8]
basepython = python3
commands = flake8 {posargs} commands = flake8 {posargs}
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs] [testenv:docs]
commands = python setup.py build_sphinx commands = python setup.py build_sphinx
@ -27,13 +25,9 @@ commands = python setup.py build_sphinx
commands = commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:debug]
commands = oslo_debug_helper {posargs}
[flake8] [flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True show-source = True
ignore = E123,E125 ignore = E123,E125
builtins = _ builtins = _
max-line-length = 120
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build