Setting up base classes

Base classes added for HTTPClient and Managers.

Users and UserPreferences implementation.

Change-Id: Ia2beb899b752a5db22a5dc12ff55852a7764ac36
This commit is contained in:
Nikita Konovalov 2014-11-26 16:19:13 +03:00
parent 9131009a77
commit 2090a8beff
18 changed files with 587 additions and 31 deletions

View File

@ -2,6 +2,6 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
${PYTHON:-python} -m subunit.run discover -t ./ ./storyboardclient/tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -9,5 +9,6 @@ oslo.config>=1.4.0
oslo.i18n>=1.0.0
oslo.serialization>=1.0.0
oslo.utils>=1.0.0
requests>=2.2.0,!=2.4.0
six>=1.7.0
stevedore>=1.1.0
stevedore>=1.1.0

View File

View File

@ -0,0 +1,33 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient.openstack.common.apiclient import auth
class OAuthPlugin(auth.BaseAuthPlugin):
def _do_authenticate(self, http_client):
# Skipping for now as there will be a separate spec and implementation
# for authenticating a python client with OAuth.
pass
def __init__(self, api_url=None, access_token=None):
super(OAuthPlugin, self).__init__()
self.api_url = api_url
self.access_token = access_token
def token_and_endpoint(self, endpoint_type=None, service_type=None):
return self.access_token, self.api_url

181
storyboardclient/base.py Normal file
View File

@ -0,0 +1,181 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 inspect
import six
from storyboardclient.auth import oauth
from storyboardclient.openstack.common.apiclient import base
from storyboardclient.openstack.common.apiclient import client
DEFAULT_API_URL = "https://storyboard.openstack.org/api/v1"
class BaseClient(client.BaseClient):
def __init__(self, api_url=None, access_token=None):
if not api_url:
api_url = DEFAULT_API_URL
self.auth_plugin = oauth.OAuthPlugin(api_url, access_token)
self.http_client = BaseHTTPClient(auth_plugin=self.auth_plugin)
class BaseHTTPClient(client.HTTPClient):
"""Base class for setting up endpoint and token.
This HTTP client is overriding a client_request method to add
Authorization header if OAuth token is provided.
"""
def client_request(self, client, method, url, **kwargs):
"""Send an http request using `client`'s endpoint and specified `url`.
If request was rejected as unauthorized (possibly because the token is
expired), issue one authorization attempt and send the request once
again.
:param client: instance of BaseClient descendant
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
`HTTPClient.request`
"""
token, endpoint = (self.cached_token, client.cached_endpoint)
if not (token and endpoint):
token, endpoint = self.auth_plugin.token_and_endpoint()
self.cached_token = token
client.cached_endpoint = endpoint
if token:
kwargs.setdefault("headers", {})["Authorization"] = \
"Bearer %s" % token
return self.request(method, self.concat_url(endpoint, url), **kwargs)
class BaseManager(base.CrudManager):
def build_url(self, base_url=None, **kwargs):
# Overriding to use "url_key" instead of the "collection_key".
# "key_id" is replaced with just "id" when querying a specific object.
url = base_url if base_url is not None else ''
url += '/%s' % self.url_key
entity_id = kwargs.get('id')
if entity_id is not None:
url += '/%s' % entity_id
return url
def get(self, id):
"""Get a resource by id.
Get method is accepting id as a positional argument for simplicity.
:param id: The id of resource.
:return: The resource object.
"""
query_kwargs = {"id": id}
return self._get(self.build_url(**query_kwargs), self.key)
def create(self, **kwargs):
"""Create a resource.
The default implementation is overridden so that the dictionary is
passed 'as is' without any wrapping.
"""
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
kwargs)
class BaseNestedManager(BaseManager):
def __init__(self, client, parent_id):
super(BaseNestedManager, self).__init__(client)
self.parent_id = parent_id
def build_url(self, base_url=None, **kwargs):
# Overriding to use "url_key" instead of the "collection_key".
# "key_id" is replaced with just "id" when querying a specific object.
url = base_url if base_url is not None else ''
url += '/%s/%s/%s' % (self.parent_url_key, self.parent_id,
self.url_key)
entity_id = kwargs.get('id')
if entity_id is not None:
url += '/%s' % entity_id
return url
class BaseObject(base.Resource):
id = None
created_at = None
updated_at = None
def __init__(self, manager, info, loaded=False, parent_id=None):
super(BaseObject, self).__init__(manager, info, loaded)
self._parent_id = parent_id
self._init_nested_managers()
def _add_details(self, info):
for field, value in six.iteritems(info):
# Skip the fields which are not declared in the object
if not hasattr(self, field):
continue
setattr(self, field, value)
def _init_nested_managers(self):
# If an object has nested resource managers, they will be initialized
# here.
manager_instances = {}
for manager_name, manager_class in self._managers():
manager_instance = manager_class(client=self.manager.client,
parent_id=self.id)
# Saving a manager to a dict as self.__dict__ should not be
# changed while iterating
manager_instances[manager_name] = manager_instance
for name, manager_instance in six.iteritems(manager_instances):
# replacing managers declarations with real managers
setattr(self, name, manager_instance)
def _managers(self):
# Iterator over nested managers
for attr in dir(self):
# Skip private fields
if attr.startswith("_"):
continue
val = getattr(self, attr)
if inspect.isclass(val) and issubclass(val, BaseNestedManager):
yield attr, val

