Skip to content
This repository has been archived by the owner on Mar 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #105 from lendingblock/master
Browse files Browse the repository at this point in the history
0.8.0
  • Loading branch information
lsbardel authored Aug 17, 2018
2 parents 17f7fb6 + 3f527d0 commit 5029b21
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 81 deletions.
2 changes: 1 addition & 1 deletion openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Minimal OpenAPI asynchronous server application
"""

__version__ = '0.7.7'
__version__ = '0.8.0'
22 changes: 18 additions & 4 deletions openapi/rest.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
from dataclasses import dataclass
import typing

from .data.fields import Choice, IntegerValidator
from .cli import OpenApiClient
from .data.fields import data_field, bool_field
from .spec import OpenApi
from .spec import OpenApi, OpenApiSpec
from .spec.utils import docjoin
from .spec.pagination import MAX_PAGINATION_LIMIT


def rest(setup_app=None, base_path=None, commands=None, **kwargs):
def rest(
openapi: dict=None,
setup_app: object=None,
base_path: str=None,
commands: typing.List=None,
allowed_tags: typing.Set=None,
validate_docs: bool=False
):
"""Create the OpenApi application server
"""
spec = OpenApi(**kwargs)
return OpenApiClient(
spec, base_path=base_path, commands=commands, setup_app=setup_app
OpenApiSpec(
OpenApi(**(openapi or {})),
allowed_tags=allowed_tags,
validate_docs=validate_docs
),
base_path=base_path,
commands=commands,
setup_app=setup_app
)


Expand Down
97 changes: 61 additions & 36 deletions openapi/spec/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from datetime import datetime, date
from decimal import Decimal
from enum import Enum
from typing import List, Dict
from typing import List, Dict, Iterable
from dataclasses import dataclass, asdict, is_dataclass, field

from aiohttp import hdrs
from aiohttp import web
from dataclasses import dataclass, asdict, is_dataclass, field

from .exceptions import InvalidTypeException, InvalidSpecException
from .path import ApiPath
Expand Down Expand Up @@ -59,8 +59,9 @@ class SchemaParser:
Decimal: {'type': 'number'}
}

def __init__(self, group=None):
def __init__(self, group=None, validate_docs=False):
self.group = group or SchemaGroup()
self.validate_docs = validate_docs

def parameters(self, Schema, default_in='path'):
params = []
Expand All @@ -77,7 +78,7 @@ def parameters(self, Schema, default_in='path'):
params.append(entry)
return params

def field2json(self, field, validate_info=True):
def field2json(self, field, validate_docs=True):
field = fields.as_field(field)
mapping = self._fields_mapping.get(field.type, None)
if not mapping:
Expand All @@ -100,9 +101,9 @@ def field2json(self, field, validate_info=True):
meta = field.metadata
field_description = meta.get(fields.DESCRIPTION)
if not field_description:
if validate_info:
if validate_docs and self.validate_docs:
raise InvalidSpecException(
f'Missing description for field {field.name}'
f'Missing description for field "{field.name}"'
)
else:
json_property['description'] = field_description
Expand Down Expand Up @@ -167,21 +168,28 @@ class SchemaGroup:
def __init__(self):
self.parsed_schemas = {}

def parse(self, schemas):
def parse(self, schemas, validate_docs=False):
for schema in set(schemas):
if schema.__name__ in self.parsed_schemas:
continue

parsed_schema = SchemaParser(self).schema2json(schema)
parsed_schema = SchemaParser(
self, validate_docs=validate_docs
).schema2json(schema)
self.parsed_schemas[schema.__name__] = parsed_schema
return self.parsed_schemas


class OpenApiSpec:
"""Open API document builder
"""
def __init__(self, info, default_content_type=None,
default_responses=None, allowed_tags=None):
def __init__(
self,
info: OpenApi=None,
default_content_type: str=None,
default_responses: Iterable=None,
allowed_tags: Iterable=None,
validate_docs: bool=False):
self.schemas = {}
self.parameters = {}
self.responses = {}
Expand All @@ -192,16 +200,25 @@ def __init__(self, info, default_content_type=None,
self.default_responses = default_responses or {}
self.doc = dict(
openapi=OPENAPI,
info=info,
info=asdict(info or OpenApi()),
paths=OrderedDict()
)
self.schemas_to_parse = set()
self.allowed_tags = allowed_tags
self.validate_docs = validate_docs

@property
def paths(self):
return self.doc['paths']

@property
def title(self):
return self.doc['info']['title']

@property
def version(self):
return self.doc['info']['version']

def build(self, app, public=True, private=False):
"""Build the ``doc`` dictionary by adding paths
"""
Expand Down Expand Up @@ -230,7 +247,7 @@ def build(self, app, public=True, private=False):
),
servers=self.servers
))
return self
return doc

def _build_paths(self, app, public, private):
"""Loop through app paths and add
Expand All @@ -244,30 +261,37 @@ def _build_paths(self, app, public, private):
handler = route.handler
if (issubclass(handler, ApiPath) and
self._include(handler.private, public, private)):
paths[path] = self._build_path_object(
handler, app, public, private
)

