From c74d8cdd3b09a433fe48db829e9625465423fa2c Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Mon, 12 Sep 2016 18:28:14 +0200 Subject: [PATCH 01/24] Provide CLI support for runnning specifications --- connexion/cli.py | 56 ++++++++++++++++++++++++++++++++++++ connexion/utils.py | 1 - requirements.txt | 1 + setup.py | 3 +- tests/api/test_parameters.py | 2 -- tests/test_cli.py | 52 +++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 connexion/cli.py create mode 100644 tests/test_cli.py diff --git a/connexion/cli.py b/connexion/cli.py new file mode 100644 index 000000000..2ea723a83 --- /dev/null +++ b/connexion/cli.py @@ -0,0 +1,56 @@ +import logging +import sys +from os import path + +import click +from connexion import App + +from clickclick import AliasedGroup + +main = AliasedGroup(context_settings=dict(help_option_names=[ + '-h', '--help'])) + + +@main.command() +@click.argument('spec_file') +@click.argument('base_path', required=False) +@click.option('--port', '-p', default=5000, type=int, help='Port to listen.') +@click.option('--server', '-s', default='gevent', + type=click.Choice(['gevent', 'tornado']), + help='Which WSGI server to use.') +@click.option('--hide-spec', + help='Hides the API spec in JSON format which is by default available at `/swagger.json`.', + is_flag=True, default=True) +@click.option('--hide-swagger-ui', + help='Hides the the Swagger UI which is by default available at `/ui`.', + is_flag=True, default=True) +@click.option('--swagger-ui-url', metavar='URL', + help='Personalize what URL path the Swagger UI will be mounted.') +@click.option('--swagger-ui-from', metavar='PATH', + help='Path to a customized Swagger UI dashboard.') +@click.option('--auth-all-paths', + help='Enable authentication to paths not defined in the spec.', + is_flag=True, default=False) +@click.option('--debug', '-d', help='Show debugging information.', + is_flag=True, default=False) +def run(spec_file, base_path, port, server, debug): + """ + Runs a server using the passed OpenAPI/Swagger 2.0 Specification file. + + Possible arguments: + + - SPEC_FILE: specification file of the API to run. + + - BASE_PATH (optional): filesystem path from where to import the API handlers. + """ + logging_level = logging.ERROR + if debug: + logging_level = logging.DEBUG + logging.basicConfig(level=logging_level) + + sys.path.insert(1, path.abspath(base_path or '.')) + + app = App(__name__) + app.add_api(path.abspath(spec_file)) + click.echo('Running at http://localhost:{}/...'.format(port)) + app.run(port=port, server=server) diff --git a/connexion/utils.py b/connexion/utils.py index 3dc5f94ca..5573c5eec 100644 --- a/connexion/utils.py +++ b/connexion/utils.py @@ -20,7 +20,6 @@ import flask import werkzeug.wrappers - PATH_PARAMETER = re.compile(r'\{([^}]*)\}') # map Swagger type to flask path converter diff --git a/requirements.txt b/requirements.txt index 0b6240e02..b62329756 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ requests>=2.9.1 six>=1.7 strict-rfc3339>=0.6 swagger_spec_validator>=2.0.2 +clickclick>=1.1 diff --git a/setup.py b/setup.py index d2d413a83..d26ebc1d6 100755 --- a/setup.py +++ b/setup.py @@ -88,5 +88,6 @@ def readme(): 'Topic :: Software Development :: Libraries :: Application Frameworks' ], include_package_data=True, # needed to include swagger-ui (see MANIFEST.in) - + entry_points={'console_scripts': ['connexion = connexion.cli:main', + 'cnx = connexion.cli:main']} ) diff --git a/tests/api/test_parameters.py b/tests/api/test_parameters.py index b25c0396f..8e4211745 100644 --- a/tests/api/test_parameters.py +++ b/tests/api/test_parameters.py @@ -287,5 +287,3 @@ def test_args_kwargs(simple_app): resp = app_client.get('/v1.0/query-params-as-kwargs?foo=a&bar=b') assert resp.status_code == 200 assert json.loads(resp.data.decode()) == {'foo': 'a'} - - diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 000000000..f6eccdef5 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,52 @@ +import logging + +from click.testing import CliRunner +from connexion import App, __version__ +from connexion.cli import main + +import pytest +from conftest import FIXTURES_FOLDER +from mock import MagicMock + + +@pytest.fixture() +def mock_app_run(monkeypatch): + test_server = MagicMock(wraps=App(__name__)) + test_server.run = MagicMock(return_value=True) + test_app = MagicMock(return_value=test_server) + monkeypatch.setattr('connexion.cli.App', test_app) + return test_server + + +def test_run_missing_spec(): + runner = CliRunner() + result = runner.invoke(main, ['run'], catch_exceptions=False) + assert "Missing argument" in result.output + + +def test_run_simple_spec(mock_app_run): + spec_file = str(FIXTURES_FOLDER / 'simple/swagger.yaml') + default_port = 5000 + default_server = 'gevent' + runner = CliRunner() + result = runner.invoke(main, + ['run', spec_file], + catch_exceptions=False) + + mock_app_run.run.assert_called_with(port=default_port, server=default_server) + assert 'Running at' in result.output + + +def test_run_in_debug_mode(mock_app_run, monkeypatch): + spec_file = str(FIXTURES_FOLDER / 'simple/swagger.yaml') + + logging_config = MagicMock(name='connexion.cli.logging.basicConfig') + monkeypatch.setattr('connexion.cli.logging.basicConfig', + logging_config) + + runner = CliRunner() + result = runner.invoke(main, + ['run', spec_file, '-d'], + catch_exceptions=False) + + logging_config.assert_called_with(level=logging.DEBUG) From e9d5a61437e4c8933e5ac6ba6ce109b1d7b8e275 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Mon, 12 Sep 2016 21:55:47 +0200 Subject: [PATCH 02/24] Run with specification stubs --- connexion/api.py | 7 ++-- connexion/cli.py | 80 +++++++++++++++++++++++++++++++++---------- connexion/resolver.py | 15 ++++++++ connexion/utils.py | 7 ++-- tests/test_cli.py | 16 +++------ 5 files changed, 90 insertions(+), 35 deletions(-) diff --git a/connexion/api.py b/connexion/api.py index f9e3100fe..38aeb95aa 100644 --- a/connexion/api.py +++ b/connexion/api.py @@ -23,7 +23,8 @@ import yaml from swagger_spec_validator.validator20 import validate_spec -from . import resolver, utils +from . import utils +from .resolver import Resolver from .handlers import AuthErrorHandler from .operation import Operation @@ -62,7 +63,7 @@ class Api(object): def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_json=None, swagger_ui=None, swagger_path=None, swagger_url=None, - validate_responses=False, strict_validation=False, resolver=resolver.Resolver(), + validate_responses=False, strict_validation=False, resolver=None, auth_all_paths=False, debug=False): """ :type swagger_yaml_path: pathlib.Path @@ -129,7 +130,7 @@ def __init__(self, swagger_yaml_path, base_url=None, arguments=None, self.swagger_path = swagger_path or SWAGGER_UI_PATH self.swagger_url = swagger_url or SWAGGER_UI_URL - self.resolver = resolver + self.resolver = resolver or Resolver() logger.debug('Validate Responses: %s', str(validate_responses)) self.validate_responses = validate_responses diff --git a/connexion/cli.py b/connexion/cli.py index 2ea723a83..81ece04d6 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -3,45 +3,84 @@ from os import path import click -from connexion import App +from connexion import App, problem +from connexion.resolver import StubResolver + +from clickclick import AliasedGroup, fatal_error -from clickclick import AliasedGroup main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) +def _operation_not_implemented(): + return problem( + title='Not Implemented Yet', + detail='The requested functionality is not implemented yet.', + status=400) + + +def validate_wsgi_server_requirements(ctx, param, value): + if value == 'gevent': + try: + import gevent.wsgi # NOQA + except: + fatal_error('gevent library is not installed') + elif value == 'tornado': + try: + import tornado.wsgi # NOQA + import tornado.httpserver # NOQA + import tornado.ioloop # NOQA + except: + fatal_error('tornado library not installed') + + @main.command() @click.argument('spec_file') @click.argument('base_path', required=False) @click.option('--port', '-p', default=5000, type=int, help='Port to listen.') -@click.option('--server', '-s', default='gevent', - type=click.Choice(['gevent', 'tornado']), - help='Which WSGI server to use.') +@click.option('--wsgi-server', '-w', default='flask', + type=click.Choice(['flask', 'gevent', 'tornado']), + callback=validate_wsgi_server_requirements, + help='Which WSGI server container to use.') +@click.option('--stub', '-s', + help='Returns status code 400, and `Not Implemented Yet` payload, for ' + 'the endpoints which handlers are not found.', + is_flag=True, default=False) @click.option('--hide-spec', help='Hides the API spec in JSON format which is by default available at `/swagger.json`.', is_flag=True, default=True) -@click.option('--hide-swagger-ui', - help='Hides the the Swagger UI which is by default available at `/ui`.', +@click.option('--hide-console-ui', + help='Hides the the API console UI which is by default available at `/ui`.', is_flag=True, default=True) -@click.option('--swagger-ui-url', metavar='URL', - help='Personalize what URL path the Swagger UI will be mounted.') -@click.option('--swagger-ui-from', metavar='PATH', - help='Path to a customized Swagger UI dashboard.') +@click.option('--console-ui-url', metavar='URL', + help='Personalize what URL path the API console UI will be mounted.') +@click.option('--console-ui-from', metavar='PATH', + help='Path to a customized API console UI dashboard.') @click.option('--auth-all-paths', help='Enable authentication to paths not defined in the spec.', is_flag=True, default=False) @click.option('--debug', '-d', help='Show debugging information.', is_flag=True, default=False) -def run(spec_file, base_path, port, server, debug): +def run(spec_file, + base_path, + port, + wsgi_server, + stub, + hide_spec, + hide_console_ui, + console_ui_url, + console_ui_from, + auth_all_paths, + debug): """ - Runs a server using the passed OpenAPI/Swagger 2.0 Specification file. + Runs a server compliant with a OpenAPI/Swagger 2.0 Specification file. - Possible arguments: + Arguments: - - SPEC_FILE: specification file of the API to run. + - SPEC_FILE: specification file that describes the server endpoints. - - BASE_PATH (optional): filesystem path from where to import the API handlers. + - BASE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. """ logging_level = logging.ERROR if debug: @@ -50,7 +89,10 @@ def run(spec_file, base_path, port, server, debug): sys.path.insert(1, path.abspath(base_path or '.')) + resolver = None + if stub: + resolver = StubResolver(_operation_not_implemented) + app = App(__name__) - app.add_api(path.abspath(spec_file)) - click.echo('Running at http://localhost:{}/...'.format(port)) - app.run(port=port, server=server) + app.add_api(path.abspath(spec_file), resolver=resolver) + app.run(port=port, server=wsgi_server) diff --git a/connexion/resolver.py b/connexion/resolver.py index 519fb4bea..67cc5f9cd 100644 --- a/connexion/resolver.py +++ b/connexion/resolver.py @@ -133,3 +133,18 @@ def get_function_name(): return self.collection_endpoint_name if is_collection_endpoint else method.lower() return get_controller_name() + '.' + get_function_name() + + +class StubResolver(Resolver): + def __init__(self, stub_function, **kwargs): + self.stub_function = stub_function + super(StubResolver, self).__init__(**kwargs) + + def resolve_function_from_operation_id(self, operation_id): + """ + In case a function for the operation is not found a stub function is returned. + """ + try: + super(StubResolver, self).resolve_function_from_operation_id(operation_id) + except ImportError: + return self.stub_function diff --git a/connexion/utils.py b/connexion/utils.py index 5573c5eec..96ff91f41 100644 --- a/connexion/utils.py +++ b/connexion/utils.py @@ -119,8 +119,11 @@ def get_function_from_name(function_name): module = importlib.import_module(module_name) except ImportError as import_error: last_import_error = import_error - module_name, attr_path1 = module_name.rsplit('.', 1) - attr_path = '{0}.{1}'.format(attr_path1, attr_path) + if '.' in module_name: + module_name, attr_path1 = module_name.rsplit('.', 1) + attr_path = '{0}.{1}'.format(attr_path1, attr_path) + else: + raise try: function = deep_getattr(module, attr_path) except AttributeError: diff --git a/tests/test_cli.py b/tests/test_cli.py index f6eccdef5..d541fd45d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,11 @@ import logging from click.testing import CliRunner -from connexion import App, __version__ -from connexion.cli import main import pytest from conftest import FIXTURES_FOLDER +from connexion import App +from connexion.cli import main from mock import MagicMock @@ -27,14 +27,10 @@ def test_run_missing_spec(): def test_run_simple_spec(mock_app_run): spec_file = str(FIXTURES_FOLDER / 'simple/swagger.yaml') default_port = 5000 - default_server = 'gevent' runner = CliRunner() - result = runner.invoke(main, - ['run', spec_file], - catch_exceptions=False) + runner.invoke(main, ['run', spec_file], catch_exceptions=False) - mock_app_run.run.assert_called_with(port=default_port, server=default_server) - assert 'Running at' in result.output + mock_app_run.run.assert_called_with(port=default_port, server=None) def test_run_in_debug_mode(mock_app_run, monkeypatch): @@ -45,8 +41,6 @@ def test_run_in_debug_mode(mock_app_run, monkeypatch): logging_config) runner = CliRunner() - result = runner.invoke(main, - ['run', spec_file, '-d'], - catch_exceptions=False) + runner.invoke(main, ['run', spec_file, '-d'], catch_exceptions=False) logging_config.assert_called_with(level=logging.DEBUG) From 9c5f6bcd8a6f9a3b988c4747bf7dc52624a1460b Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Mon, 12 Sep 2016 22:58:38 +0200 Subject: [PATCH 03/24] Refine stub option --- connexion/cli.py | 20 +++---- connexion/resolver.py | 2 +- .../missing_implementation/swagger.yaml | 16 ++++++ .../module_does_not_exist/swagger.yaml | 16 ++++++ tests/test_cli.py | 54 +++++++++++++++++-- 5 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/missing_implementation/swagger.yaml create mode 100644 tests/fixtures/module_does_not_exist/swagger.yaml diff --git a/connexion/cli.py b/connexion/cli.py index 81ece04d6..7d9f2d176 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -13,26 +13,17 @@ '-h', '--help'])) -def _operation_not_implemented(): - return problem( - title='Not Implemented Yet', - detail='The requested functionality is not implemented yet.', - status=400) - - def validate_wsgi_server_requirements(ctx, param, value): if value == 'gevent': try: - import gevent.wsgi # NOQA + import gevent # NOQA except: fatal_error('gevent library is not installed') elif value == 'tornado': try: - import tornado.wsgi # NOQA - import tornado.httpserver # NOQA - import tornado.ioloop # NOQA + import tornado # NOQA except: - fatal_error('tornado library not installed') + fatal_error('tornado library is not installed') @main.command() @@ -91,7 +82,10 @@ def run(spec_file, resolver = None if stub: - resolver = StubResolver(_operation_not_implemented) + resolver = StubResolver(lambda: problem( + title='Not Implemented Yet', + detail='The requested functionality is not implemented yet.', + status=400)) app = App(__name__) app.add_api(path.abspath(spec_file), resolver=resolver) diff --git a/connexion/resolver.py b/connexion/resolver.py index 67cc5f9cd..d68f6ba5c 100644 --- a/connexion/resolver.py +++ b/connexion/resolver.py @@ -146,5 +146,5 @@ def resolve_function_from_operation_id(self, operation_id): """ try: super(StubResolver, self).resolve_function_from_operation_id(operation_id) - except ImportError: + except (ImportError, AttributeError): return self.stub_function diff --git a/tests/fixtures/missing_implementation/swagger.yaml b/tests/fixtures/missing_implementation/swagger.yaml new file mode 100644 index 000000000..64d92b4e9 --- /dev/null +++ b/tests/fixtures/missing_implementation/swagger.yaml @@ -0,0 +1,16 @@ +swagger: "2.0" + +info: + title: "Testing API" + version: "1.0" + +basePath: "/testing" + +paths: + /operation-not-implemented: + get: + summary: Operation function does not exist. + operationId: api.this_function_does_not_exist + responses: + 200: + description: OK diff --git a/tests/fixtures/module_does_not_exist/swagger.yaml b/tests/fixtures/module_does_not_exist/swagger.yaml new file mode 100644 index 000000000..1c16d39fa --- /dev/null +++ b/tests/fixtures/module_does_not_exist/swagger.yaml @@ -0,0 +1,16 @@ +swagger: "2.0" + +info: + title: "Not Exist API" + version: "1.0" + +basePath: '/na' + +paths: + /module-not-implemented: + get: + summary: Operation function does not exist. + operationId: m.module_does_not_exist + responses: + 200: + description: OK diff --git a/tests/test_cli.py b/tests/test_cli.py index d541fd45d..cc54391ad 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,14 +18,18 @@ def mock_app_run(monkeypatch): return test_server +@pytest.fixture() +def spec_file(): + return str(FIXTURES_FOLDER / 'simple/swagger.yaml') + + def test_run_missing_spec(): runner = CliRunner() result = runner.invoke(main, ['run'], catch_exceptions=False) assert "Missing argument" in result.output -def test_run_simple_spec(mock_app_run): - spec_file = str(FIXTURES_FOLDER / 'simple/swagger.yaml') +def test_run_simple_spec(mock_app_run, spec_file): default_port = 5000 runner = CliRunner() runner.invoke(main, ['run', spec_file], catch_exceptions=False) @@ -33,9 +37,7 @@ def test_run_simple_spec(mock_app_run): mock_app_run.run.assert_called_with(port=default_port, server=None) -def test_run_in_debug_mode(mock_app_run, monkeypatch): - spec_file = str(FIXTURES_FOLDER / 'simple/swagger.yaml') - +def test_run_in_debug_mode(mock_app_run, spec_file, monkeypatch): logging_config = MagicMock(name='connexion.cli.logging.basicConfig') monkeypatch.setattr('connexion.cli.logging.basicConfig', logging_config) @@ -44,3 +46,45 @@ def test_run_in_debug_mode(mock_app_run, monkeypatch): runner.invoke(main, ['run', spec_file, '-d'], catch_exceptions=False) logging_config.assert_called_with(level=logging.DEBUG) + + +def test_run_unimplemented_operations_and_stub(mock_app_run): + runner = CliRunner() + + spec_file = str(FIXTURES_FOLDER / 'missing_implementation/swagger.yaml') + with pytest.raises(AttributeError): + runner.invoke(main, ['run', spec_file], catch_exceptions=False) + # yet can be run with --stub option + result = runner.invoke(main, ['run', spec_file, '--stub'], catch_exceptions=False) + assert result.exit_code == 0 + + spec_file = str(FIXTURES_FOLDER / 'module_does_not_exist/swagger.yaml') + with pytest.raises(ImportError): + runner.invoke(main, ['run', spec_file], catch_exceptions=False) + # yet can be run with -s (stub) option + result = runner.invoke(main, ['run', spec_file, '-s'], catch_exceptions=False) + assert result.exit_code == 0 + + +def test_run_with_wsgi_containers(mock_app_run, spec_file): + runner = CliRunner() + + # missing gevent + result = runner.invoke(main, + ['run', spec_file, '-w', 'gevent'], + catch_exceptions=False) + assert 'gevent library is not installed' in result.output + assert result.exit_code == 1 + + # missing tornado + result = runner.invoke(main, + ['run', spec_file, '-w', 'tornado'], + catch_exceptions=False) + assert 'tornado library is not installed' in result.output + assert result.exit_code == 1 + + # using flask + result = runner.invoke(main, + ['run', spec_file, '-w', 'flask'], + catch_exceptions=False) + assert result.exit_code == 0 From 82dcb617a9ddd26d30cedd39f9132fcc3247add4 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Mon, 12 Sep 2016 23:18:22 +0200 Subject: [PATCH 04/24] Pass along all options available --- connexion/cli.py | 26 ++++++++++++++++++++++---- tests/test_cli.py | 16 +++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 7d9f2d176..6c676e72b 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -34,16 +34,16 @@ def validate_wsgi_server_requirements(ctx, param, value): type=click.Choice(['flask', 'gevent', 'tornado']), callback=validate_wsgi_server_requirements, help='Which WSGI server container to use.') -@click.option('--stub', '-s', +@click.option('--stub', help='Returns status code 400, and `Not Implemented Yet` payload, for ' 'the endpoints which handlers are not found.', is_flag=True, default=False) @click.option('--hide-spec', help='Hides the API spec in JSON format which is by default available at `/swagger.json`.', - is_flag=True, default=True) + is_flag=True, default=False) @click.option('--hide-console-ui', help='Hides the the API console UI which is by default available at `/ui`.', - is_flag=True, default=True) + is_flag=True, default=False) @click.option('--console-ui-url', metavar='URL', help='Personalize what URL path the API console UI will be mounted.') @click.option('--console-ui-from', metavar='PATH', @@ -51,6 +51,12 @@ def validate_wsgi_server_requirements(ctx, param, value): @click.option('--auth-all-paths', help='Enable authentication to paths not defined in the spec.', is_flag=True, default=False) +@click.option('--validate-responses', + help='Enable validation of response values from operation handlers.', + is_flag=True, default=False) +@click.option('--strict-validation', + help='Enable strict validation of request payloads.', + is_flag=True, default=False) @click.option('--debug', '-d', help='Show debugging information.', is_flag=True, default=False) def run(spec_file, @@ -63,6 +69,8 @@ def run(spec_file, console_ui_url, console_ui_from, auth_all_paths, + validate_responses, + strict_validation, debug): """ Runs a server compliant with a OpenAPI/Swagger 2.0 Specification file. @@ -89,4 +97,14 @@ def run(spec_file, app = App(__name__) app.add_api(path.abspath(spec_file), resolver=resolver) - app.run(port=port, server=wsgi_server) + app.run( + port=port, + server=wsgi_server, + swagger_json=hide_spec or None, + swagger_ui=hide_console_ui or None, + swagger_path=console_ui_from or None, + swagger_url=console_ui_url or None, + strict_validation=strict_validation, + validate_responses=validate_responses, + auth_all_paths=auth_all_paths, + debug=debug) diff --git a/tests/test_cli.py b/tests/test_cli.py index cc54391ad..f764b64d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -34,7 +34,17 @@ def test_run_simple_spec(mock_app_run, spec_file): runner = CliRunner() runner.invoke(main, ['run', spec_file], catch_exceptions=False) - mock_app_run.run.assert_called_with(port=default_port, server=None) + mock_app_run.run.assert_called_with( + port=default_port, + server=None, + strict_validation=False, + swagger_json=None, + swagger_path=None, + swagger_ui=None, + swagger_url=None, + auth_all_paths=False, + validate_responses=False, + debug=False) def test_run_in_debug_mode(mock_app_run, spec_file, monkeypatch): @@ -61,8 +71,8 @@ def test_run_unimplemented_operations_and_stub(mock_app_run): spec_file = str(FIXTURES_FOLDER / 'module_does_not_exist/swagger.yaml') with pytest.raises(ImportError): runner.invoke(main, ['run', spec_file], catch_exceptions=False) - # yet can be run with -s (stub) option - result = runner.invoke(main, ['run', spec_file, '-s'], catch_exceptions=False) + # yet can be run with --stub option + result = runner.invoke(main, ['run', spec_file, '--stub'], catch_exceptions=False) assert result.exit_code == 0 From e7f96e4bd9049572a19eb67eb5c2fcc9f66d17f5 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Tue, 13 Sep 2016 00:11:43 +0200 Subject: [PATCH 05/24] Sort imports --- connexion/api.py | 2 +- connexion/cli.py | 1 - tests/test_cli.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/connexion/api.py b/connexion/api.py index 38aeb95aa..488bb8ad3 100644 --- a/connexion/api.py +++ b/connexion/api.py @@ -24,9 +24,9 @@ from swagger_spec_validator.validator20 import validate_spec from . import utils -from .resolver import Resolver from .handlers import AuthErrorHandler from .operation import Operation +from .resolver import Resolver MODULE_PATH = pathlib.Path(__file__).absolute().parent SWAGGER_UI_PATH = MODULE_PATH / 'vendor' / 'swagger-ui' diff --git a/connexion/cli.py b/connexion/cli.py index 6c676e72b..6a526b8ac 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -8,7 +8,6 @@ from clickclick import AliasedGroup, fatal_error - main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) diff --git a/tests/test_cli.py b/tests/test_cli.py index f764b64d8..b6411ab67 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,11 @@ import logging from click.testing import CliRunner +from connexion import App +from connexion.cli import main import pytest from conftest import FIXTURES_FOLDER -from connexion import App -from connexion.cli import main from mock import MagicMock From 97adb2588d66a1c8a4d641ab7e168605f96e88e9 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Tue, 13 Sep 2016 00:37:11 +0200 Subject: [PATCH 06/24] Sort imports --- connexion/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 6a526b8ac..e423087fc 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -3,11 +3,10 @@ from os import path import click +from clickclick import AliasedGroup, fatal_error from connexion import App, problem from connexion.resolver import StubResolver -from clickclick import AliasedGroup, fatal_error - main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) From d9b59c65ace826e858865ab11d34d27a5bf55b88 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Tue, 13 Sep 2016 22:44:15 +0200 Subject: [PATCH 07/24] Fix imports --- connexion/cli.py | 3 ++- examples/sqlalchemy/app.py | 1 + tests/test_cli.py | 2 +- tests/test_operation.py | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 9483d0985..7dc4127e3 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -3,8 +3,9 @@ from os import path import click +from connexion import App + from clickclick import AliasedGroup, fatal_error -from connexion import App, problem main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) diff --git a/examples/sqlalchemy/app.py b/examples/sqlalchemy/app.py index ff23540be..bedf9eb2d 100755 --- a/examples/sqlalchemy/app.py +++ b/examples/sqlalchemy/app.py @@ -4,6 +4,7 @@ import connexion from connexion import NoContent + import orm db_session = None diff --git a/tests/test_cli.py b/tests/test_cli.py index 3822f93b8..eef8a1b93 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,8 +2,8 @@ from click.testing import CliRunner from connexion import App -from connexion.exceptions import ResolverError from connexion.cli import main +from connexion.exceptions import ResolverError import pytest from conftest import FIXTURES_FOLDER diff --git a/tests/test_operation.py b/tests/test_operation.py index 3794706be..2f44ac5d1 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -8,7 +8,6 @@ import pytest - TEST_FOLDER = pathlib.Path(__file__).parent DEFINITIONS = {'new_stack': {'required': ['image_version', 'keep_stacks', 'new_traffic', 'senza_yaml'], From 7ecb585167c8a787b912090a68b5c172cb4c160d Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Tue, 13 Sep 2016 22:56:38 +0200 Subject: [PATCH 08/24] Fix imports --- connexion/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 7dc4127e3..902155a20 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -3,9 +3,8 @@ from os import path import click -from connexion import App - from clickclick import AliasedGroup, fatal_error +from connexion import App main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) From 186f33b5ecaa6319713fd040de2ce0f515ca06e1 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 06:54:12 +0200 Subject: [PATCH 09/24] Use clickclick>=1.2 with support to Python 2.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b62329756..0d7af9e8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ requests>=2.9.1 six>=1.7 strict-rfc3339>=0.6 swagger_spec_validator>=2.0.2 -clickclick>=1.1 +clickclick>=1.2 From dbe20e9a14adabbd8702d8b5b22323bd47dede9d Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 06:56:09 +0200 Subject: [PATCH 10/24] Remove cnx alias to CLI --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d26ebc1d6..e1b906e92 100755 --- a/setup.py +++ b/setup.py @@ -88,6 +88,5 @@ def readme(): 'Topic :: Software Development :: Libraries :: Application Frameworks' ], include_package_data=True, # needed to include swagger-ui (see MANIFEST.in) - entry_points={'console_scripts': ['connexion = connexion.cli:main', - 'cnx = connexion.cli:main']} + entry_points={'console_scripts': ['connexion = connexion.cli:main']} ) From 07c6eb7b8aa806192ce3397e3c3e65b8499ffbe0 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 21:06:17 +0200 Subject: [PATCH 11/24] Fix parameters in the CLI interface --- connexion/cli.py | 33 +++++++++++++++++++-------------- tests/test_cli.py | 7 ------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 902155a20..127bd6623 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -83,22 +83,27 @@ def run(spec_file, logging_level = logging.DEBUG logging.basicConfig(level=logging_level) - sys.path.insert(1, path.abspath(base_path or '.')) + spec_file_full_path = path.abspath(spec_file) + base_path = base_path or path.dirname(spec_file_full_path) + sys.path.insert(1, path.abspath(base_path)) resolver_error = None if stub: resolver_error = 501 - app = App(__name__) - app.add_api(path.abspath(spec_file), resolver_error=resolver_error) - app.run( - port=port, - server=wsgi_server, - swagger_json=hide_spec or None, - swagger_ui=hide_console_ui or None, - swagger_path=console_ui_from or None, - swagger_url=console_ui_url or None, - strict_validation=strict_validation, - validate_responses=validate_responses, - auth_all_paths=auth_all_paths, - debug=debug) + app = App(__name__, + swagger_json=hide_spec is False, + swagger_ui=hide_console_ui is False, + swagger_path=console_ui_from or None, + swagger_url=console_ui_url or None, + auth_all_paths=auth_all_paths, + debug=debug) + + app.add_api(spec_file_full_path, + resolver_error=resolver_error, + validate_responses=validate_responses, + strict_validation=strict_validation) + + app.run(port=port, + server=wsgi_server, + debug=debug) diff --git a/tests/test_cli.py b/tests/test_cli.py index eef8a1b93..fa6640220 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -38,13 +38,6 @@ def test_run_simple_spec(mock_app_run, spec_file): mock_app_run.run.assert_called_with( port=default_port, server=None, - strict_validation=False, - swagger_json=None, - swagger_path=None, - swagger_ui=None, - swagger_url=None, - auth_all_paths=False, - validate_responses=False, debug=False) From 2208a95d7753fcdd2dbe2cd83efa781d3cc87019 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 21:42:00 +0200 Subject: [PATCH 12/24] Directories to be considered modules in Python2.7 needs to contain a __init__.py file --- tests/api/__init__.py | 0 tests/decorators/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/api/__init__.py create mode 100644 tests/decorators/__init__.py diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/decorators/__init__.py b/tests/decorators/__init__.py new file mode 100644 index 000000000..e69de29bb From b024a90a96e0dd95691662f87c130447c8f0e55d Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 21:54:51 +0200 Subject: [PATCH 13/24] Add basic logging to CLI for troubleshooting --- connexion/cli.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 127bd6623..f5620c3c7 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -6,6 +6,8 @@ from clickclick import AliasedGroup, fatal_error from connexion import App +logger = logging.getLogger('connexion.cli') + main = AliasedGroup(context_settings=dict(help_option_names=[ '-h', '--help'])) @@ -25,7 +27,7 @@ def validate_wsgi_server_requirements(ctx, param, value): @main.command() @click.argument('spec_file') -@click.argument('base_path', required=False) +@click.argument('base_module_path', required=False) @click.option('--port', '-p', default=5000, type=int, help='Port to listen.') @click.option('--wsgi-server', '-w', default='flask', type=click.Choice(['flask', 'gevent', 'tornado']), @@ -57,7 +59,7 @@ def validate_wsgi_server_requirements(ctx, param, value): @click.option('--debug', '-d', help='Show debugging information.', is_flag=True, default=False) def run(spec_file, - base_path, + base_module_path, port, wsgi_server, stub, @@ -76,7 +78,7 @@ def run(spec_file, - SPEC_FILE: specification file that describes the server endpoints. - - BASE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. + - BASE_MODULE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. """ logging_level = logging.ERROR if debug: @@ -84,8 +86,9 @@ def run(spec_file, logging.basicConfig(level=logging_level) spec_file_full_path = path.abspath(spec_file) - base_path = base_path or path.dirname(spec_file_full_path) - sys.path.insert(1, path.abspath(base_path)) + py_module_path = base_module_path or path.dirname(spec_file_full_path) + sys.path.insert(1, path.abspath(py_module_path)) + logger.debug('Added {} to system path.'.format(py_module_path)) resolver_error = None if stub: From fec010b2f8f1354a732d9153d841f5ca3bfc925d Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Wed, 14 Sep 2016 23:04:35 +0200 Subject: [PATCH 14/24] Document connexion run command --- docs/cli.rst | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 51 insertions(+) create mode 100644 docs/cli.rst diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 000000000..85182a30d --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,50 @@ +Commnad-Line Interface +====================== +For convenience Connexion provides a command-line interface +(CLI). This interface aims to be a starting point in developing or +testing OpenAPI specifications with Connexion. + +The available commands are: + +- ``connexion run`` + +All commands can run with -h or --help to list more information. + +Running an OpenAPI specification +-------------------------------- + +The subcommand ``run`` of Connexion's CLI makes it easy to run OpenAPI +specifications directly even before any operation handler function gets +implemented. This allows you to verify and inspect how your API will +work with Connexion. + +To run your specification, execute in your shell: + +.. code-block:: bash + + $ connexion run your_api.yaml --stub --debug + +This command will tell Connexion to run the ``your_api.yaml`` +specification file attaching a stub operation (``--stub``) to the +unavailable operations/functions of your API and in debug mode +(``--debug``). + +The basic usage of this command is: + +.. code-block:: bash + + $ connexion run [OPTIONS] SPEC_FILE [BASE_MODULE_PATH] + +Where: + +- SPEC_FILE: Your OpenAPI specification file in YAML format. +- BASE_MODULE_PATH (optional): filesystem path where the API endpoints + handlers are going to be imported from. In short, where your Python + code is saved. + +There are more options available for the ``run`` command, for a full +list run: + +.. code-block:: bash + + $ connexion run --help diff --git a/docs/index.rst b/docs/index.rst index ae8f827c2..58ebba448 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ Contents: :maxdepth: 2 quickstart + cli routing request response From 6076f96fd30d08531549ddf0a0a90353e4744390 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 00:51:53 +0200 Subject: [PATCH 15/24] Fix typo --- docs/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli.rst b/docs/cli.rst index 85182a30d..6a2b53982 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,4 +1,4 @@ -Commnad-Line Interface +Command-Line Interface ====================== For convenience Connexion provides a command-line interface (CLI). This interface aims to be a starting point in developing or From 9b7cb993181eee96a7b71c9b2f5347e5c70bd4da Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 09:53:58 +0200 Subject: [PATCH 16/24] Check that all options are passed correctly from the CLI module --- connexion/cli.py | 4 +-- tests/test_cli.py | 82 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index f5620c3c7..065227b8f 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -95,8 +95,8 @@ def run(spec_file, resolver_error = 501 app = App(__name__, - swagger_json=hide_spec is False, - swagger_ui=hide_console_ui is False, + swagger_json=not hide_spec, + swagger_ui=not hide_console_ui, swagger_path=console_ui_from or None, swagger_url=console_ui_url or None, auth_all_paths=auth_all_paths, diff --git a/tests/test_cli.py b/tests/test_cli.py index fa6640220..e4453e54c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,8 +16,21 @@ def mock_app_run(monkeypatch): test_server.run = MagicMock(return_value=True) test_app = MagicMock(return_value=test_server) monkeypatch.setattr('connexion.cli.App', test_app) - return test_server + return test_app +@pytest.fixture() +def expected_arguments(): + """ + Default values arguments used to call `connexion.App` by cli. + """ + return { + "swagger_json": True, + "swagger_ui": True, + "swagger_path": None, + "swagger_url": None, + "auth_all_paths": False, + "debug": False + } @pytest.fixture() def spec_file(): @@ -35,13 +48,73 @@ def test_run_simple_spec(mock_app_run, spec_file): runner = CliRunner() runner.invoke(main, ['run', spec_file], catch_exceptions=False) - mock_app_run.run.assert_called_with( + app_instance = mock_app_run() + app_instance.run.assert_called_with( port=default_port, server=None, debug=False) -def test_run_in_debug_mode(mock_app_run, spec_file, monkeypatch): +def test_run_no_options_all_default(mock_app_run, expected_arguments, spec_file): + runner = CliRunner() + runner.invoke(main, ['run', spec_file], catch_exceptions=False) + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_using_option_hide_spec(mock_app_run, expected_arguments, + spec_file): + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '--hide-spec'], + catch_exceptions=False) + + expected_arguments['swagger_json'] = False + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_using_option_hide_console_ui(mock_app_run, expected_arguments, + spec_file): + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '--hide-console-ui'], + catch_exceptions=False) + + expected_arguments['swagger_ui'] = False + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_using_option_console_ui_from(mock_app_run, expected_arguments, + spec_file): + user_path = '/some/path/here' + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '--console-ui-from', user_path], + catch_exceptions=False) + + expected_arguments['swagger_path'] = user_path + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_using_option_console_ui_url(mock_app_run, expected_arguments, + spec_file): + user_url = '/console_ui_test' + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '--console-ui-url', user_url], + catch_exceptions=False) + + expected_arguments['swagger_url'] = user_url + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_using_option_auth_all_paths(mock_app_run, expected_arguments, + spec_file): + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '--auth-all-paths'], + catch_exceptions=False) + + expected_arguments['auth_all_paths'] = True + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + +def test_run_in_debug_mode(mock_app_run, expected_arguments, spec_file, + monkeypatch): logging_config = MagicMock(name='connexion.cli.logging.basicConfig') monkeypatch.setattr('connexion.cli.logging.basicConfig', logging_config) @@ -51,6 +124,9 @@ def test_run_in_debug_mode(mock_app_run, spec_file, monkeypatch): logging_config.assert_called_with(level=logging.DEBUG) + expected_arguments['debug'] = True + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + def test_run_unimplemented_operations_and_stub(mock_app_run): runner = CliRunner() From a2c074d0142f51bf8b3b0813eaba2edf5463ff8b Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:02:43 +0200 Subject: [PATCH 17/24] Make connexion executable --- connexion/__main__.py | 3 +++ connexion/cli.py | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 connexion/__main__.py diff --git a/connexion/__main__.py b/connexion/__main__.py new file mode 100644 index 000000000..8decd5575 --- /dev/null +++ b/connexion/__main__.py @@ -0,0 +1,3 @@ +from connexion.cli import main + +main() diff --git a/connexion/cli.py b/connexion/cli.py index 065227b8f..2646757a4 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -110,3 +110,7 @@ def run(spec_file, app.run(port=port, server=wsgi_server, debug=debug) + + +if __name__ == '__main__': + main() From b3fc3fffa379b6b5207fff6294462d9940b83389 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:09:15 +0200 Subject: [PATCH 18/24] Very simple code to run module, no tests :( --- connexion/__main__.py | 2 +- connexion/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connexion/__main__.py b/connexion/__main__.py index 8decd5575..e29888b7a 100644 --- a/connexion/__main__.py +++ b/connexion/__main__.py @@ -1,3 +1,3 @@ from connexion.cli import main -main() +main() # noqa diff --git a/connexion/cli.py b/connexion/cli.py index 2646757a4..cf379478b 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -112,5 +112,5 @@ def run(spec_file, debug=debug) -if __name__ == '__main__': +if __name__ == '__main__': # noqa main() From b57ebfacd509378d6d374bc57779a1c9e553bd25 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:12:47 +0200 Subject: [PATCH 19/24] No coverage for those two lines --- connexion/__main__.py | 4 +--- connexion/cli.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/connexion/__main__.py b/connexion/__main__.py index e29888b7a..1305ea2c7 100644 --- a/connexion/__main__.py +++ b/connexion/__main__.py @@ -1,3 +1 @@ -from connexion.cli import main - -main() # noqa +from connexion.cli import main; main() # pragma: no cover diff --git a/connexion/cli.py b/connexion/cli.py index cf379478b..92a4b5149 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -112,5 +112,5 @@ def run(spec_file, debug=debug) -if __name__ == '__main__': # noqa +if __name__ == '__main__': # pragma: no cover main() From 46fa0197eacecdc06e07e5871e2564eb5559d075 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:16:22 +0200 Subject: [PATCH 20/24] Use INFO log level by default --- connexion/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connexion/cli.py b/connexion/cli.py index 92a4b5149..7fde2afb4 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -80,7 +80,7 @@ def run(spec_file, - BASE_MODULE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. """ - logging_level = logging.ERROR + logging_level = logging.INFO if debug: logging_level = logging.DEBUG logging.basicConfig(level=logging_level) From c1d55c1116b81892b971a031be888242ecb38f38 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:25:22 +0200 Subject: [PATCH 21/24] Support different log levels in CLI --- connexion/cli.py | 16 +++++++++++++--- tests/test_cli.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 7fde2afb4..87187bc28 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -58,6 +58,10 @@ def validate_wsgi_server_requirements(ctx, param, value): is_flag=True, default=False) @click.option('--debug', '-d', help='Show debugging information.', is_flag=True, default=False) +@click.option('--verbose', '-v', help='Show logging information.', + is_flag=True, default=False) +@click.option('--very-verbose', '-vv', help='Show debugging information, same as `--debug`.', + is_flag=True, default=False) def run(spec_file, base_module_path, port, @@ -70,7 +74,9 @@ def run(spec_file, auth_all_paths, validate_responses, strict_validation, - debug): + debug, + verbose, + very_verbose): """ Runs a server compliant with a OpenAPI/Swagger 2.0 Specification file. @@ -80,9 +86,13 @@ def run(spec_file, - BASE_MODULE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. """ - logging_level = logging.INFO - if debug: + logging_level = logging.WARN + if verbose: + logging_level = logging.INFO + + if debug or very_verbose: logging_level = logging.DEBUG + logging.basicConfig(level=logging_level) spec_file_full_path = path.abspath(spec_file) diff --git a/tests/test_cli.py b/tests/test_cli.py index e4453e54c..18fc903a6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -128,6 +128,22 @@ def test_run_in_debug_mode(mock_app_run, expected_arguments, spec_file, mock_app_run.assert_called_with('connexion.cli', **expected_arguments) +def test_run_in_verbose_mode(mock_app_run, expected_arguments, spec_file, + monkeypatch): + logging_config = MagicMock(name='connexion.cli.logging.basicConfig') + monkeypatch.setattr('connexion.cli.logging.basicConfig', + logging_config) + + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '-v'], catch_exceptions=False) + + logging_config.assert_called_with(level=logging.INFO) + + expected_arguments['debug'] = False + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + + def test_run_unimplemented_operations_and_stub(mock_app_run): runner = CliRunner() From d71674b35b2bd87a88de2d6ba548fdfcc2bf0364 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:29:00 +0200 Subject: [PATCH 22/24] Make sure very verbose is the same as debug --- connexion/cli.py | 1 + tests/test_cli.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/connexion/cli.py b/connexion/cli.py index 87187bc28..181f29396 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -92,6 +92,7 @@ def run(spec_file, if debug or very_verbose: logging_level = logging.DEBUG + debug = True logging.basicConfig(level=logging_level) diff --git a/tests/test_cli.py b/tests/test_cli.py index 18fc903a6..46074c457 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -128,6 +128,21 @@ def test_run_in_debug_mode(mock_app_run, expected_arguments, spec_file, mock_app_run.assert_called_with('connexion.cli', **expected_arguments) +def test_run_in_very_verbose_mode(mock_app_run, expected_arguments, spec_file, + monkeypatch): + logging_config = MagicMock(name='connexion.cli.logging.basicConfig') + monkeypatch.setattr('connexion.cli.logging.basicConfig', + logging_config) + + runner = CliRunner() + runner.invoke(main, ['run', spec_file, '-vv'], catch_exceptions=False) + + logging_config.assert_called_with(level=logging.DEBUG) + + expected_arguments['debug'] = True + mock_app_run.assert_called_with('connexion.cli', **expected_arguments) + + def test_run_in_verbose_mode(mock_app_run, expected_arguments, spec_file, monkeypatch): logging_config = MagicMock(name='connexion.cli.logging.basicConfig') From 08b20e5fbe3465a990768238264838efa01bb5f5 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 10:37:46 +0200 Subject: [PATCH 23/24] Fix flake8 checks --- connexion/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/connexion/__main__.py b/connexion/__main__.py index 1305ea2c7..9a4a5439f 100644 --- a/connexion/__main__.py +++ b/connexion/__main__.py @@ -1 +1,3 @@ -from connexion.cli import main; main() # pragma: no cover +from connexion.cli import main # pragma: no cover + +main() # pragma: no cover From 2d23ac949a4798ecbbce716c6dabbfaf2ef67cee Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Thu, 15 Sep 2016 13:18:23 +0200 Subject: [PATCH 24/24] Nicer verbosity impl. --- connexion/cli.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/connexion/cli.py b/connexion/cli.py index 181f29396..e294a8433 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -58,10 +58,7 @@ def validate_wsgi_server_requirements(ctx, param, value): is_flag=True, default=False) @click.option('--debug', '-d', help='Show debugging information.', is_flag=True, default=False) -@click.option('--verbose', '-v', help='Show logging information.', - is_flag=True, default=False) -@click.option('--very-verbose', '-vv', help='Show debugging information, same as `--debug`.', - is_flag=True, default=False) +@click.option('--verbose', '-v', help='Show verbose information.', count=True) def run(spec_file, base_module_path, port, @@ -75,8 +72,7 @@ def run(spec_file, validate_responses, strict_validation, debug, - verbose, - very_verbose): + verbose): """ Runs a server compliant with a OpenAPI/Swagger 2.0 Specification file. @@ -87,10 +83,10 @@ def run(spec_file, - BASE_MODULE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from. """ logging_level = logging.WARN - if verbose: + if verbose > 0: logging_level = logging.INFO - if debug or very_verbose: + if debug or verbose > 1: logging_level = logging.DEBUG debug = True