Import code for basic functionality

This commit adds a lot of the base code needed for a basic
python client for the App Catalog.

Change-Id: I9229e6890d100ec8dfbb23bf419e81128e21e5e8
This commit is contained in:
Paul Van Eck 2016-04-10 19:24:05 -07:00
parent aa8ed349ca
commit 32592bc139
26 changed files with 615 additions and 2 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# Compiled files
*.py[co]
*.a
*.o
*.so
# Sphinx
_build
doc/source/api/
# Release notes
releasenotes/build
# Packages/installer info
*.egg
*.egg-info
dist
build
eggs
parts
var
sdist
develop-eggs
.installed.cfg
# Other
*.DS_Store
.testrepository
.tox
.venv
.*.swp
.coverage
cover
AUTHORS
ChangeLog
*.sqlite
test.conf

4
.testr.conf Normal file
View File

@ -0,0 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 coverage run -m subunit.run discover -t ./ -s ./appcatalogclient/tests/unit $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -1,5 +1,32 @@
Python App Catalog Client
=========================
This repo will soon hold the Community App Catalog OpenStack Client
Plugin.
This is the Community App Catalog OpenStack Client Plugin.
How to Install
--------------
To install without a virtualenv::
git clone https://github.com/openstack/python-appcatalogclient
cd python-appcatalogclient
pip install .
To install using a virtualenv::
git clone https://github.com/openstack/python-appcatalogclient
cd python-appcatalogclient
virtualenv .venv
source .venv/bin/activate
pip install .
Usage
-----
List all applications::
openstack appcatalog list
Show details for a specific application::
openstack appcatalog show "App Name"

View File

View File

@ -0,0 +1,27 @@
#
# 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 oslo_utils import importutils
def import_versioned_module(version, submodule=None):
module = 'appcatalogclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
def Client(version, *args, **kwargs):
module = import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)

View File

View File

@ -0,0 +1,49 @@
#
# 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 logging
from openstackclient.common import utils
LOG = logging.getLogger(__name__)
DEFAULT_API_VERSION = '1'
API_NAME = 'appcatalog'
API_VERSION_OPTION = 'os_appcatalog_api_version'
API_VERSIONS = {
'1': 'appcatalogclient.v1.client.Client',
}
def make_client(instance):
"""Returns an appcatalog service client."""
plugin_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
LOG.debug('Instantiating app catalog client: %s', plugin_client)
client = plugin_client()
return client
def build_option_parser(parser):
"""Hook to add global options."""
parser.add_argument(
'--os-appcatalog-api-version',
metavar='<appcatalog-api-version>',
help='App Catalog API version, default=' +
DEFAULT_API_VERSION +
' (Env: OS_APPCATALOG_API_VERSION)')
return parser

View File

View File