self._validate_tags()
try:
paths[path] = self._build_path_object(
handler, app, public, private
)
except InvalidSpecException as exc:
raise InvalidSpecException(
f'Invalid spec in route "{path}": {exc}'
) from None

if self.validate_docs:
self._validate_tags()

def _validate_tags(self):
for tag_name, tag_obj in self.tags.items():
if self.allowed_tags and tag_name not in self.allowed_tags:
raise InvalidSpecException(f'Tag {tag_name} not allowed')
raise InvalidSpecException(f'Tag "{tag_name}" not allowed')
if 'description' not in tag_obj:
raise InvalidSpecException(
f'Missing tag {tag_name} description'
f'Missing tag "{tag_name}" description'
)

def _build_path_object(self, handler, path_obj, public, private):
path_obj = load_yaml_from_docstring(handler.__doc__) or {}
doc_tags = path_obj.pop('tags', None)
if not doc_tags:
raise InvalidSpecException(f'Missing tags docstring for {handler}')
if not doc_tags and self.validate_docs:
raise InvalidSpecException(
f'Missing tags docstring for "{handler}"')

tags = self._extend_tags(doc_tags)
if handler.path_schema:
p = SchemaParser()
p = SchemaParser(validate_docs=self.validate_docs)
path_obj['parameters'] = p.parameters(handler.path_schema)
for method in METHODS:
method_handler = getattr(handler, method, None)
Expand Down Expand Up @@ -311,16 +335,17 @@ def _get_schema_info(self, schema):
return info

def _get_method_info(self, method_handler, method_doc):
summary = method_doc.get('summary')
if not summary:
raise InvalidSpecException(
f'Missing method summary for {method_handler}'
)
description = method_doc.get('description')
if not description:
raise InvalidSpecException(
f'Missing method description for {method_handler}'
)
summary = method_doc.get('summary', '')
description = method_doc.get('description', '')
if self.validate_docs:
if not summary:
raise InvalidSpecException(
f'Missing method summary for "{method_handler}"'
)
if not description:
raise InvalidSpecException(
f'Missing method description for "{method_handler}"'
)
return {'summary': summary, 'description': description}

def _get_response_object(self, op_attrs, doc):
Expand Down Expand Up @@ -359,7 +384,8 @@ def _get_request_body_object(self, op_attrs, doc):
def _get_query_parameters(self, op_attrs, doc):
schema = op_attrs.get('query_schema', None)
if schema:
doc['parameters'] = SchemaParser().parameters(schema, 'query')
doc['parameters'] = SchemaParser(
validate_docs=self.validate_docs).parameters(schema, 'query')

def _add_schemas_from_operation(self, operation_obj):
for schema in SCHEMAS_TO_SCHEMA:
Expand Down Expand Up @@ -396,6 +422,5 @@ async def spec_root(request):
app = request.app
spec = app.get('spec_doc')
if not spec:
spec = OpenApiSpec(asdict(app['spec']))
app['spec_doc'] = spec.build(app)
return web.json_response(spec.doc)
app['spec_doc'] = app['spec'].build(app)
return web.json_response(app['spec_doc'])
22 changes: 9 additions & 13 deletions tests/spec/test_schema_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ class MyClass:

def test_field2json():
parser = SchemaParser([])
str_json = parser.field2json(str, validate_info=False)
int_json = parser.field2json(int, validate_info=False)
float_json = parser.field2json(float, validate_info=False)
bool_json = parser.field2json(bool, validate_info=False)
datetime_json = parser.field2json(datetime, validate_info=False)
str_json = parser.field2json(str)
int_json = parser.field2json(int)
float_json = parser.field2json(float)
bool_json = parser.field2json(bool)
datetime_json = parser.field2json(datetime)

assert str_json == {'type': 'string'}
assert int_json == {'type': 'integer', 'format': 'int32'}
Expand All @@ -123,12 +123,8 @@ def test_field2json():

def test_field2json_format():
parser = SchemaParser([])
str_json = parser.field2json(
as_field(str, format='uuid'), validate_info=False
)
int_json = parser.field2json(
as_field(int, format='int64'), validate_info=False
)
str_json = parser.field2json(as_field(str, format='uuid'))
int_json = parser.field2json(as_field(int, format='int64'))

assert str_json == {'type': 'string', 'format': 'uuid'}
assert int_json == {'type': 'integer', 'format': 'int64'}
Expand All @@ -149,7 +145,7 @@ class MyClass:
desc_field: str = data_field(description='Valid field')
no_desc_field: str = data_field()

parser = SchemaParser()
parser = SchemaParser(validate_docs=True)
with pytest.raises(InvalidSpecException):
parser.get_schema_ref(MyClass)

Expand All @@ -161,7 +157,7 @@ class MyEnum(Enum):
FIELD_3 = 2

parser = SchemaParser([])
json_type = parser.field2json(MyEnum, validate_info=False)
json_type = parser.field2json(MyEnum)
assert json_type == {
'type': 'string', 'enum': ['FIELD_1', 'FIELD_2', 'FIELD_3']
}
Expand Down
Loading

0 comments on commit 5029b21

Please sign in to comment.