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

Apispec 2.0 #1

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 20 additions & 17 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
[run]
branch = True
omit =
aiohug_swagger/tests/*
aiohug_swagger/*/tests/*

[report]
skip_covered = True
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
[run]
branch = True
omit =
aiohug_swagger/tests/*
aiohug_swagger/*/tests/*

[report]
skip_covered = True
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:

[html]
directory = .reports/coverage
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ __pychache__
/*.egg-info

.reports

.tox/
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ language: python
python:
- '3.7'
install:
- pip install -r requirements-test.txt
- pip install coveralls
- pip install -r requirements-test.txt
- pip install coveralls
script:
- make ci_test
- make venv
- make ci_test
after_success: coveralls
deploy:
provider: pypi
Expand Down
17 changes: 12 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ PYTHON_VERSION = 3.6
REQUIREMENTS = requirements.txt
REQUIREMENTS_TEST = requirements-test.txt
VIRTUAL_ENV ?= .venv
PYTHON ?= $(VIRTUAL_ENV)/bin/python
PYTHON = $(VIRTUAL_ENV)/bin/python
PIP_CONF = pip.conf
PYPI = pypi
TEST_SETTINGS = settings_test

ci_test:
pip install -r $(REQUIREMENTS_TEST)
pytest --cov-report html:.reports/coverage --cov-config .coveragerc --cov $(PROJECT)
make tox

test: venv
test:
$(VIRTUAL_ENV)/bin/py.test

test_coverage: venv
test_coverage:
$(VIRTUAL_ENV)/bin/py.test --cov-report html:.reports/coverage --cov-config .coveragerc --cov $(PROJECT)

tox:
pip install tox
$(VIRTUAL_ENV)/bin/tox

venv_init:
pip install virtualenv
if [ ! -d $(VIRTUAL_ENV) ]; then \
Expand All @@ -34,5 +38,8 @@ clean_venv:
clean_pyc:
find . -name \*.pyc -delete

clean: clean_venv clean_pyc
clean_tox:
rm -rf .tox

clean: clean_venv clean_pyc clean_tox

155 changes: 13 additions & 142 deletions aiohug_swagger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,142 +1,13 @@
import importlib
import logging
from inspect import getfullargspec

from aiohttp import web
from apispec import APISpec
from apispec.ext.marshmallow import OpenAPIConverter

from marshmallow import fields, Schema
from marshmallow.schema import SchemaMeta

from aiohug.arguments import get_default_args
from aiohug.directives import get_available_directives
from .decorators import response, spec, ensure_swagger_attr

logger = logging.getLogger(__name__)

converter = OpenAPIConverter("2.1")

DEFAULT_HOST = "localhost:8080"
DEFAULT_SCHEMES = ["http"]
DEFAULT_VERSION = None
DEFAULT_TITLE = "Swagger Application"
DEFAULT_DEFINITIONS_PATH = None
DEFAULT_TESTING_MODE = False
DEFAULT_USE_DEFAULT_RESPONSE = True
DEFAULT_DESCRIPTION = None
DEFAULT_CONTACT_EMAIL = None

PARAMETER_IN_PATH = "path"
PARAMETER_IN_QUERY = "query"


def get_summary(doc):
if doc is not None:
return doc.split("\n")[0]


def where_is_parameter(name, url):
return PARAMETER_IN_PATH if "{%s}" % name in url else PARAMETER_IN_QUERY


def get_parameters(url, handler, spec):
defaults = get_default_args(handler._original_handler)
args_spec = getfullargspec(handler._original_handler)
parameters = []
for name in args_spec.args:
kind = args_spec.annotations.get(name, fields.Field())
if name in get_available_directives() or name == "request":
continue

if isinstance(kind, fields.Field):
parameter_place = where_is_parameter(name, url)
kind.metadata = {"location": where_is_parameter(name, url)}
kind.required = name not in defaults
parameter = converter.field2parameter(
kind, name=name, default_in=parameter_place, use_refs=False
)
if name in defaults:
parameter["default"] = defaults[name]
parameters.append(parameter)
# body
elif name == "body" and (
isinstance(kind, Schema) or isinstance(kind, SchemaMeta)
):
if isinstance(kind, Schema):
schema_name = kind.__class__.__name__
schema = kind
elif isinstance(kind, SchemaMeta):
schema_name = kind.__name__
schema = kind()

spec.definition(schema_name, schema=schema)

ref_definition = "#/definitions/{}".format(schema_name)
ref_schema = {"$ref": ref_definition}

parameters.append(
{"in": "body", "name": "body", "required": True, "schema": ref_schema}
)

return parameters


def generate_swagger(
app: web.Application,
title=DEFAULT_TITLE,
version=DEFAULT_VERSION,
host=DEFAULT_HOST,
schemes=DEFAULT_SCHEMES,
definitions_path=DEFAULT_DEFINITIONS_PATH,
**options
):
if host is not None:
options["host"] = host
if schemes is not None:
options["schemes"] = schemes

spec = APISpec(
title=title, version=version, plugins=("apispec.ext.marshmallow",), **options
)
if definitions_path is not None:
definitions = importlib.import_module(definitions_path)

for name, schema in definitions.__dict__.items(): # type: str, Schema
if name.endswith("Schema") and len(name) > len("Schema"):
spec.definition(name, schema=schema)

resources = app.router._resources
for resource in resources:
url = resource.canonical
name = resource.name
for route in resource._routes:
method = route.method
if method == "HEAD":
continue

handler = route._handler

try:
handler_spec = handler.swagger_spec
except AttributeError:
handler_spec = {}

if "excluded" in handler_spec:
continue

try:
handler_spec["summary"] = get_summary(handler.__doc__)
handler_spec["description"] = handler.__doc__
except KeyError:
pass

parameters = get_parameters(url, handler, spec)
if parameters:
handler_spec["parameters"] = parameters

handler_spec["operationId"] = name

spec.add_path(url, operations={method.lower(): handler_spec})

return spec.to_dict()
from .swagger import (
DEFAULT_HOST,
DEFAULT_SCHEMES,
DEFAULT_VERSION,
DEFAULT_OPENAPI_VERSION,
DEFAULT_TITLE,
DEFAULT_TESTING_MODE,
DEFAULT_USE_DEFAULT_RESPONSE,
DEFAULT_DESCRIPTION,
DEFAULT_CONTACT_EMAIL,
generate_spec
)
from .decorators import response, spec
6 changes: 3 additions & 3 deletions aiohug_swagger/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional, Iterable


def ensure_swagger_attr(handler):
def _ensure_swagger_attr(handler):
try:
handler.swagger_spec
except AttributeError:
Expand All @@ -12,7 +12,7 @@ def response(response_code, schema=None, description=None):
"""A decorator that adds swagger response"""

def decorator(handler):
ensure_swagger_attr(handler)
_ensure_swagger_attr(handler)
handler.swagger_spec["responses"][response_code] = {}
if schema is not None:
handler.swagger_spec["responses"][response_code]["schema"] = schema
Expand All @@ -33,7 +33,7 @@ def spec(
response_codes: Optional[Iterable] = None,
):
def decorator(handler):
ensure_swagger_attr(handler)
_ensure_swagger_attr(handler)
handler.swagger_spec["private"] = private
handler.swagger_spec["exclude"] = exclude
handler.swagger_spec["deprecated"] = deprecated
Expand Down
18 changes: 9 additions & 9 deletions aiohug_swagger/handlers.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import os

import yaml
from aiohttp import web

from aiohug import RouteTableDef
import aiohug_swagger as swagger

from aiohug_swagger import generate_spec, spec

routes = RouteTableDef()


@swagger.spec(exclude=True)
@spec(exclude=True)
@routes.get("/swagger.json")
async def swagger_json(request):
return swagger.generate_swagger(request.app)
return generate_spec(request.app)


@swagger.spec(exclude=True)
@spec(exclude=True)
@routes.get("/swagger.yaml")
async def swagger_yaml(request):
return web.Response(
text=yaml.dump(swagger.generate_swagger(request.app)), content_type="text/yaml"
text=yaml.dump(generate_spec(request.app)), content_type="text/yaml"
)


Expand All @@ -30,18 +31,17 @@ async def _render_template(template):
return template.read().replace("{{ swagger_url }}", "/swagger.json")


@swagger.spec(exclude=True)
@spec(exclude=True)
@routes.get("/swagger.html")
async def swagger_html():
return web.Response(
text=await _render_template("swaggerui.html"), content_type="text/html"
)


@swagger.spec(exclude=True)
@spec(exclude=True)
@routes.get("/redoc.html")
async def redoc_html():

return web.Response(
text=await _render_template("redoc.html"), content_type="text/html"
)
Loading