Merge pull request #3 from Maillol/add-cmd-to-generate-oas
Add a command line tool to generate OAS in a file
This commit is contained in:
commit
57b50725ea
19
README.rst
19
README.rst
@ -145,8 +145,8 @@ To declare a HTTP headers parameters, you must declare your argument as a `keywo
|
|||||||
.. _pydantic Model: https://pydantic-docs.helpmanual.io/usage/models/
|
.. _pydantic Model: https://pydantic-docs.helpmanual.io/usage/models/
|
||||||
.. _keyword-only argument: https://www.python.org/dev/peps/pep-3102/
|
.. _keyword-only argument: https://www.python.org/dev/peps/pep-3102/
|
||||||
|
|
||||||
Add route to generate Open Api Specification
|
Add route to generate Open Api Specification (OAS)
|
||||||
--------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
aiohttp_pydantic provides a sub-application to serve a route to generate Open Api Specification
|
aiohttp_pydantic provides a sub-application to serve a route to generate Open Api Specification
|
||||||
reading annotation in your PydanticView. Use *aiohttp_pydantic.oas.setup()* to add the sub-application
|
reading annotation in your PydanticView. Use *aiohttp_pydantic.oas.setup()* to add the sub-application
|
||||||
@ -168,8 +168,8 @@ By default, the route to display the Open Api Specification is /oas but you can
|
|||||||
|
|
||||||
oas.setup(app, url_prefix='/spec-api')
|
oas.setup(app, url_prefix='/spec-api')
|
||||||
|
|
||||||
If you want generate the Open Api Specification from several aiohttp sub-application.
|
If you want generate the Open Api Specification from specific aiohttp sub-applications.
|
||||||
on the same route, you must use *apps_to_expose* parameters
|
on the same route, you must use *apps_to_expose* parameter.
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python3
|
.. code-block:: python3
|
||||||
@ -179,9 +179,9 @@ on the same route, you must use *apps_to_expose* parameters
|
|||||||
|
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
sub_app_1 = web.Application()
|
sub_app_1 = web.Application()
|
||||||
|
sub_app_2 = web.Application()
|
||||||
|
|
||||||
oas.setup(app, apps_to_expose=[app, sub_app_1])
|
oas.setup(app, apps_to_expose=[sub_app_1, sub_app_2])
|
||||||
|
|
||||||
|
|
||||||
Add annotation to define response content
|
Add annotation to define response content
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -232,7 +232,6 @@ defined using a pydantic.BaseModel
|
|||||||
self.request.app["model"].remove_pet(id)
|
self.request.app["model"].remove_pet(id)
|
||||||
return web.Response(status=204)
|
return web.Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
Demo
|
Demo
|
||||||
----
|
----
|
||||||
|
|
||||||
@ -247,5 +246,11 @@ Have a look at `demo`_ for a complete example
|
|||||||
|
|
||||||
Go to http://127.0.0.1:8080/oas
|
Go to http://127.0.0.1:8080/oas
|
||||||
|
|
||||||
|
You can generate the OAS in a json file using the command:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python -m aiohttp_pydantic.oas demo.main
|
||||||
|
|
||||||
|
|
||||||
.. _demo: https://github.com/Maillol/aiohttp-pydantic/tree/main/demo
|
.. _demo: https://github.com/Maillol/aiohttp-pydantic/tree/main/demo
|
||||||
|
8
aiohttp_pydantic/oas/__main__.py
Normal file
8
aiohttp_pydantic/oas/__main__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
from .cmd import setup
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Generate Open API Specification")
|
||||||
|
setup(parser)
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
45
aiohttp_pydantic/oas/cmd.py
Normal file
45
aiohttp_pydantic/oas/cmd.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .view import generate_oas
|
||||||
|
|
||||||
|
|
||||||
|
def application_type(value):
|
||||||
|
"""
|
||||||
|
Return aiohttp application defined in the value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
module_name, app_name = value.split(":")
|
||||||
|
except ValueError:
|
||||||
|
module_name, app_name = value, "app"
|
||||||
|
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
try:
|
||||||
|
if app_name.endswith("()"):
|
||||||
|
app_name = app_name.strip("()")
|
||||||
|
factory_app = getattr(module, app_name)
|
||||||
|
return factory_app()
|
||||||
|
return getattr(module, app_name)
|
||||||
|
|
||||||
|
except AttributeError as error:
|
||||||
|
raise argparse.ArgumentTypeError(error) from error
|
||||||
|
|
||||||
|
|
||||||
|
def setup(parser: argparse.ArgumentParser):
|
||||||
|
parser.add_argument(
|
||||||
|
"apps",
|
||||||
|
metavar="APP",
|
||||||
|
type=application_type,
|
||||||
|
nargs="*",
|
||||||
|
help="The name of the module containing the asyncio.web.Application."
|
||||||
|
" By default the variable named 'app' is loaded but you can define"
|
||||||
|
" an other variable name ending the name of module with : characters"
|
||||||
|
" and the name of variable. Example: my_package.my_module:my_app",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.set_defaults(func=show_oas)
|
||||||
|
|
||||||
|
|
||||||
|
def show_oas(args: argparse.Namespace):
|
||||||
|
print(json.dumps(generate_oas(args.apps), sort_keys=True, indent=4))
|
@ -1,7 +1,8 @@
|
|||||||
import typing
|
import typing
|
||||||
from typing import Type
|
from typing import List, Type
|
||||||
|
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
|
from aiohttp.web_app import Application
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
|
from aiohttp_pydantic.oas.struct import OpenApiSpec3, OperationObject, PathItem
|
||||||
@ -106,11 +107,10 @@ def _add_http_method_to_oas(
|
|||||||
_OASResponseBuilder(oas_operation).build(return_type)
|
_OASResponseBuilder(oas_operation).build(return_type)
|
||||||
|
|
||||||
|
|
||||||
async def get_oas(request):
|
def generate_oas(apps: List[Application]) -> dict:
|
||||||
"""
|
"""
|
||||||
Generate Open Api Specification from PydanticView in application.
|
Generate and return Open Api Specification from PydanticView in application.
|
||||||
"""
|
"""
|
||||||
apps = request.app["apps to expose"]
|
|
||||||
oas = OpenApiSpec3()
|
oas = OpenApiSpec3()
|
||||||
for app in apps:
|
for app in apps:
|
||||||
for resources in app.router.resources():
|
for resources in app.router.resources():
|
||||||
@ -125,7 +125,15 @@ async def get_oas(request):
|
|||||||
else:
|
else:
|
||||||
_add_http_method_to_oas(path, resource_route.method, view)
|
_add_http_method_to_oas(path, resource_route.method, view)
|
||||||
|
|
||||||
return json_response(oas.spec)
|
return oas.spec
|
||||||
|
|
||||||
|
|
||||||
|
async def get_oas(request):
|
||||||
|
"""
|
||||||
|
View to generate the Open Api Specification from PydanticView in application.
|
||||||
|
"""
|
||||||
|
apps = request.app["apps to expose"]
|
||||||
|
return json_response(generate_oas(apps))
|
||||||
|
|
||||||
|
|
||||||
async def oas_ui(request):
|
async def oas_ui(request):
|
||||||
|
@ -9,8 +9,14 @@ from aiohttp.web_exceptions import HTTPMethodNotAllowed
|
|||||||
from aiohttp.web_response import StreamResponse
|
from aiohttp.web_response import StreamResponse
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from .injectors import (AbstractInjector, BodyGetter, HeadersGetter,
|
from .injectors import (
|
||||||
MatchInfoGetter, QueryGetter, _parse_func_signature)
|
AbstractInjector,
|
||||||
|
BodyGetter,
|
||||||
|
HeadersGetter,
|
||||||
|
MatchInfoGetter,
|
||||||
|
QueryGetter,
|
||||||
|
_parse_func_signature,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PydanticView(AbstractView):
|
class PydanticView(AbstractView):
|
||||||
|
@ -1,25 +1,5 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import middleware
|
|
||||||
|
|
||||||
from aiohttp_pydantic import oas
|
from .main import app
|
||||||
|
|
||||||
from .model import Model
|
|
||||||
from .view import PetCollectionView, PetItemView
|
|
||||||
|
|
||||||
|
|
||||||
@middleware
|
|
||||||
async def pet_not_found_to_404(request, handler):
|
|
||||||
try:
|
|
||||||
return await handler(request)
|
|
||||||
except Model.NotFound as key:
|
|
||||||
return web.json_response({"error": f"Pet {key} does not exist"}, status=404)
|
|
||||||
|
|
||||||
|
|
||||||
app = web.Application(middlewares=[pet_not_found_to_404])
|
|
||||||
oas.setup(app)
|
|
||||||
|
|
||||||
app["model"] = Model()
|
|
||||||
app.router.add_view("/pets", PetCollectionView)
|
|
||||||
app.router.add_view("/pets/{id}", PetItemView)
|
|
||||||
|
|
||||||
web.run_app(app)
|
web.run_app(app)
|
||||||
|
22
demo/main.py
Normal file
22
demo/main.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from aiohttp.web import Application, json_response, middleware
|
||||||
|
|
||||||
|
from aiohttp_pydantic import oas
|
||||||
|
|
||||||
|
from .model import Model
|
||||||
|
from .view import PetCollectionView, PetItemView
|
||||||
|
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def pet_not_found_to_404(request, handler):
|
||||||
|
try:
|
||||||
|
return await handler(request)
|
||||||
|
except Model.NotFound as key:
|
||||||
|
return json_response({"error": f"Pet {key} does not exist"}, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
app = Application(middlewares=[pet_not_found_to_404])
|
||||||
|
oas.setup(app)
|
||||||
|
|
||||||
|
app["model"] = Model()
|
||||||
|
app.router.add_view("/pets", PetCollectionView)
|
||||||
|
app.router.add_view("/pets/{id}", PetItemView)
|
0
tests/test_oas/test_cmd/__init__.py
Normal file
0
tests/test_oas/test_cmd/__init__.py
Normal file
27
tests/test_oas/test_cmd/sample.py
Normal file
27
tests/test_oas/test_cmd/sample.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from aiohttp_pydantic import PydanticView
|
||||||
|
|
||||||
|
|
||||||
|
class View1(PydanticView):
|
||||||
|
async def get(self, a: int, /):
|
||||||
|
return web.json_response()
|
||||||
|
|
||||||
|
|
||||||
|
class View2(PydanticView):
|
||||||
|
async def post(self, b: int, /):
|
||||||
|
return web.json_response()
|
||||||
|
|
||||||
|
|
||||||
|
sub_app = web.Application()
|
||||||
|
sub_app.router.add_view("/route-2/{b}", View2)
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/route-1/{a}", View1)
|
||||||
|
app.add_subapp("/sub-app", sub_app)
|
||||||
|
|
||||||
|
|
||||||
|
def make_app():
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_view("/route-3/{a}", View1)
|
||||||
|
return app
|
120
tests/test_oas/test_cmd/test_cmd.py
Normal file
120
tests/test_oas/test_cmd/test_cmd.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import argparse
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp_pydantic.oas import cmd
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cmd_line():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
cmd.setup(parser)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_oad_of_app(cmd_line, capfd):
|
||||||
|
args = cmd_line.parse_args(["tests.test_oas.test_cmd.sample"])
|
||||||
|
args.func(args)
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
expected = dedent(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/route-1/{a}": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "a",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/sub-app/route-2/{b}": {
|
||||||
|
"post": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "b",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured.out.strip() == expected.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_oad_of_sub_app(cmd_line, capfd):
|
||||||
|
args = cmd_line.parse_args(["tests.test_oas.test_cmd.sample:sub_app"])
|
||||||
|
args.func(args)
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
expected = dedent(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/sub-app/route-2/{b}": {
|
||||||
|
"post": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "b",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured.out.strip() == expected.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_oad_of_a_callable(cmd_line, capfd):
|
||||||
|
args = cmd_line.parse_args(["tests.test_oas.test_cmd.sample:make_app()"])
|
||||||
|
args.func(args)
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
expected = dedent(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"paths": {
|
||||||
|
"/route-3/{a}": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "a",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured.out.strip() == expected.strip()
|
Loading…
x
Reference in New Issue
Block a user