Merge pull request #25 from Maillol/user_can_add_tags_to_generated_oas
We can add custom tags to generated OPS
This commit is contained in:
commit
1b10ebbcfa
@ -269,6 +269,8 @@ Open Api Specification.
|
|||||||
async def get(self) -> r200[List[Pet]]:
|
async def get(self) -> r200[List[Pet]]:
|
||||||
"""
|
"""
|
||||||
Find all pets
|
Find all pets
|
||||||
|
|
||||||
|
Tags: pet
|
||||||
"""
|
"""
|
||||||
pets = self.request.app["model"].list_pets()
|
pets = self.request.app["model"].list_pets()
|
||||||
return web.json_response([pet.dict() for pet in pets])
|
return web.json_response([pet.dict() for pet in pets])
|
||||||
@ -277,6 +279,7 @@ Open Api Specification.
|
|||||||
"""
|
"""
|
||||||
Add a new pet to the store
|
Add a new pet to the store
|
||||||
|
|
||||||
|
Tags: pet
|
||||||
Status Codes:
|
Status Codes:
|
||||||
201: The pet is created
|
201: The pet is created
|
||||||
"""
|
"""
|
||||||
@ -289,6 +292,7 @@ Open Api Specification.
|
|||||||
"""
|
"""
|
||||||
Find a pet by ID
|
Find a pet by ID
|
||||||
|
|
||||||
|
Tags: pet
|
||||||
Status Codes:
|
Status Codes:
|
||||||
200: Successful operation
|
200: Successful operation
|
||||||
404: Pet not found
|
404: Pet not found
|
||||||
@ -300,6 +304,7 @@ Open Api Specification.
|
|||||||
"""
|
"""
|
||||||
Update an existing pet
|
Update an existing pet
|
||||||
|
|
||||||
|
Tags: pet
|
||||||
Status Codes:
|
Status Codes:
|
||||||
200: successful operation
|
200: successful operation
|
||||||
"""
|
"""
|
||||||
|
@ -4,7 +4,7 @@ Utility to extract extra OAS description from docstring.
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
class LinesIterator:
|
class LinesIterator:
|
||||||
@ -47,11 +47,10 @@ def _i_extract_block(lines: LinesIterator):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the size of the indentation.
|
indent = re.fullmatch("( *).*", line).groups()[0]
|
||||||
if (match := re.search("^ +", line)) is None:
|
indentation = len(indent)
|
||||||
return # No block to extract.
|
start_of_other_block = re.compile(f" {{0,{indentation}}}[^ ].*")
|
||||||
indent = match.group()
|
yield line[indentation:]
|
||||||
yield line[len(indent) :]
|
|
||||||
|
|
||||||
# Yield lines until the indentation is the same or is greater than
|
# Yield lines until the indentation is the same or is greater than
|
||||||
# the first block line.
|
# the first block line.
|
||||||
@ -59,8 +58,8 @@ def _i_extract_block(lines: LinesIterator):
|
|||||||
line = next(lines)
|
line = next(lines)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return
|
return
|
||||||
while (is_empty := line.strip() == "") or line.startswith(indent):
|
while not start_of_other_block.fullmatch(line):
|
||||||
yield "" if is_empty else line[len(indent) :]
|
yield line[indentation:]
|
||||||
try:
|
try:
|
||||||
line = next(lines)
|
line = next(lines)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
@ -87,10 +86,13 @@ def status_code(docstring: str) -> Dict[int, str]:
|
|||||||
iterator = LinesIterator(docstring)
|
iterator = LinesIterator(docstring)
|
||||||
for line in iterator:
|
for line in iterator:
|
||||||
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
|
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
|
||||||
|
iterator.rewind()
|
||||||
blocks = []
|
blocks = []
|
||||||
lines = []
|
lines = []
|
||||||
for line_of_block in _i_extract_block(iterator):
|
i_block = _i_extract_block(iterator)
|
||||||
if re.search("^\\d{3}\\s*:", line_of_block):
|
next(i_block)
|
||||||
|
for line_of_block in i_block:
|
||||||
|
if re.search("^\\s*\\d{3}\\s*:", line_of_block):
|
||||||
if lines:
|
if lines:
|
||||||
blocks.append("\n".join(lines))
|
blocks.append("\n".join(lines))
|
||||||
lines = []
|
lines = []
|
||||||
@ -105,6 +107,19 @@ def status_code(docstring: str) -> Dict[int, str]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def tags(docstring: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extract the "Tags:" block of the docstring.
|
||||||
|
"""
|
||||||
|
iterator = LinesIterator(docstring)
|
||||||
|
for line in iterator:
|
||||||
|
if re.fullmatch("tags\\s*:.*", line, re.IGNORECASE):
|
||||||
|
iterator.rewind()
|
||||||
|
lines = " ".join(_i_extract_block(iterator))
|
||||||
|
return [" ".join(e.split()) for e in re.split("[,;]", lines.split(":")[1])]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def operation(docstring: str) -> str:
|
def operation(docstring: str) -> str:
|
||||||
"""
|
"""
|
||||||
Extract all docstring except the "Status Code:" block.
|
Extract all docstring except the "Status Code:" block.
|
||||||
@ -112,7 +127,8 @@ def operation(docstring: str) -> str:
|
|||||||
lines = LinesIterator(docstring)
|
lines = LinesIterator(docstring)
|
||||||
ret = []
|
ret = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
|
if re.fullmatch("status\\s+codes?\\s*:|tags\\s*:.*", line, re.IGNORECASE):
|
||||||
|
lines.rewind()
|
||||||
for _ in _i_extract_block(lines):
|
for _ in _i_extract_block(lines):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Utility to write Open Api Specifications using the Python language.
|
Utility to write Open Api Specifications using the Python language.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Union
|
from typing import Union, List
|
||||||
|
|
||||||
|
|
||||||
class Info:
|
class Info:
|
||||||
@ -157,7 +157,7 @@ class Responses:
|
|||||||
self._spec = spec.setdefault("responses", {})
|
self._spec = spec.setdefault("responses", {})
|
||||||
|
|
||||||
def __getitem__(self, status_code: Union[int, str]) -> Response:
|
def __getitem__(self, status_code: Union[int, str]) -> Response:
|
||||||
if not (100 <= int(status_code) < 600):
|
if not 100 <= int(status_code) < 600:
|
||||||
raise ValueError("status_code must be between 100 and 599")
|
raise ValueError("status_code must be between 100 and 599")
|
||||||
|
|
||||||
spec = self._spec.setdefault(str(status_code), {})
|
spec = self._spec.setdefault(str(status_code), {})
|
||||||
@ -196,6 +196,17 @@ class OperationObject:
|
|||||||
def responses(self) -> Responses:
|
def responses(self) -> Responses:
|
||||||
return Responses(self._spec)
|
return Responses(self._spec)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self) -> List[str]:
|
||||||
|
return self._spec.get("tags", [])[:]
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, tags: List[str]):
|
||||||
|
if tags:
|
||||||
|
self._spec["tags"] = tags[:]
|
||||||
|
else:
|
||||||
|
self._spec.pop("tags", None)
|
||||||
|
|
||||||
|
|
||||||
class PathItem:
|
class PathItem:
|
||||||
def __init__(self, spec: dict):
|
def __init__(self, spec: dict):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import typing
|
import typing
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import List, Type, Optional, Dict
|
from typing import List, Type, Optional
|
||||||
|
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import Response, json_response
|
||||||
from aiohttp.web_app import Application
|
from aiohttp.web_app import Application
|
||||||
@ -86,6 +86,7 @@ def _add_http_method_to_oas(
|
|||||||
description = getdoc(handler)
|
description = getdoc(handler)
|
||||||
if description:
|
if description:
|
||||||
oas_operation.description = docstring_parser.operation(description)
|
oas_operation.description = docstring_parser.operation(description)
|
||||||
|
oas_operation.tags = docstring_parser.tags(description)
|
||||||
status_code_descriptions = docstring_parser.status_code(description)
|
status_code_descriptions = docstring_parser.status_code(description)
|
||||||
else:
|
else:
|
||||||
status_code_descriptions = {}
|
status_code_descriptions = {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from inspect import iscoroutinefunction
|
from inspect import iscoroutinefunction
|
||||||
from typing import Any, Callable, Generator, Iterable, Set, ClassVar, Literal
|
from typing import Any, Callable, Generator, Iterable, Set, ClassVar
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from aiohttp.abc import AbstractView
|
from aiohttp.abc import AbstractView
|
||||||
|
@ -20,7 +20,6 @@ class ArticleModels(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ArticleView(PydanticView):
|
class ArticleView(PydanticView):
|
||||||
|
|
||||||
async def post(self, article: ArticleModel):
|
async def post(self, article: ArticleModel):
|
||||||
return web.json_response(article.dict())
|
return web.json_response(article.dict())
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
from aiohttp_pydantic.oas.docstring_parser import (
|
from aiohttp_pydantic.oas.docstring_parser import (
|
||||||
status_code,
|
status_code,
|
||||||
|
tags,
|
||||||
operation,
|
operation,
|
||||||
_i_extract_block,
|
_i_extract_block,
|
||||||
LinesIterator,
|
LinesIterator,
|
||||||
@ -13,6 +16,13 @@ def web_handler():
|
|||||||
bla bla bla
|
bla bla bla
|
||||||
|
|
||||||
|
|
||||||
|
Tags: tag1, tag2
|
||||||
|
, tag3,
|
||||||
|
|
||||||
|
t a
|
||||||
|
g
|
||||||
|
4
|
||||||
|
|
||||||
Status Codes:
|
Status Codes:
|
||||||
200: line 1
|
200: line 1
|
||||||
|
|
||||||
@ -36,6 +46,19 @@ def web_handler():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def web_handler_2():
|
||||||
|
"""
|
||||||
|
bla bla bla
|
||||||
|
|
||||||
|
|
||||||
|
Tags: tag1
|
||||||
|
Status Codes:
|
||||||
|
200: line 1
|
||||||
|
|
||||||
|
bla bla
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def test_lines_iterator():
|
def test_lines_iterator():
|
||||||
lines_iterator = LinesIterator("AAAA\nBBBB")
|
lines_iterator = LinesIterator("AAAA\nBBBB")
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
@ -61,28 +84,72 @@ def test_status_code():
|
|||||||
assert status_code(getdoc(web_handler)) == expected
|
assert status_code(getdoc(web_handler)) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_tags():
|
||||||
|
expected = ["tag1", "tag2", "tag3", "t a g 4"]
|
||||||
|
assert tags(getdoc(web_handler)) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_operation():
|
def test_operation():
|
||||||
expected = "bla bla bla\n\n\nbla bla"
|
expected = "bla bla bla\n\n\nbla bla"
|
||||||
assert operation(getdoc(web_handler)) == expected
|
assert operation(getdoc(web_handler)) == expected
|
||||||
|
assert operation(getdoc(web_handler_2)) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_i_extract_block():
|
def test_i_extract_block():
|
||||||
lines = LinesIterator(" aaaa\n\n bbbb\n\n cccc\n dddd")
|
|
||||||
|
blocks = dedent(
|
||||||
|
"""
|
||||||
|
aaaa:
|
||||||
|
|
||||||
|
bbbb
|
||||||
|
|
||||||
|
cccc
|
||||||
|
dddd
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = LinesIterator(blocks)
|
||||||
text = "\n".join(_i_extract_block(lines))
|
text = "\n".join(_i_extract_block(lines))
|
||||||
assert text == """aaaa\n\n bbbb\n\ncccc"""
|
assert text == """aaaa:\n\n bbbb\n\n cccc"""
|
||||||
|
|
||||||
|
blocks = dedent(
|
||||||
|
"""
|
||||||
|
aaaa:
|
||||||
|
|
||||||
|
bbbb
|
||||||
|
|
||||||
|
cccc
|
||||||
|
|
||||||
|
dddd
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = LinesIterator(blocks)
|
||||||
|
text = "\n".join(_i_extract_block(lines))
|
||||||
|
assert text == """aaaa:\n\n bbbb\n\n cccc\n"""
|
||||||
|
|
||||||
|
blocks = dedent(
|
||||||
|
"""
|
||||||
|
aaaa:
|
||||||
|
|
||||||
|
bbbb
|
||||||
|
|
||||||
|
cccc
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = LinesIterator(blocks)
|
||||||
|
text = "\n".join(_i_extract_block(lines))
|
||||||
|
assert text == """aaaa:\n\n bbbb\n\n cccc"""
|
||||||
|
|
||||||
lines = LinesIterator("")
|
lines = LinesIterator("")
|
||||||
text = "\n".join(_i_extract_block(lines))
|
text = "\n".join(_i_extract_block(lines))
|
||||||
assert text == ""
|
assert text == ""
|
||||||
|
|
||||||
lines = LinesIterator("aaaa\n bbbb") # the indented block is cut by a new block
|
lines = LinesIterator("\n")
|
||||||
text = "\n".join(_i_extract_block(lines))
|
text = "\n".join(_i_extract_block(lines))
|
||||||
assert text == ""
|
assert text == ""
|
||||||
|
|
||||||
lines = LinesIterator(" \n ")
|
lines = LinesIterator("aaaa:")
|
||||||
text = "\n".join(_i_extract_block(lines))
|
text = "\n".join(_i_extract_block(lines))
|
||||||
assert text == ""
|
assert text == "aaaa:"
|
||||||
|
|
||||||
lines = LinesIterator(" aaaa\n bbbb")
|
|
||||||
text = "\n".join(_i_extract_block(lines))
|
|
||||||
assert text == "aaaa\nbbbb"
|
|
||||||
|
@ -119,6 +119,24 @@ def test_paths_operation_requestBody():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_operation_tags():
|
||||||
|
oas = OpenApiSpec3()
|
||||||
|
operation = oas.paths["/users/{petId}"].get
|
||||||
|
assert operation.tags == []
|
||||||
|
operation.tags = ['pets']
|
||||||
|
|
||||||
|
assert oas.spec['paths']['/users/{petId}'] == {
|
||||||
|
'get': {
|
||||||
|
'tags': ['pets']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.tags = []
|
||||||
|
assert oas.spec['paths']['/users/{petId}'] == {
|
||||||
|
'get': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_paths_operation_responses():
|
def test_paths_operation_responses():
|
||||||
oas = OpenApiSpec3()
|
oas = OpenApiSpec3()
|
||||||
response = oas.paths["/users/{petId}"].get.responses[200]
|
response = oas.paths["/users/{petId}"].get.responses[200]
|
||||||
|
@ -35,6 +35,7 @@ class PetCollectionView(PydanticView):
|
|||||||
"""
|
"""
|
||||||
Get a list of pets
|
Get a list of pets
|
||||||
|
|
||||||
|
Tags: pet
|
||||||
Status Codes:
|
Status Codes:
|
||||||
200: Successful operation
|
200: Successful operation
|
||||||
"""
|
"""
|
||||||
@ -46,7 +47,13 @@ class PetCollectionView(PydanticView):
|
|||||||
|
|
||||||
|
|
||||||
class PetItemView(PydanticView):
|
class PetItemView(PydanticView):
|
||||||
async def get(self, id: int, /, size: Union[int, Literal['x', 'l', 's']], day: Union[int, Literal["now"]] = "now") -> Union[r200[Pet], r404]:
|
async def get(
|
||||||
|
self,
|
||||||
|
id: int,
|
||||||
|
/,
|
||||||
|
size: Union[int, Literal["x", "l", "s"]],
|
||||||
|
day: Union[int, Literal["now"]] = "now",
|
||||||
|
) -> Union[r200[Pet], r404]:
|
||||||
return web.json_response()
|
return web.json_response()
|
||||||
|
|
||||||
async def put(self, id: int, /, pet: Pet):
|
async def put(self, id: int, /, pet: Pet):
|
||||||
@ -120,6 +127,7 @@ async def test_generated_oas_should_have_pets_paths(generated_oas):
|
|||||||
async def test_pets_route_should_have_get_method(generated_oas):
|
async def test_pets_route_should_have_get_method(generated_oas):
|
||||||
assert generated_oas["paths"]["/pets"]["get"] == {
|
assert generated_oas["paths"]["/pets"]["get"] == {
|
||||||
"description": "Get a list of pets",
|
"description": "Get a list of pets",
|
||||||
|
"tags": ["pet"],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"in": "query",
|
"in": "query",
|
||||||
@ -246,20 +254,28 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
|
|||||||
"required": True,
|
"required": True,
|
||||||
"schema": {"title": "id", "type": "integer"},
|
"schema": {"title": "id", "type": "integer"},
|
||||||
},
|
},
|
||||||
{'in': 'query',
|
{
|
||||||
'name': 'size',
|
"in": "query",
|
||||||
'required': True,
|
"name": "size",
|
||||||
'schema': {'anyOf': [{'type': 'integer'},
|
"required": True,
|
||||||
{'enum': ['x', 'l', 's'],
|
"schema": {
|
||||||
'type': 'string'}],
|
"anyOf": [
|
||||||
'title': 'size'}},
|
{"type": "integer"},
|
||||||
{'in': 'query',
|
{"enum": ["x", "l", "s"], "type": "string"},
|
||||||
'name': 'day',
|
],
|
||||||
'required': False,
|
"title": "size",
|
||||||
'schema': {'anyOf': [{'type': 'integer'},
|
},
|
||||||
{'enum': ['now'], 'type': 'string'}],
|
},
|
||||||
'default': 'now',
|
{
|
||||||
'title': 'day'}}
|
"in": "query",
|
||||||
|
"name": "day",
|
||||||
|
"required": False,
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [{"type": "integer"}, {"enum": ["now"], "type": "string"}],
|
||||||
|
"default": "now",
|
||||||
|
"title": "day",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user