Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run an API spec, no boilerplate needed #284

Merged
merged 25 commits into from
Sep 15, 2016
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c74d8cd
Provide CLI support for runnning specifications
rafaelcaricio Sep 12, 2016
e9d5a61
Run with specification stubs
rafaelcaricio Sep 12, 2016
9c5f6bc
Refine stub option
rafaelcaricio Sep 12, 2016
82dcb61
Pass along all options available
rafaelcaricio Sep 12, 2016
e7f96e4
Sort imports
rafaelcaricio Sep 12, 2016
97adb25
Sort imports
rafaelcaricio Sep 12, 2016
8ef5794
Merge master into cli branch
rafaelcaricio Sep 13, 2016
d9b59c6
Fix imports
rafaelcaricio Sep 13, 2016
7ecb585
Fix imports
rafaelcaricio Sep 13, 2016
186f33b
Use clickclick>=1.2 with support to Python 2.7
rafaelcaricio Sep 14, 2016
dbe20e9
Remove cnx alias to CLI
rafaelcaricio Sep 14, 2016
07c6eb7
Fix parameters in the CLI interface
rafaelcaricio Sep 14, 2016
2208a95
Directories to be considered modules in Python2.7 needs to contain a …
rafaelcaricio Sep 14, 2016
b024a90
Add basic logging to CLI for troubleshooting
rafaelcaricio Sep 14, 2016
fec010b
Document connexion run command
rafaelcaricio Sep 14, 2016
6076f96
Fix typo
rafaelcaricio Sep 14, 2016
9b7cb99
Check that all options are passed correctly from the CLI module
rafaelcaricio Sep 15, 2016
a2c074d
Make connexion executable
rafaelcaricio Sep 15, 2016
b3fc3ff
Very simple code to run module, no tests :(
rafaelcaricio Sep 15, 2016
b57ebfa
No coverage for those two lines
rafaelcaricio Sep 15, 2016
46fa019
Use INFO log level by default
rafaelcaricio Sep 15, 2016
c1d55c1
Support different log levels in CLI
rafaelcaricio Sep 15, 2016
d71674b
Make sure very verbose is the same as debug
rafaelcaricio Sep 15, 2016
08b20e5
Fix flake8 checks
rafaelcaricio Sep 15, 2016
2d23ac9
Nicer verbosity impl.
rafaelcaricio Sep 15, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions connexion/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from connexion.cli import main # pragma: no cover

main() # pragma: no cover
20 changes: 4 additions & 16 deletions connexion/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
"""
Copyright 2015 Zalando SE

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 copy
import logging
import pathlib
Expand All @@ -23,10 +10,11 @@
import yaml
from swagger_spec_validator.validator20 import validate_spec

from . import resolver, utils
from . import utils
from .exceptions import ResolverError
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'
Expand Down Expand Up @@ -65,7 +53,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, resolver_error_handler=None):
"""
:type swagger_yaml_path: pathlib.Path
Expand Down Expand Up @@ -136,7 +124,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
Expand Down
13 changes: 0 additions & 13 deletions connexion/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
"""
Copyright 2015 Zalando SE

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
"""

import logging
import pathlib

Expand Down
127 changes: 127 additions & 0 deletions connexion/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import logging
import sys
from os import path

import click
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']))


def validate_wsgi_server_requirements(ctx, param, value):
if value == 'gevent':
try:
import gevent # NOQA
except:
fatal_error('gevent library is not installed')
elif value == 'tornado':
try:
import tornado # NOQA
except:
fatal_error('tornado library is not installed')


@main.command()
@click.argument('spec_file')
@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']),
callback=validate_wsgi_server_requirements,
help='Which WSGI server container to use.')
@click.option('--stub',
help='Returns status code 501, 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=False)
@click.option('--hide-console-ui',
help='Hides the the API console UI which is by default available at `/ui`.',
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',
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('--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)
@click.option('--verbose', '-v', help='Show logging information.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
wsgi_server,
stub,
hide_spec,
hide_console_ui,
console_ui_url,
console_ui_from,
auth_all_paths,
validate_responses,
strict_validation,
debug,
verbose,
very_verbose):
"""
Runs a server compliant with a OpenAPI/Swagger 2.0 Specification file.

Arguments:

- SPEC_FILE: specification file that describes the server endpoints.

- BASE_MODULE_PATH (optional): filesystem path where the API endpoints handlers are going to be imported from.
"""
logging_level = logging.WARN
if verbose:
logging_level = logging.INFO

if debug or very_verbose:
logging_level = logging.DEBUG
debug = True

logging.basicConfig(level=logging_level)

spec_file_full_path = path.abspath(spec_file)
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a future improvement we could implement a resolver that mocks a response based on the specification.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitely interesting. There are some open source tools that do that, but bringing together the possibility to have partially mocked API's, that can morph in a fully implemented one, is a new approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm looking to do is use my resolving swagger parser prance to generate test cases for an API. That's coming from realizing that swagger-tester isn't quite what I'd like it to be.

Some of that code might well be useful for generating the mocked response. One way or another it's going through the spec and generating stuff.

Just a thought. But I can't guarantee I'll be able to spend time on that.

resolver_error = 501

app = App(__name__,
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,
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)


if __name__ == '__main__': # pragma: no cover
main()
13 changes: 0 additions & 13 deletions connexion/resolver.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
"""
Copyright 2015 Zalando SE

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
"""

import logging
import re

Expand Down
1 change: 0 additions & 1 deletion connexion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import flask
import werkzeug.wrappers


PATH_PARAMETER = re.compile(r'\{([^}]*)\}')

# map Swagger type to flask path converter
Expand Down
50 changes: 50 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Command-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
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Contents:
:maxdepth: 2

quickstart
cli
routing
request
response
Expand Down
1 change: 1 addition & 0 deletions examples/sqlalchemy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import connexion
from connexion import NoContent

import orm

db_session = None
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ requests>=2.9.1
six>=1.7
strict-rfc3339>=0.6
swagger_spec_validator>=2.0.2
clickclick>=1.2
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +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']}
)
Empty file added tests/api/__init__.py
Empty file.
Empty file added tests/decorators/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions tests/fixtures/missing_implementation/swagger.yaml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/fixtures/module_does_not_exist/swagger.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading