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:
MAILLOL Vincent
2021-08-22 15:03:04 +02:00
committed by GitHub
9 changed files with 173 additions and 40 deletions
+5
View File
@@ -269,6 +269,8 @@ Open Api Specification.
async def get(self) -> r200[List[Pet]]:
"""
Find all pets
Tags: pet
"""
pets = self.request.app["model"].list_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
Tags: pet
Status Codes:
201: The pet is created
"""
@@ -289,6 +292,7 @@ Open Api Specification.
"""
Find a pet by ID
Tags: pet
Status Codes:
200: Successful operation
404: Pet not found
@@ -300,6 +304,7 @@ Open Api Specification.
"""
Update an existing pet
Tags: pet
Status Codes:
200: successful operation
"""
+27 -11
View File
@@ -4,7 +4,7 @@ Utility to extract extra OAS description from docstring.
import re
import textwrap
from typing import Dict
from typing import Dict, List
class LinesIterator:
@@ -47,11 +47,10 @@ def _i_extract_block(lines: LinesIterator):
except StopIteration:
return
# Get the size of the indentation.
if (match := re.search("^ +", line)) is None:
return # No block to extract.
indent = match.group()
yield line[len(indent) :]
indent = re.fullmatch("( *).*", line).groups()[0]
indentation = len(indent)
start_of_other_block = re.compile(f" {{0,{indentation}}}[^ ].*")
yield line[indentation:]
# Yield lines until the indentation is the same or is greater than
# the first block line.
@@ -59,8 +58,8 @@ def _i_extract_block(lines: LinesIterator):
line = next(lines)
except StopIteration:
return
while (is_empty := line.strip() == "") or line.startswith(indent):
yield "" if is_empty else line[len(indent) :]
while not start_of_other_block.fullmatch(line):
yield line[indentation:]
try:
line = next(lines)
except StopIteration:
@@ -87,10 +86,13 @@ def status_code(docstring: str) -> Dict[int, str]:
iterator = LinesIterator(docstring)
for line in iterator:
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
iterator.rewind()
blocks = []
lines = []
for line_of_block in _i_extract_block(iterator):
if re.search("^\\d{3}\\s*:", line_of_block):
i_block = _i_extract_block(iterator)
next(i_block)
for line_of_block in i_block:
if re.search("^\\s*\\d{3}\\s*:", line_of_block):
if lines:
blocks.append("\n".join(lines))
lines = []
@@ -105,6 +107,19 @@ def status_code(docstring: str) -> Dict[int, str]:
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:
"""
Extract all docstring except the "Status Code:" block.
@@ -112,7 +127,8 @@ def operation(docstring: str) -> str:
lines = LinesIterator(docstring)
ret = []
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):
pass
else:
+13 -2
View File
@@ -2,7 +2,7 @@
Utility to write Open Api Specifications using the Python language.
"""
from typing import Union
from typing import Union, List
class Info:
@@ -157,7 +157,7 @@ class Responses:
self._spec = spec.setdefault("responses", {})
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")
spec = self._spec.setdefault(str(status_code), {})
@@ -196,6 +196,17 @@ class OperationObject:
def responses(self) -> Responses:
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:
def __init__(self, spec: dict):
+2 -1
View File
@@ -1,7 +1,7 @@
import typing
from inspect import getdoc
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_app import Application
@@ -86,6 +86,7 @@ def _add_http_method_to_oas(
description = getdoc(handler)
if description:
oas_operation.description = docstring_parser.operation(description)
oas_operation.tags = docstring_parser.tags(description)
status_code_descriptions = docstring_parser.status_code(description)
else:
status_code_descriptions = {}
+1 -1
View File
@@ -1,6 +1,6 @@
from functools import update_wrapper
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
from aiohttp.abc import AbstractView
-1
View File
@@ -20,7 +20,6 @@ class ArticleModels(BaseModel):
class ArticleView(PydanticView):
async def post(self, article: ArticleModel):
return web.json_response(article.dict())
+75 -8
View File
@@ -1,5 +1,8 @@
from textwrap import dedent
from aiohttp_pydantic.oas.docstring_parser import (
status_code,
tags,
operation,
_i_extract_block,
LinesIterator,
@@ -13,6 +16,13 @@ def web_handler():
bla bla bla
Tags: tag1, tag2
, tag3,
t a
g
4
Status Codes:
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():
lines_iterator = LinesIterator("AAAA\nBBBB")
with pytest.raises(StopIteration):
@@ -61,28 +84,72 @@ def test_status_code():
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():
expected = "bla bla bla\n\n\nbla bla"
assert operation(getdoc(web_handler)) == expected
assert operation(getdoc(web_handler_2)) == expected
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))
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("")
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator("aaaa\n bbbb") # the indented block is cut by a new block
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator("\n")
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator(" aaaa\n bbbb")
lines = LinesIterator("aaaa:")
text = "\n".join(_i_extract_block(lines))
assert text == "aaaa\nbbbb"
assert text == "aaaa:"
+18
View File
@@ -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():
oas = OpenApiSpec3()
response = oas.paths["/users/{petId}"].get.responses[200]
+31 -15
View File
@@ -35,6 +35,7 @@ class PetCollectionView(PydanticView):
"""
Get a list of pets
Tags: pet
Status Codes:
200: Successful operation
"""
@@ -46,7 +47,13 @@ class PetCollectionView(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()
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):
assert generated_oas["paths"]["/pets"]["get"] == {
"description": "Get a list of pets",
"tags": ["pet"],
"parameters": [
{
"in": "query",
@@ -246,20 +254,28 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
"required": True,
"schema": {"title": "id", "type": "integer"},
},
{'in': 'query',
'name': 'size',
'required': True,
'schema': {'anyOf': [{'type': 'integer'},
{'enum': ['x', 'l', 's'],
'type': 'string'}],
'title': 'size'}},
{'in': 'query',
'name': 'day',
'required': False,
'schema': {'anyOf': [{'type': 'integer'},
{'enum': ['now'], 'type': 'string'}],
'default': 'now',
'title': 'day'}}
{
"in": "query",
"name": "size",
"required": True,
"schema": {
"anyOf": [
{"type": "integer"},
{"enum": ["x", "l", "s"], "type": "string"},
],
"title": "size",
},
},
{
"in": "query",
"name": "day",
"required": False,
"schema": {
"anyOf": [{"type": "integer"}, {"enum": ["now"], "type": "string"}],
"default": "now",
"title": "day",
},
},
],
"responses": {
"200": {