@ -0,0 +1,91 @@
#
# 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 logging
from cliff import lister
from cliff import show
LOG = logging.getLogger(__name__)
class ListApps(lister.Lister):
"""List all apps in the catalog."""
fields = ['Name', 'Type', 'License']
fields_long = ['Name', 'Hash', 'Type', 'License']
def get_parser(self, prog_name):
parser = super(ListApps, self).get_parser(prog_name)
parser.add_argument(
'--long',
action='store_true',
default=False,
help='List additional fields in output',
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.appcatalog
catalog_json = client.catalog.get_apps()
if parsed_args.long:
return (self.fields_long,
((s['name'],
s.get('hash', ''),
s['service']['type'],
s.get('license', '')) for s in catalog_json['assets'])
)
else:
return (self.fields,
((s['name'],
s['service']['type'],
s.get('license', '')) for s in catalog_json['assets'])
)
class ShowApp(show.ShowOne):
"""Show details of one specific app."""
def _format_output(self, app):
"""This function will change the output to a cleaner format."""
fields, data = self.dict2columns(app)
formatted_data = []
for value in data:
if isinstance(value, dict):
dict_strings = []
for key, val in value.items():
dict_strings.append("{!s}={!s}".format(key, val))
formatted_data.append('\n'.join(dict_strings))
elif isinstance(value, list):
formatted_data.append(', '.join(str(s) for s in value))
else:
formatted_data.append(value)
return fields, formatted_data
def get_parser(self, prog_name):
parser = super(ShowApp, self).get_parser(prog_name)
parser.add_argument(
'application',
metavar="<application>",
help='Name of the application',
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.appcatalog
app = client.catalog.get_app(parsed_args.application)
return self._format_output(app)

View File

View File

View File

@ -0,0 +1,20 @@
#
# 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 testtools
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()

View File

@ -0,0 +1,22 @@
#
# 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 mock
from openstackclient.tests import utils
class TestAppCatalog(utils.TestCommand):
def setUp(self):
super(TestAppCatalog, self).setUp()
self.app.client_manager.appcatalog = mock.Mock()

View File

@ -0,0 +1,94 @@
#
# 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 appcatalogclient.osc.v1 import catalog as osc_catalog
from appcatalogclient.tests.unit.osc.v1 import fakes
FAKE_CATALOG = {'assets': [
{'service': {'type': 'glance'},
'hash': 'abcd',
'name': 'Foo Bar',
'license': 'test license'},
{'service': {'type': 'glance'},
'hash': 'efgh',
'name': 'Test App',
'license': 'test license'},
{'service': {'type': 'heat'},
'hash': 'ijkl',
'name': 'Test App 2',
'license': 'test license'}]}
class TestCatalog(fakes.TestAppCatalog):
def setUp(self):
super(TestCatalog, self).setUp()
self.catalog_mock = self.app.client_manager.appcatalog.catalog
self.catalog_mock.reset_mock()
class TestListApps(TestCatalog):
def setUp(self):
super(TestListApps, self).setUp()
self.catalog_mock.get_apps.return_value = FAKE_CATALOG
# Command to test
self.cmd = osc_catalog.ListApps(self.app, None)
def test_list_apps(self):
parsed_args = self.check_parser(self.cmd, [], [])
columns, data = self.cmd.take_action(parsed_args)
expected_columns = ['Name', 'Type', 'License']
self.assertEqual(expected_columns, columns)
expected_data = [('Foo Bar', 'glance', 'test license'),
('Test App', 'glance', 'test license'),
('Test App 2', 'heat', 'test license')]
self.assertEqual(expected_data, list(data))
def test_list_apps_long(self):
parsed_args = self.check_parser(self.cmd, ['--long'], [])
columns, data = self.cmd.take_action(parsed_args)
expected_columns = ['Name', 'Hash', 'Type', 'License']
self.assertEqual(expected_columns, columns)
expected_data = [('Foo Bar', 'abcd', 'glance', 'test license'),
('Test App', 'efgh', 'glance', 'test license'),
('Test App 2', 'ijkl', 'heat', 'test license')]
self.assertEqual(expected_data, list(data))
class TestShowApp(TestCatalog):
def setUp(self):
super(TestShowApp, self).setUp()
fake_app = {'service': {'type': 'glance'},
'hash': 'abcd',
'name': 'Foo Bar',
'license': 'test license'}
self.catalog_mock.get_app.return_value = fake_app
# Command to test
self.cmd = osc_catalog.ShowApp(self.app, None)
def test_get_app(self):
parsed_args = self.check_parser(self.cmd, ['Foo Bar'], [])
columns, data = self.cmd.take_action(parsed_args)
expected_columns = ('hash', 'license', 'name', 'service')
self.assertEqual(expected_columns, columns)
expected_data = ['abcd', 'test license', 'Foo Bar', 'type=glance']
self.assertEqual(expected_data, data)

View File

@ -0,0 +1,54 @@
#
# 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_mock
from appcatalogclient.tests.unit import base
from appcatalogclient.v1 import catalog
FAKE_CATALOG = {'assets': [
{'service': {'type': 'glance'},
'hash': 'abcd',
'name': 'Foo Bar',
'license': 'test license'},
{'service': {'type': 'glance'},
'hash': 'efgh',
'name': 'Test App',
'license': 'test license'},
{'service': {'type': 'heat'},
'hash': 'ijkl',
'name': 'Test App 2',
'license': 'test license'}]}
class TestCatalogManager(base.BaseTestCase):
def setUp(self):
super(TestCatalogManager, self).setUp()
self.manager = catalog.CatalogManager('http://foo.bar/')
def test_list_apps(self):
with requests_mock.mock() as m:
m.get('http://foo.bar/assets', json=FAKE_CATALOG)
response = self.manager.get_apps()
self.assertEqual(FAKE_CATALOG, response)
def test_get_app(self):
with requests_mock.mock() as m:
m.get('http://foo.bar/assets', json=FAKE_CATALOG)
result = self.manager.get_app('Test App')
expected = {'service': {'type': 'glance'},
'hash': 'efgh',
'name': 'Test App',
'license': 'test license'}
self.assertEqual(expected, result)

View File

View File

@ -0,0 +1,33 @@
#
# 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
class CatalogManager(object):
"""Manager for the app catalog."""
def __init__(self, api_url):
self.catalog_url = api_url + 'assets'
def get_apps(self):
"""Get all applications in the catalog."""
response = requests.get(self.catalog_url)
return response.json()
def get_app(self, app_name):
"""Get details for a specific application given its name."""
catalog = self.get_apps()
for service in catalog['assets']:
if app_name == service['name']:
return service

View File

@ -0,0 +1,24 @@
#
# 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 appcatalogclient.v1 import catalog
class Client(object):
"""Client for the App Catalog v1 API."""
API_URL = 'http://apps.openstack.org/api/v1/'
def __init__(self, *args, **kwargs):
"""Initialize a new client for the App Catalog v1 API."""
self.catalog = catalog.CatalogManager(self.API_URL)

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
# 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,!=1.17.0,>=1.15.0 # Apache-2.0
python-openstackclient>=2.1.0 # Apache-2.0
requests!=2.9.0,>=2.8.1 # Apache-2.0
six>=1.9.0 # MIT

40
setup.cfg Normal file
View File

@ -0,0 +1,40 @@
[metadata]
name = python-appcatalogclient
summary = OpenStack App Catalog API Client Library
description-file = README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
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.4
[files]
packages = appcatalogclient
[entry_points]
openstack.cli.extension =
appcatalog = appcatalogclient.osc.plugin
openstack.appcatalog.v1 =
appcatalog_list = appcatalogclient.osc.v1.catalog:ListApps
appcatalog_show = appcatalogclient.osc.v1.catalog:ShowApp
[pbr]
autodoc_index_modules = True
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
[wheel]
universal = 1

28
setup.py Normal file
View File

@ -0,0 +1,28 @@
#
# 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.
# 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>=1.8'],
pbr=True)

14
test-requirements.txt Normal file
View File

@ -0,0 +1,14 @@
# 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.11,>=0.10.0
coverage>=3.6 # Apache-2.0
discover # BSD
doc8 # Apache-2.
fixtures>=1.3.1 # Apache-2.0/BSD
requests-mock>=0.7.0 # Apache-2.0
mock>=1.2 # BSD
Babel>=1.3 # BSD
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
testtools>=1.4.0 # MIT

41
tox.ini Normal file
View File

@ -0,0 +1,41 @@
[tox]
minversion = 1.6
envlist = py34,py27,pep8,pypy
skipsdist = True
[testenv]
setenv = VIRTUAL_ENV={envdir}
LANGUAGE=en_US
# .testr.conf uses TESTS_DIR
TESTS_DIR=./appcatalogclient/tests/unit
usedevelop = True
install_command = pip install -U {opts} {packages}
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = find . -type f -name "*.pyc" -delete
python setup.py testr --testr-args='{posargs}'
whitelist_externals = find
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:pep8]
commands =
flake8 {posargs}
[testenv:cover]
setenv = VIRTUAL_ENV={envdir}
LANGUAGE=en_US
commands =
python setup.py testr --coverage {posargs}
[testenv:venv]
commands = {posargs}
[flake8]
ignore =
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools
[hacking]
import_exceptions = testtools.matchers