View File

@ -0,0 +1,52 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient.auth import oauth
from storyboardclient import base
from storyboardclient.tests import base as test_base
class BaseHTTPClientTestCase(test_base.TestCase):
@mock.patch("storyboardclient.base.BaseHTTPClient.request")
def test_unauthorized_client_request(self, mock_request):
auth_plugin = oauth.OAuthPlugin(api_url="http://some_endpoint")
client = base.BaseHTTPClient(auth_plugin=auth_plugin)
client.client_request(client=mock.MagicMock(),
method="GET", url="/some_url")
mock_request.assert_called_once_with("GET",
"http://some_endpoint/some_url")
@mock.patch("storyboardclient.base.BaseHTTPClient.request")
def test_authorized_client_request(self, mock_request):
auth_plugin = oauth.OAuthPlugin(api_url="http://some_endpoint",
access_token="some_token")
client = base.BaseHTTPClient(auth_plugin=auth_plugin)
client.client_request(client=mock.MagicMock(),
method="GET", url="/some_url")
mock_request.assert_called_once_with(
"GET",
"http://some_endpoint/some_url",
headers={
"Authorization": "Bearer some_token"
})

View File

@ -0,0 +1,39 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient import base
from storyboardclient.tests import base as test_base
class BaseManagerTestCase(test_base.TestCase):
@mock.patch("storyboardclient.base.BaseManager._get")
def test_get(self, mock_private_get):
manager = base.BaseManager(mock.MagicMock())
manager.url_key = "key"
manager.get("id1")
mock_private_get.assert_called_once_with("/key/id1", None)
@mock.patch("storyboardclient.base.BaseManager._post")
def test_create(self, mock_private_post):
manager = base.BaseManager(mock.MagicMock())
manager.url_key = "key"
manager.create(title="test_story")
mock_private_post.assert_called_once_with("/key",
{"title": "test_story"})

View File

@ -0,0 +1,45 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient import base
from storyboardclient.tests import base as test_base
class BaseNestedManagerTestCase(test_base.TestCase):
@mock.patch("storyboardclient.base.BaseManager._get")
def test_get(self, mock_private_get):
manager = base.BaseNestedManager(mock.MagicMock(),
parent_id="parent_id")
manager.parent_url_key = "parent_key"
manager.url_key = "key"
manager.get("id1")
mock_private_get.assert_called_once_with(
"/parent_key/parent_id/key/id1", None)
@mock.patch("storyboardclient.base.BaseManager._post")
def test_create(self, mock_private_post):
manager = base.BaseNestedManager(mock.MagicMock(),
parent_id="parent_id")
manager.parent_url_key = "parent_key"
manager.url_key = "key"
manager.create(title="test_task")
mock_private_post.assert_called_once_with(
"/parent_key/parent_id/key",
{"title": "test_task"})

View File

@ -0,0 +1,43 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient import base
from storyboardclient.tests import base as test_base
class BaseObjectTestCase(test_base.TestCase):
def test_init_no_nested(self):
manager_mock = mock.MagicMock()
obj = base.BaseObject(manager=manager_mock, info={"id": "test_id"})
self.assertEqual("test_id", obj.id)
self.assertEqual(manager_mock, obj.manager)
def test_init_with_nested(self):
manager_mock = mock.MagicMock()
class TestInheritedObject(base.BaseObject):
manager_field = base.BaseNestedManager
obj = TestInheritedObject(manager=manager_mock, info={"id": "test_id"})
self.assertEqual(base.BaseNestedManager, type(obj.manager_field))
self.assertEqual("test_id", obj.id)
self.assertEqual("test_id", obj.manager_field.parent_id)

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_storyboard
----------------------------------
Tests for `storyboard` module.
"""
from storyboardclient.tests import base
class TestStoryboard(base.TestCase):
def test_something(self):
pass

View File

View File

@ -0,0 +1,47 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient.tests import base as test_base
from storyboardclient.v1 import user_preferences
from storyboardclient.v1 import users
class UserPreferencesTestCase(test_base.TestCase):
@mock.patch("storyboardclient.v1.user_preferences.UserPreferencesManager"
"._get")
def test_user_preferences_get(self, mock_private_get):
mock_private_get.return_value = user_preferences.UserPreferences(
mock.MagicMock(),
info={"k1": "v1"})
user = users.User(manager=mock.MagicMock(), info={"id": "test_id"})
preferences = user.user_preferences.get_all()
self.assertEqual("v1", preferences.k1)
p_k1 = user.user_preferences.get("k1")
self.assertEqual("v1", p_k1)
@mock.patch("storyboardclient.v1.user_preferences.UserPreferencesManager"
"._post")
def test_user_preferences_set(self, mock_private_post):
user = users.User(manager=mock.MagicMock(), info={"id": "test_id"})
user.user_preferences.set({"k1": "v1"})
mock_private_post.assert_called_once_with("/users/test_id/preferences",
{"k1": "v1"})

View File

View File

@ -0,0 +1,44 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient import base
from storyboardclient.v1 import users
class Client(base.BaseClient):
"""A client class for StoryBoard.
Usage example:
@code:
from storyboard.v1 import client
storyboard = client.Client("https://storyboard.openstack.org/api/v1",
"mytoken")
"""
def __init__(self, api_url=None, access_token=None):
"""Sets up a client with endpoint managers.
:param api_url: (Optional) Full API url. Defaults to
https://storyboard.openstack.org/api/v1
:param access_token: (Optional) OAuth2 access token. If skipped only
public read-only endpoint will be available. All other requests will
fail with Unauthorized error.
:return: a client instance.
"""
super(Client, self).__init__(api_url=api_url,
access_token=access_token)
self.users = users.UsersManager(self)

View File

@ -0,0 +1,66 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 six
from storyboardclient import base
from storyboardclient.openstack.common import log
LOG = log.getLogger(__name__)
class UserPreferences(base.BaseObject):
def _add_details(self, info):
# User preferences can not be declared before the data is received.
# Adding all properties to an object directly.
for key, value in six.iteritems(info):
setattr(self, key, value)
class UserPreferencesManager(base.BaseNestedManager):
parent_url_key = "users"
url_key = "preferences"
resource_class = UserPreferences
def get_all(self):
"""Get a dictionary of User Preferences
User preferences are returned always as a dict, so it's better to use
a get base method instead of a list here.
:return: UserPreferences object
"""
return super(UserPreferencesManager, self).get(None)
def get(self, key):
all_prefs = super(UserPreferencesManager, self).get(None)
return getattr(all_prefs, key)
def set(self, data):
"""Set a dictionary of user preferences.
"""
return self.create(**data)
def set_one(self, key, value):
"""Set a user preference by key.
"""
return self.set({key: value})

View File

@ -0,0 +1,33 @@
# Copyright (c) 2014 Mirantis Inc.
#
# 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 storyboardclient import base
from storyboardclient.v1 import user_preferences
class User(base.BaseObject):
username = None
full_name = None
openid = None
is_superuser = None
last_login = None
enable_login = None
user_preferences = user_preferences.UserPreferencesManager
class UsersManager(base.BaseManager):
url_key = "users"
resource_class = User

View File

@ -29,6 +29,6 @@ commands = python setup.py build_sphinx
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125,H803
ignore = E123,E125,H803,H904
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build