From 7178cc0f61a06127e8139d072ddc0d2c32a1c810 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 6 Dec 2016 03:35:33 +0200 Subject: [PATCH] Initial commit - Python LaOS client - depends on devstack plugin PR#6 - depends on server PR#10 --- .gitignore | 21 ++++ .testr.conf | 4 + CONTRIBUTING.rst | 16 +++ LICENSE | 175 +++++++++++++++++++++++++++ README.rst | 125 ++++++++++++++++++++ babel.cfg | 1 + laosclient/__init__.py | 0 laosclient/client.py | 60 ++++++++++ laosclient/common/__init__.py | 0 laosclient/common/utils.py | 23 ++++ laosclient/exc.py | 191 ++++++++++++++++++++++++++++++ laosclient/i18n.py | 35 ++++++ laosclient/osc/__init__.py | 0 laosclient/osc/plugin.py | 61 ++++++++++ laosclient/osc/v1/__init__.py | 0 laosclient/osc/v1/apps.py | 133 +++++++++++++++++++++ laosclient/osc/v1/routes.py | 214 ++++++++++++++++++++++++++++++++++ laosclient/v1/__init__.py | 0 laosclient/v1/apps.py | 93 +++++++++++++++ laosclient/v1/client.py | 39 +++++++ laosclient/v1/routes.py | 133 +++++++++++++++++++++ requirements.txt | 13 +++ setup.cfg | 53 +++++++++ setup.py | 29 +++++ test-requirements.txt | 13 +++ tox.ini | 43 +++++++ 26 files changed, 1475 insertions(+) create mode 100644 .gitignore create mode 100644 .testr.conf create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 laosclient/__init__.py create mode 100644 laosclient/client.py create mode 100644 laosclient/common/__init__.py create mode 100644 laosclient/common/utils.py create mode 100644 laosclient/exc.py create mode 100644 laosclient/i18n.py create mode 100644 laosclient/osc/__init__.py create mode 100644 laosclient/osc/plugin.py create mode 100644 laosclient/osc/v1/__init__.py create mode 100644 laosclient/osc/v1/apps.py create mode 100644 laosclient/osc/v1/routes.py create mode 100644 laosclient/v1/__init__.py create mode 100644 laosclient/v1/apps.py create mode 100644 laosclient/v1/client.py create mode 100644 laosclient/v1/routes.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1238894 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.coverage* +.venv +*,cover +cover +*.pyc +AUTHORS +build +dist +ChangeLog +run_tests.err.log +.tox +doc/source/api +doc/build +*.egg +*.eggs +searchlightclient/versioninfo +*.egg-info +*.log +.testrepository +*.swp +*venv* diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..e91a163 --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./laosclient/tests} $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..8098fd8 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-laosclient diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e9e51b3 --- /dev/null +++ b/README.rst @@ -0,0 +1,125 @@ +======================== +python-laosclient +======================== + +OpenStack Functions Client Library + +This is a client library for Project LaOS built on the Project LaOS API. It +provides a Python API (the ``laosclient`` module) and a command-line +tool (``laos``). + +The project is hosted on `Launchpad`_, where bugs can be filed. The code is +hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github +pull requests. + +.. _Github: https://github.com/iron-io/python-laosclient +.. _Launchpad: https://github.com/iron-io/python-laosclient/issues +.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow + +python-laosclient is licensed under the Apache License like the rest of +OpenStack. + +.. contents:: Contents: + :local: + +Install the client from PyPI +---------------------------- +The :program:`python-laosclient` package is published on `PyPI`_ and +so can be installed using the pip tool, which will manage installing all +python dependencies:: + + $ pip install python-laosclient + +.. note:: + The packages on PyPI may lag behind the git repo in functionality. + +.. _PyPI: https://pypi.python.org/pypi/python-laosclient/ + +Setup the client from source +---------------------------- + +* Clone repository for python-laosclient:: + + $ git clone https://github.com/iron-io/python-laosclient.git + $ cd python-laosclient + +* Setup a virtualenv + +.. note:: + This is an optional step, but will allow laosclient's dependencies + to be installed in a contained environment that can be easily deleted + if you choose to start over or uninstall laosclient. + +:: + + $ tox -evenv --notest + +Activate the virtual environment whenever you want to work in it. +All further commands in this section should be run with the venv active: + +:: + + $ source .tox/venv/bin/activate + +.. note:: + When ALL steps are complete, deactivate the virtualenv: $ deactivate + +* Install laosclient and its dependencies:: + + (venv) $ python setup.py develop + +Command-line API +---------------- + +Set Keystone environment variables to execute CLI commands against LaOS. + +* To execute CLI commands:: + + $ export OS_USERNAME= + $ export OS_PASSWORD= + $ export OS_PROJECT_NAME= + $ export OS_AUTH_URL=http://localhost:5000/v3 + +.. note:: + With devstack you just need to $ source openrc . And you can + work with a local installation by passing --os-token and --os-url + http://localhost:9393. You can also set up a `Openstackclient`_ config file + to work with the CLI. + +.. _Openstackclient: http://docs.openstack.org/developer/python-openstackclient/configuration.html#clouds-yaml + +:: + + $ openstack + (openstack) fn-apps list + (openstack) fn-apps create testapp + + +Python API +---------- + +To use with keystone as the authentication system:: + + >>> from keystoneclient.auth.identity import generic + >>> from keystoneclient import session + >>> from laosclient import client + >>> auth = generic.Password(auth_url=OS_AUTH_URL, username=OS_USERNAME, password=OS_PASSWORD, tenant_name=OS_TENANT_NAME) + >>> keystone_session = session.Session(auth=auth) + >>> lc = client.Client('v1', session=keystone_session) + >>> lc.apps.list() + [...] + + +* License: Apache License, Version 2.0 +* Documentation: https://github.com/iron-io/python-laosclient +* Source: https://github.com/iron-io/python-laosclient +* Bugs: https://github.com/iron-io/python-laosclient + +Testing +------- + +There are multiple test targets that can be run to validate the code. + +* tox -e pep8 - style guidelines enforcement +* tox -e py34 - traditional unit testing +* tox -e py35 - traditional unit testing diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/laosclient/__init__.py b/laosclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laosclient/client.py b/laosclient/client.py new file mode 100644 index 0000000..93991ac --- /dev/null +++ b/laosclient/client.py @@ -0,0 +1,60 @@ +# 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 functools + +from keystoneauth1 import adapter + +from laosclient.common import utils + + +def Client(version, *args, **kwargs): + module = utils.import_versioned_module(version, 'client') + client_class = getattr(module, 'Client') + return client_class(*args, **kwargs) + + +def construct_http_client(*args, **kwargs): + kwargs = kwargs.copy() + if kwargs.get('session') is None: + raise ValueError("A session instance is required") + + return SessionClient( + session=kwargs.get('session'), + auth=kwargs.get('auth'), + region_name=kwargs.get('region_name'), + service_type=kwargs.get('service_type', 'functions'), + service_name=kwargs.get('service_name', 'functions'), + interface=kwargs.get('endpoint_type', 'public').rstrip('URL'), + user_agent=kwargs.get('user_agent', 'python-laosclient'), + endpoint_override=kwargs.get('endpoint_override'), + timeout=kwargs.get('timeout'), + ) + + +class SessionClient(adapter.Adapter): + def __init__(self, *args, **kwargs): + self.timeout = kwargs.pop('timeout', None) + super(SessionClient, self).__init__(*args, **kwargs) + + +def inject_project_id(action): + + @functools.wraps(action) + def wraps(*args, **kwargs): + self = args[0] + project_id = self.client.get_project_id() + new_args = [self, project_id, ] + new_args.extend(list(args)[1:]) + return action(*new_args, **kwargs) + + return wraps diff --git a/laosclient/common/__init__.py b/laosclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laosclient/common/utils.py b/laosclient/common/utils.py new file mode 100644 index 0000000..9ef3295 --- /dev/null +++ b/laosclient/common/utils.py @@ -0,0 +1,23 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# 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 = 'laosclient.%s' % version + if submodule: + module = '.'.join((module, submodule)) + return importutils.import_module(module) diff --git a/laosclient/exc.py b/laosclient/exc.py new file mode 100644 index 0000000..c30420f --- /dev/null +++ b/laosclient/exc.py @@ -0,0 +1,191 @@ +# 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 sys + +from oslo_serialization import jsonutils + +from laosclient.i18n import _ + +verbose = 0 + + +class BaseException(Exception): + """An error occurred.""" + def __init__(self, message=None): + self.message = message + + def __str__(self): + return self.message or self.__class__.__doc__ + + +class CommandError(BaseException): + """Invalid usage of CLI.""" + + +class InvalidEndpoint(BaseException): + """The provided endpoint is invalid.""" + + +class CommunicationError(BaseException): + """Unable to communicate with server.""" + + +class HTTPException(BaseException): + """Base exception for all HTTP-derived exceptions.""" + code = 'N/A' + + def __init__(self, message=None): + super(HTTPException, self).__init__(message) + try: + self.error = jsonutils.loads(message) + if 'error' not in self.error: + raise KeyError(_('Key "error" not exists')) + except KeyError: + # NOTE(jianingy): If key 'error' happens not exist, + # self.message becomes no sense. In this case, we + # return doc of current exception class instead. + self.error = {'error': + {'message': self.__class__.__doc__}} + except Exception: + self.error = {'error': + {'message': self.message or self.__class__.__doc__}} + + def __str__(self): + message = self.error['error'].get('message', 'Internal Error') + if verbose: + traceback = self.error['error'].get('traceback', '') + return (_('ERROR: %(message)s\n%(traceback)s') % + {'message': message, 'traceback': traceback}) + else: + return _('ERROR: %s') % message + + +class HTTPMultipleChoices(HTTPException): + code = 300 + + def __str__(self): + self.details = _("Requested version of Searchlight API is not" + "available.") + return (_("%(name)s (HTTP %(code)s) %(details)s") % + { + 'name': self.__class__.__name__, + 'code': self.code, + 'details': self.details}) + + +class BadRequest(HTTPException): + """DEPRECATED.""" + code = 400 + + +class HTTPBadRequest(BadRequest): + pass + + +class Unauthorized(HTTPException): + """DEPRECATED.""" + code = 401 + + +class HTTPUnauthorized(Unauthorized): + pass + + +class Forbidden(HTTPException): + """DEPRECATED.""" + code = 403 + + +class HTTPForbidden(Forbidden): + pass + + +class NotFound(HTTPException): + """DEPRECATED.""" + code = 404 + + +class HTTPNotFound(NotFound): + pass + + +class HTTPMethodNotAllowed(HTTPException): + code = 405 + + +class Conflict(HTTPException): + """DEPRECATED.""" + code = 409 + + +class HTTPConflict(Conflict): + pass + + +class OverLimit(HTTPException): + """DEPRECATED.""" + code = 413 + + +class HTTPOverLimit(OverLimit): + pass + + +class HTTPUnsupported(HTTPException): + code = 415 + + +class HTTPInternalServerError(HTTPException): + code = 500 + + +class HTTPNotImplemented(HTTPException): + code = 501 + + +class HTTPBadGateway(HTTPException): + code = 502 + + +class ServiceUnavailable(HTTPException): + """DEPRECATED.""" + code = 503 + + +class HTTPServiceUnavailable(ServiceUnavailable): + pass + + +# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception +# classes +_code_map = {} +for obj_name in dir(sys.modules[__name__]): + if obj_name.startswith('HTTP'): + obj = getattr(sys.modules[__name__], obj_name) + _code_map[obj.code] = obj + + +def from_response(response): + """Return an instance of an HTTPException based on requests response.""" + cls = _code_map.get(response.status_code, HTTPException) + return cls(response.content) + + +class NoTokenLookupException(Exception): + """DEPRECATED.""" + pass + + +class EndpointNotFound(Exception): + """DEPRECATED.""" + pass diff --git a/laosclient/i18n.py b/laosclient/i18n.py new file mode 100644 index 0000000..60cd559 --- /dev/null +++ b/laosclient/i18n.py @@ -0,0 +1,35 @@ +# 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. + +"""oslo_i18n integration module for laosclient. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html . + +""" + +import oslo_i18n + + +_translators = oslo_i18n.TranslatorFactory(domain='laosclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/laosclient/osc/__init__.py b/laosclient/osc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laosclient/osc/plugin.py b/laosclient/osc/plugin.py new file mode 100644 index 0000000..b44a052 --- /dev/null +++ b/laosclient/osc/plugin.py @@ -0,0 +1,61 @@ +# 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. +# + +"""OpenStackClient plugin for Functions service.""" + +import logging + +from osc_lib import utils + +DEFAULT_SEARCH_API_VERSION = '1' +API_VERSION_OPTION = 'os_functions_api_version' +API_NAME = 'functions' +API_VERSIONS = { + '1': 'laosclient.v1.client.Client', +} + + +def make_client(instance): + """Returns a functions service client""" + functions_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + + # Set client http_log_debug to True if verbosity level is high enough + http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + + # Remember interface only if it is set + kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface) + client = functions_client( + session=instance.session, + http_log_debug=http_log_debug, + region_name=instance._region_name, + **kwargs + ) + + return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-functions-api-version', + metavar='', + default=utils.env( + 'OS_FUNCTIONS_API_VERSION', + default=DEFAULT_SEARCH_API_VERSION), + help='Functions API version, default=' + + DEFAULT_SEARCH_API_VERSION + + ' (Env: OS_FUNCTIONS_API_VERSION)') + return parser diff --git a/laosclient/osc/v1/__init__.py b/laosclient/osc/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laosclient/osc/v1/apps.py b/laosclient/osc/v1/apps.py new file mode 100644 index 0000000..b771af7 --- /dev/null +++ b/laosclient/osc/v1/apps.py @@ -0,0 +1,133 @@ +# 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. +# + +"""Functions v1 App actions implementations""" + +import json +import logging + +from osc_lib.command import command +from osc_lib import utils + + +class ListApps(command.Lister): + """Lists apps""" + + log = logging.getLogger(__name__ + ".ListApps") + + def take_action(self, parsed_args): + COLUMNS = ( + "name", + "config", + "description", + ) + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + result = [] + for item in fc.apps.list()["apps"]: + result.append(utils.get_dict_properties(item, COLUMNS)) + + return COLUMNS, result + + +class ShowApp(command.ShowOne): + """Show app info""" + + log = logging.getLogger(__name__ + ".ShowApp") + + def get_parser(self, prog_name): + parser = super(ShowApp, self).get_parser(prog_name) + parser.add_argument("name", metavar="", + help="Specifies which app to show") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app_name = parsed_args.name + app = fc.apps.show(app_name)["app"] + keys = list(app.keys()) + return keys, utils.get_dict_properties(app, keys) + + +class CreateApp(command.ShowOne): + """Creates an app""" + + log = logging.getLogger(__name__ + ".CreateApp") + + def get_parser(self, prog_name): + parser = super(CreateApp, self).get_parser(prog_name) + parser.add_argument("name", metavar="", + help="App name to create") + parser.add_argument("--config", metavar="", + help="Config for app to create in JSON format") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app_name = parsed_args.name + config = parsed_args.config + if config: + try: + config = json.loads(config) + except Exception as ex: + self.log.error("Invalid config JSON: Reason: {}".format(str(ex))) + raise ex + + app = fc.apps.create(app_name, config=config)["app"] + keys = list(app.keys()) + return keys, utils.get_dict_properties(app, keys) + + +class DeleteApp(command.Command): + """Deletes an app""" + log = logging.getLogger(__name__ + ".DeleteApp") + + def get_parser(self, prog_name): + parser = super(DeleteApp, self).get_parser(prog_name) + parser.add_argument("name", metavar="", + help="App name to delete") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app_name = parsed_args.name + fc.apps.delete(app_name) + + +class UpdateApp(command.ShowOne): + """Deletes an app""" + log = logging.getLogger(__name__ + ".UpdateApp") + + def get_parser(self, prog_name): + parser = super(UpdateApp, self).get_parser(prog_name) + parser.add_argument("name", metavar="", + help="App name to delete") + parser.add_argument("config", metavar="", + help="Config for app to create in JSON format") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app_name = parsed_args.name + fc.apps.update(app_name) + config = parsed_args.config + if not config: + raise Exception("Nothing to update. " + "App config was not specified.") + app = fc.apps.update(app_name, config=json.loads(config))["app"] + keys = license(app.keys()) + return keys, utils.get_dict_properties(app, keys) diff --git a/laosclient/osc/v1/routes.py b/laosclient/osc/v1/routes.py new file mode 100644 index 0000000..1f974a3 --- /dev/null +++ b/laosclient/osc/v1/routes.py @@ -0,0 +1,214 @@ +# 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. +# + +"""Functions v1 App routes actions implementations""" + +import json +import logging + +from osc_lib.command import command +from osc_lib import utils + + +class ListAppRoutes(command.Lister): + """Lists app routes""" + log = logging.getLogger(__name__ + ".ListAppRoutes") + + def get_parser(self, prog_name): + parser = super(ListAppRoutes, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies which app to show") + return parser + + def take_action(self, parsed_args): + COLUMNS = ( + "type", + "path", + "image", + "memory", + "timeout", + "max_concurrency", + "is_public", + "config", + ) + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app = parsed_args.app + result = [] + for item in fc.routes.list(app)["routes"]: + result.append(utils.get_dict_properties(item, COLUMNS)) + + return COLUMNS, result + + +class ShowAppRoute(command.ShowOne): + """Shows specific route info""" + log = logging.getLogger(__name__ + ".GetAppRoute") + + def get_parser(self, prog_name): + parser = super(ShowAppRoute, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies at which app to search for route") + parser.add_argument("route", metavar="", + help="Specifies which app to show") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app, route_path = parsed_args.app, parsed_args.route + route = fc.routes.show(app, route_path)["route"] + keys = list(route.keys()) + return keys, utils.get_dict_properties(route, keys) + + +class CreateAppRoute(command.ShowOne): + """Creates a new route""" + log = logging.getLogger(__name__ + ".CreateAppRoute") + + def get_parser(self, prog_name): + parser = super(CreateAppRoute, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies which app to show") + parser.add_argument("route", metavar="", + help="App route name to create") + parser.add_argument("type", metavar="", + help="App route type to create") + parser.add_argument("image", metavar="", + help="Docker image to run") + parser.add_argument("--is-public", metavar="", + help="Public/Private route to create") + parser.add_argument("--memory", metavar="", + help="App route memory to allocate, in Mbs") + parser.add_argument("--timeout", metavar="", + help="For how log to run the function") + parser.add_argument("--max-concurrency", metavar="", + help="Cold/Hot container to use.") + parser.add_argument("--config", metavar="", + help="App route config") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + # required + app, route, r_type, image = (parsed_args.app, parsed_args.route, + parsed_args.type, parsed_args.image) + #optional + is_public, memory, timeout, max_c, config = ( + parsed_args.is_public, parsed_args.memory, + parsed_args.timeout, parsed_args.max_concurrency, + parsed_args.config + ) + if config: + try: + config = json.loads(config) + except Exception as ex: + self.log.error("Invalid config JSON: Reason: {}".format(str(ex))) + raise ex + + new_route = fc.routes.create(app, r_type, route, image, + is_public=is_public, memory=memory, + timeout=timeout, max_concurrency=max_c, + config=config)["route"] + keys = list(new_route.keys()) + return keys, utils.get_dict_properties(new_route, keys) + + +class DeleteAppRoute(command.Command): + """Deletes specific route""" + log = logging.getLogger(__name__ + ".DeleteAppRoute") + + def get_parser(self, prog_name): + parser = super(DeleteAppRoute, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies which app to show") + parser.add_argument("route", metavar="", + help="App route to look for") + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app, route = parsed_args.app, parsed_args.route + fc.routes.delete(app, route) + + +class UpdateAppRoute(command.ShowOne): + """Updates specific route""" + log = logging.getLogger(__name__ + ".UpdateAppRoute") + + def get_parser(self, prog_name): + parser = super(UpdateAppRoute, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies which app to show") + parser.add_argument("route", metavar="", + help="App route to look for") + parser.add_argument("--image", metavar="", + help="New Docker image") + parser.add_argument("--memory", metavar="", + help="App route memory to allocate, in Mbs") + parser.add_argument("--timeout", metavar="", + help="For how log to run the function") + parser.add_argument("--max-concurrency", metavar="", + name="max_concurrency", + help="Cold/Hot container to use.") + parser.add_argument("--config", metavar="", + help="App route config") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + #required + app, route = parsed_args.app, parsed_args.route + #optional + image, memory, max_c, config = ( + parsed_args.image, parsed_args.memory, + parsed_args.max_concurrency, parsed_args.config) + + if config: + config = json.loads(config) + + updated_route = fc.routes.update(app, route, **{ + "image": image, + "memory": memory, + "max_concurrency": max_c, + "config": config, + }) + keys = list(updated_route.keys()) + return keys, utils.get_dict_properties(updated_route, keys) + + +class ExecuteAppRoute(command.ShowOne): + """Runs execution against specific route""" + log = logging.getLogger(__name__ + ".ExecuteAppRoute") + + def get_parser(self, prog_name): + parser = super(ExecuteAppRoute, self).get_parser(prog_name) + parser.add_argument("app", metavar="", + help="Specifies which app to show") + parser.add_argument("route", metavar="", + help="App route to look for") + parser.add_argument("--data", metavar="", + help="Execution data") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + fc = self.app.client_manager.functions + app, route, data = parsed_args.app, parsed_args.route, parsed_args.data + if data: + data = json.loads(data) + result = fc.routes.execute(app, route, **data) + clmns = list(result.keys()) + return clmns, utils.get_dict_properties(result, clmns) diff --git a/laosclient/v1/__init__.py b/laosclient/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laosclient/v1/apps.py b/laosclient/v1/apps.py new file mode 100644 index 0000000..5770b96 --- /dev/null +++ b/laosclient/v1/apps.py @@ -0,0 +1,93 @@ +# 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 laosclient import client + + +class Apps(object): + + apps_route = "/v1/{project_id}/apps" + app_route = "/v1/{project_id}/apps/{app}" + + def __init__(self, session_client: client.SessionClient): + self.client = session_client + + @client.inject_project_id + def list(self, project_id): + """ + + :param project_id: + :return: + """ + response = self.client.get( + self.apps_route.format(project_id=project_id)) + return response.json() + + @client.inject_project_id + def show(self, project_id, app_name): + """ + + :param project_id: + :param app_name: + :return: + """ + response = self.client.get( + self.app_route.format(project_id=project_id, + app=app_name)) + return response.json() + + @client.inject_project_id + def create(self, project_id, app_name, config=None): + """ + + :param project_id: + :param app_name: + :param config: + :return: + """ + data = { + "app": { + "name": app_name, + "config": config if config else {} + } + } + response = self.client.post( + self.apps_route.format(project_id=project_id), + json=data) + return response.json() + + @client.inject_project_id + def update(self, project_id, app_name, **data): + """ + + :param project_id: + :param app_name: + :param data: + :return: + """ + response = self.client.put( + self.app_route.format(project_id=project_id, + app=app_name), json=data) + return response.json() + + @client.inject_project_id + def delete(self, project_id, app_name): + """ + + :param project_id: + :param app_name: + :return: + """ + response = self.client.delete( + self.app_route.format(project_id=project_id, + app=app_name)) + return response.json() diff --git a/laosclient/v1/client.py b/laosclient/v1/client.py new file mode 100644 index 0000000..159b42f --- /dev/null +++ b/laosclient/v1/client.py @@ -0,0 +1,39 @@ +# 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 laosclient import client +from laosclient.v1 import apps +from laosclient.v1 import routes + + +class Client(object): + """Client for the Functions v1 API. + + :param session: a keystoneauth session object + :type session: keystoneauth1.session.Session + :param str service_type: The default service_type for URL discovery + :param str interface: The default interface for URL discovery + (Default: public) + :param str region_name: The default region_name for URL discovery + :param str endpoint_override: Always use this endpoint URL for requests + for this laosclient + :param auth: An auth plugin to use instead of the session one + :type auth: keystoneauth1.plugin.BaseAuthPlugin + :param str user_agent: The User-Agent string to set + (Default is python-laosclient) + """ + + def __init__(self, *args, **kwargs): + """Initialize a new client for the Functions v1 API.""" + self.http_client = client.construct_http_client(*args, **kwargs) + self.apps = apps.Apps(self.http_client) + self.routes = routes.Routes(self.http_client) diff --git a/laosclient/v1/routes.py b/laosclient/v1/routes.py new file mode 100644 index 0000000..a58aaa2 --- /dev/null +++ b/laosclient/v1/routes.py @@ -0,0 +1,133 @@ +# 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 json + +from laosclient import client + + +class Routes(object): + + routes_path = "/v1/{project_id}/apps/{app}/routes" + route_path = "/v1/{project_id}/apps/{app}/routes{route_path}" + private_execution = "/v1/r/{app}{route_path}" + public_execution = "/r/{app}{route_path}" + + def __init__(self, session_client: client.SessionClient): + self.client = session_client + + @client.inject_project_id + def create(self, project_id, app_name, execution_type, + route_path, image, is_public=False, memory=None, + timeout=None, max_concurrency=None, config=None): + """ + Creates an app route + + :param app_name: + :param execution_type: + :param route_path: + :param image: + :param is_public: + :param memory: + :param timeout: + :param max_concurrency: + :param config: + :return: + """ + body = { + "route": { + "type": execution_type, + "path": route_path, + "image": image, + "memory": memory if memory else 128, + "timeout": timeout if timeout else 30, + "max_concurrency": (max_concurrency + if max_concurrency else 1), + "is_public": str(is_public if + is_public is not None else False).lower(), + "config": config if config else {}, + } + } + response = self.client.post(self.routes_path.format( + project_id=project_id, app=app_name), json=body) + return response.json() + + @client.inject_project_id + def list(self, project_id, app_name): + """ + + :param app_name: + :return: + """ + response = self.client.get(self.routes_path.format( + project_id=project_id, app=app_name)) + return response.json() + + @client.inject_project_id + def show(self, project_id, app_name, route_path): + """ + + :param app_name: + :param route_path: + :return: + """ + response = self.client.get(self.route_path.format( + project_id=project_id, app=app_name, + route_path=route_path)) + return response.json() + + @client.inject_project_id + def update(self, project_id, app_name, route_path, **data): + """ + + :param app_name: + :param route_path: + :param data: + :return: + """ + response = self.client.put(self.route_path.format( + project_id=project_id, app=app_name, + route_path=route_path), json=data) + return response.json() + + @client.inject_project_id + def delete(self, project_id, app_name, route_path): + """ + + :param app_name: + :param route_path: + :return: + """ + response = self.client.delete( + self.route_path.format( + project_id=project_id, app=app_name, + route_path=route_path)) + return response.json() + + @client.inject_project_id + def execute(self, project_id, app_name, route_path, **data): + """ + + :param app_name: + :param route_path: + :param data: + :return: + """ + route = self.show(app_name, route_path) + is_public = json.loads(route.get("is_public")) + url = (self.public_execution.format( + app=app_name, route_path=route_path) if is_public else + self.private_execution.format( + project_id=project_id, app=app_name, + route_path=route_path)) + response = self.client.post(url, json=data) + return response.json() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7efb819 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +# 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.8 # Apache-2.0 +osc-lib>=1.2.0 # Apache-2.0 +oslo.i18n>=2.1.0 # Apache-2.0 +oslo.utils>=3.18.0 # Apache-2.0 + +python-keystoneclient>=3.6.0 # Apache-2.0 +python-openstackclient>=3.3.0 # Apache-2.0 + +PyYAML>=3.10.0 # MIT diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..53d2433 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,53 @@ +[metadata] +name = python-laosclient +summary = OpenStack Functions API Client Library +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://docs.openstack.org/developer/python-laosclient +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 :: 3.4 + Programming Language :: Python :: 3.5 + +[files] +packages = + laosclient + +[entry_points] +openstack.cli.extension = + functions = laosclient.osc.plugin + +openstack.functions.v1 = + fn-apps_list = laosclient.osc.v1.apps:ListApps + fn-apps_show = laosclient.osc.v1.apps:ShowApp + fn-apps_create = laosclient.osc.v1.apps:CreateApp + fn-apps_delete = laosclient.osc.v1.apps:DeleteApp + fn-apps_update = laosclient.osc.v1.apps:UpdateApp + fn-app_routes_list = laosclient.osc.v1.routes:ListAppRoutes + fn-app_routes_show = laosclient.osc.v1.routes:ShowAppRoute + fn-app_routes_create = laosclient.osc.v1.routes:CreateAppRoute + fn-app_routes_delete = laosclient.osc.v1.routes:DeleteAppRoute + fn-app_routes_update = laosclient.osc.v1.routes:UpdateAppRoute + fn-app_route_execute = laosclient.osc.v1.routes:ExecuteAppRoute + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..782bb21 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..47b27db --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,13 @@ +# 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 already pins down pep8, pyflakes and flake8 +hacking<0.11,>=0.10.2 +coverage>=4.0 # Apache-2.0 +fixtures>=3.0.0 # Apache-2.0/BSD +mock>=2.0 # BSD +oslosphinx>=4.7.0 # Apache-2.0 +sphinx!=1.3b1,<1.4,>=1.2.1 # BSD +testrepository>=0.0.18 # Apache-2.0/BSD +testtools>=1.4.0 # MIT diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ba91455 --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +envlist = pypy,py34,py35,pep8 +minversion = 1.6 +skipsdist = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} +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 --slowest --testr-args='{posargs}' +whitelist_externals = find + +[testenv:pypy] +deps = setuptools<3.2 + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +[testenv:pep8] +sitepackages = False +commands = flake8 + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[testenv:docs] +commands= + python setup.py build_sphinx + +[flake8] +ignore = E123,E126,E128,E241,E265,E713,H202,H405,H238,H404 +show-source = True +exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,*venv* +max-complexity=20 + +[hacking] +import_exceptions = laosclient.openstack.common._i18n