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

Make api docs route optional #82

Merged
merged 6 commits into from
Feb 26, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ A few relevant settings for your `Pyramid .ini file <http://docs.pylonsproject.o
# Default: None
pyramid_swagger.validation_context_path = 'path.to.user.defined.contextmanager'

# Enable/disable automatic /api-doc endpoints to serve the swagger
# schemas (true by default)
pyramid_swagger.enable_api_doc_views = true

Note that, equivalently, you can add these during webapp configuration:

.. code-block:: python
Expand Down
21 changes: 19 additions & 2 deletions pyramid_swagger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@
Import this module to add the validation tween to your pyramid app.
"""
import pyramid
from .api import register_swagger_endpoints
from .api import register_api_doc_endpoints
from .ingest import (
compile_swagger_schema,
validate_swagger_schema,
)


def includeme(config):
settings = config.registry.settings
schema_dir = settings.get('pyramid_swagger.schema_directory', 'api_docs/')

if settings.get('pyramid_swagger.enable_swagger_spec_validation', True):
validate_swagger_schema(schema_dir)

# Add the SwaggerSchema to settings to make it avialable to the validation
# tween and `register_api_doc_endpoints`
if 'pyramid_swagger.schema' not in settings:
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's put a comment explaining the purpose of stuffing this in here.

settings['pyramid_swagger.schema'] = compile_swagger_schema(schema_dir)

config.add_tween(
"pyramid_swagger.tween.validation_tween_factory",
under=pyramid.tweens.EXCVIEW
)
register_swagger_endpoints(config)

if settings.get('pyramid_swagger.enable_api_doc_views', True):
register_api_doc_endpoints(config)
14 changes: 4 additions & 10 deletions pyramid_swagger/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,10 @@
"""
import simplejson

from .ingest import compile_swagger_schema
from .tween import load_settings


def register_swagger_endpoints(config):
def register_api_doc_endpoints(config):
"""Create and register pyramid endpoints for /api-docs*."""
settings = load_settings(config.registry)
swagger_schema = compile_swagger_schema(
settings.schema_dir,
settings.validate_swagger_spec,
)
swagger_schema = config.registry.settings['pyramid_swagger.schema']
with open(swagger_schema.resource_listing) as input_file:
register_resource_listing(config, simplejson.load(input_file))

Expand Down Expand Up @@ -73,7 +66,8 @@ def register_api_declaration(config, resource_name, api_declaration):

def build_api_declaration_view(api_declaration_json):
"""Thanks to the magic of closures, this means we gracefully return JSON
without file IO at request time."""
without file IO at request time.
"""
def view_for_api_declaration(request):
# Note that we rewrite basePath to always point at this server's root.
return dict(
Expand Down
86 changes: 33 additions & 53 deletions pyramid_swagger/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,93 +21,80 @@ class ApiDeclarationNotFoundError(Exception):


def find_resource_names(api_docs_json):
return [
api['path'].lstrip('/')
for api in api_docs_json['apis']
]
return [api['path'].lstrip('/') for api in api_docs_json['apis']]


def build_schema_mapping(schema_dir):
def build_schema_mapping(schema_dir, listing_json):
"""Discovers schema file locations and relations.

:param schema_dir: the directory schema files live inside
:type schema_dir: string
:returns: A tuple of (resource listing filepath, mapping) where the mapping
is between resource name and file path
:rtype: (string, dict)
:param listing_json: the contents of the listing document
:type listing_json: string
:returns: a mapping from resource name to file path
:rtype: dict
"""
def resource_name_to_filepath(name):
return os.path.join(schema_dir, '{0}.json'.format(name))

listing, listing_json = _load_resource_listing(schema_dir)

return (
listing,
dict(
(resource, resource_name_to_filepath(resource))
for resource in find_resource_names(listing_json)
)
return dict(
(resource, resource_name_to_filepath(resource))
for resource in find_resource_names(listing_json)
)


def _load_resource_listing(schema_dir):
def _load_resource_listing(resource_listing):
"""Load the resource listing from file, handling errors.

:param schema_dir: the directory schema files live inside
:type schema_dir: string
:returns: (resource listing filepath, resource listing json)
:param resource_listing: path to the api-docs resource listing file
:type resource_listing: string
:returns: contents of the resource listing file
:rtype: dict
"""
resource_listing = os.path.join(schema_dir, API_DOCS_FILENAME)
try:
with open(resource_listing) as resource_listing_file:
resource_listing_json = simplejson.load(resource_listing_file)
return simplejson.load(resource_listing_file)
# If not found, raise a more user-friendly error.
except IOError:
raise ResourceListingNotFoundError(
'No resource listing found at {0}. Note that your json file '
'must be named {1}'.format(resource_listing, API_DOCS_FILENAME)
)
return resource_listing, resource_listing_json


def compile_swagger_schema(schema_dir, should_validate_schemas):
def compile_swagger_schema(schema_dir):
"""Build a SwaggerSchema from various files.

:param schema_dir: the directory schema files live inside
:type schema_dir: string
:param should_validate_schemas: if True, check schemas for correctness
:type should_validate_schemas: boolean
:returns: a SwaggerSchema object
"""
listing, mapping = build_schema_mapping(schema_dir)
schema_resolvers = ingest_resources(
listing,
mapping,
schema_dir,
should_validate_schemas,
)
return SwaggerSchema(
listing,
mapping,
schema_resolvers,
)
listing_filename = os.path.join(schema_dir, API_DOCS_FILENAME)
listing_json = _load_resource_listing(listing_filename)
mapping = build_schema_mapping(schema_dir, listing_json)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to call build_schema_mapping again in line 84? I was thinking to instead call the validate_swagger_schema from inside the build_schema_mapping and pass the mapping to validate_swagger_schema during the call. This way validation will happen before ingest_resources and with a single call to build_schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason I don't like that as much is because it makes the validation kind of implicit instead of being it's own call it's just a side-effect of building a schema.

Does ssv need build_schema_mapping() ? I was thinking that it would work off just listing_filename, which is why I left the duplicate lines.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yeah, i missed that. ssv would just need the listing_filename for its purpose.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to not exposing the schema mapping data structure to the outside world. I've explicitly avoided making it public so we have refactoring flexibility going forward. In the event pyramid_swagger is ever 2.0-only, I bet we could (and would want to) do a major refactoring of the data structure itself.

schema_resolvers = ingest_resources(mapping, schema_dir)
return SwaggerSchema(listing_filename, mapping, schema_resolvers)


def ingest_resources(listing, mapping, schema_dir, should_validate_schemas):
def validate_swagger_schema(schema_dir):
"""Add the swagger_schema to the registry.settings """
listing_filename = os.path.join(schema_dir, API_DOCS_FILENAME)
# TODO: this will be replaced by ssv shortly
listing_json = _load_resource_listing(listing_filename)
mapping = build_schema_mapping(schema_dir, listing_json)
validate_swagger_schemas(listing_filename, mapping.values())


def ingest_resources(mapping, schema_dir):
"""Consume the Swagger schemas and produce a queryable datastructure.

:param listing: Filepath to a resource listing
:type listing: string
:param mapping: Map from resource name to filepath of its api declaration
:type mapping: dict
:param schema_dir: the directory schema files live inside
:type schema_dir: string
:param should_validate_schemas: if True, check schemas for correctness
:type should_validate_schemas: boolean
:returns: A list of SchemaAndResolver objects
:returns: A list of :class:`pyramid_swagger.load_schema.SchemaAndResolver`
objects
"""
resource_filepaths = mapping.values()

ingested_resources = []
for name, filepath in mapping.items():
try:
Expand All @@ -121,11 +108,4 @@ def ingest_resources(listing, mapping, schema_dir, should_validate_schemas):
'your resource name and API declaration file do not '
'match?'.format(filepath, name, schema_dir)
)

if should_validate_schemas:
validate_swagger_schemas(
listing,
resource_filepaths
)

return ingested_resources
3 changes: 0 additions & 3 deletions pyramid_swagger/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ def validate_resource_listing(resource_listing_json):

def validate_api_declaration(api_declaration_json):
"""Validate a swagger schema.

:param schema_dir: the directory schema files live inside
:type schema_dir: string
"""
api_spec_path = resource_filename(
'pyramid_swagger',
Expand Down
26 changes: 7 additions & 19 deletions pyramid_swagger/tween.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from jsonschema.validators import Draft3Validator, Draft4Validator
from pyramid_swagger.exceptions import RequestValidationError
from pyramid_swagger.exceptions import ResponseValidationError
from .ingest import compile_swagger_schema
from .model import PathNotMatchedError


Expand All @@ -29,8 +28,7 @@
class Settings(namedtuple(
'Settings',
[
'schema_dir',
'validate_swagger_spec',
'schema',
'validate_request',
'validate_response',
'validate_path',
Expand All @@ -40,7 +38,7 @@ class Settings(namedtuple(

"""A settings object for configuratble options.

:param schema_dir: location of Swagger schema files.
:param schema: a :class:`pyramid_swagger.model.SwaggerSchema`
:param validate_swagger_spec: check Swagger files for correctness.
:param validate_request: check requests against Swagger spec.
:param validate_response: check responses against Swagger spec.
Expand Down Expand Up @@ -85,10 +83,6 @@ def validation_tween_factory(handler, registry):
while delegating to the relevant matching view.
"""
settings = load_settings(registry)
schema = compile_swagger_schema(
settings.schema_dir,
settings.validate_swagger_spec
)
route_mapper = registry.queryUtility(IRoutesMapper)
disable_all_validation = not any((
settings.validate_request,
Expand All @@ -104,8 +98,10 @@ def validator_tween(request):
validation_context = _get_validation_context(registry)

try:
schema_data, resolver = schema.schema_and_resolver_for_request(
request)
(
schema_data,
resolver
) = settings.schema.schema_and_resolver_for_request(request)
except PathNotMatchedError as exc:
if settings.validate_path:
with validation_context(request):
Expand Down Expand Up @@ -139,15 +135,7 @@ def validator_tween(request):

def load_settings(registry):
return Settings(
# By default, assume cwd contains the swagger schemas.
schema_dir=registry.settings.get(
'pyramid_swagger.schema_directory',
'api_docs/'
),
validate_swagger_spec=registry.settings.get(
'pyramid_swagger.enable_swagger_spec_validation',
True
),
schema=registry.settings['pyramid_swagger.schema'],
validate_request=registry.settings.get(
'pyramid_swagger.enable_request_validation',
True
Expand Down
14 changes: 8 additions & 6 deletions tests/acceptance/request_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,14 @@ def test_200_skip_validation_when_disabled():


def test_path_validation_context():
assert test_app(**{'pyramid_swagger.validation_context_path': validation_ctx_path}) \
.get('/does_not_exist') \
.status_code == 206
app = test_app(
**{'pyramid_swagger.validation_context_path': validation_ctx_path}
)
assert app.get('/does_not_exist').status_code == 206


def test_request_validation_context():
assert test_app(**{'pyramid_swagger.validation_context_path': validation_ctx_path}) \
.get('/get_with_non_string_query_args', params={}) \
.status_code == 206
app = test_app(
**{'pyramid_swagger.validation_context_path': validation_ctx_path})
response = app.get('/get_with_non_string_query_args', params={})
assert response.status_code == 206
4 changes: 3 additions & 1 deletion tests/acceptance/response_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pyramid.registry import Registry
from pyramid.response import Response
from pyramid_swagger.exceptions import ResponseValidationError
from pyramid_swagger.ingest import compile_swagger_schema
from pyramid_swagger.tween import validation_tween_factory
from webtest import AppError

Expand Down Expand Up @@ -49,7 +50,8 @@ def handler(request):
return response or Response()

settings = dict({
'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
'pyramid_swagger.schema': compile_swagger_schema(
'tests/sample_schemas/good_app/'),
'pyramid_swagger.enable_swagger_spec_validation': False},
**overrides
)
Expand Down
33 changes: 0 additions & 33 deletions tests/acceptance/schema_validation_test.py

This file was deleted.