Skip to content

Commit

Permalink
Merge pull request #147 from 0b01001001/release
Browse files Browse the repository at this point in the history
Release 0.5.2
  • Loading branch information
kemingy authored Jun 4, 2021
2 parents d34ac5e + 5831c93 commit 14292a0
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 78 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

setup(
name="spectree",
version="0.5.1",
version="0.5.2",
author="Keming Yang",
author_email="kemingy94@gmail.com",
description=(
Expand Down
4 changes: 2 additions & 2 deletions spectree/response.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel

from .models import UnprocessableEntity
from .utils import parse_code
from .utils import get_model_key, parse_code


class Response:
Expand Down Expand Up @@ -61,7 +61,7 @@ def generate_spec(self):
responses[parse_code(code)] = {"description": DEFAULT_CODE_DESC[code]}

for code, model in self.code_models.items():
model_name = f"{model.__module__}.{model.__name__}"
model_name = get_model_key(model=model)
responses[parse_code(code)] = {
"description": DEFAULT_CODE_DESC[code],
"content": {
Expand Down
29 changes: 14 additions & 15 deletions spectree/spec.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from copy import deepcopy
from functools import wraps

from pydantic import BaseModel

from .config import Config
from .models import Tag
from .plugins import PLUGINS
from .utils import (
default_after_handler,
default_before_handler,
get_model_key,
get_model_schema,
parse_comments,
parse_name,
parse_params,
Expand Down Expand Up @@ -171,23 +171,12 @@ async def async_validate(*args, **kwargs):
("query", "json", "headers", "cookies"), (query, json, headers, cookies)
):
if model is not None:
assert issubclass(model, BaseModel)
model_key = f"{model.__module__}.{model.__name__}"
self.models[model_key] = deepcopy(
model.schema(
ref_template=f"#/components/schemas/{model_key}.{{model}}"
)
)
model_key = self._add_model(model=model)
setattr(validation, name, model_key)

if resp:
for model in resp.models:
model_key = f"{model.__module__}.{model.__name__}"
self.models[model_key] = deepcopy(
model.schema(
ref_template=f"#/components/schemas/{model_key}.{{model}}"
)
)
self._add_model(model=model)
validation.resp = resp

if tags:
Expand All @@ -201,6 +190,16 @@ async def async_validate(*args, **kwargs):

return decorate_validation

def _add_model(self, model) -> str:
"""
unified model processing
"""

model_key = get_model_key(model=model)
self.models[model_key] = deepcopy(get_model_schema(model=model))

return model_key

def _generate_spec(self):
"""
generate OpenAPI spec according to routes and decorators
Expand Down
58 changes: 58 additions & 0 deletions spectree/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import inspect
import logging
import re
from hashlib import sha1

from pydantic import BaseModel

# parse HTTP status code to get the code
HTTP_CODE = re.compile(r"^HTTP_(?P<code>\d{3})$")
Expand Down Expand Up @@ -155,3 +158,58 @@ def default_after_handler(req, resp, resp_validation_error, instance):
"spectree_validation": resp_validation_error.errors(),
},
)


def hash_module_path(module_path: str):
"""
generate short hashed prefix for module path
:param modelpath: `str` module path
"""

return sha1(module_path.encode()).hexdigest()[:7]


def get_model_path_key(model_path: str):
"""
generate short hashed prefix for module path (instead of its path to avoid
code-structure leaking)
:param modelpath: `str` model path in string
"""

model_path_parts = model_path.rsplit(".", 1)
if len(model_path_parts) > 1:
hashed_module_path = hash_module_path(module_path=model_path_parts[0])
model_path_key = f"{hashed_module_path}.{model_path_parts[1]}"
else:
model_path_key = model_path_parts[0]

return model_path_key


def get_model_key(model: BaseModel):
"""
generate model name prefixed by short hashed path (instead of its path to
avoid code-structure leaking)
:param model: `pydantic.BaseModel` query, json, headers or cookies from
request or response
"""

return f"{hash_module_path(module_path=model.__module__)}.{model.__name__}"


def get_model_schema(model):
"""
return a dictionary representing the model as JSON Schema with using hashed
prefix in ref
:param model: `pydantic.BaseModel` query, json, headers or cookies from
request or response
"""
assert issubclass(model, BaseModel)

return model.schema(
ref_template=f"#/components/schemas/{get_model_key(model)}.{{model}}"
)
8 changes: 4 additions & 4 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest

from spectree.utils import get_model_key, get_model_path_key, get_model_schema

from .common import JSON, SECURITY_SCHEMAS, Cookies, Headers, Query, Resp, get_paths
from .test_plugin_falcon import api as falcon_api
from .test_plugin_flask import api as flask_api
Expand All @@ -21,9 +23,7 @@
)
def test_plugin_spec(api):
models = {
f"{m.__module__}.{m.__name__}": m.schema(
ref_template=f"#/components/schemas/{m.__module__}.{m.__name__}.{{model}}"
)
get_model_key(model=m): get_model_schema(model=m)
for m in (Query, JSON, Resp, Cookies, Headers)
}
for name, schema in models.items():
Expand Down Expand Up @@ -60,7 +60,7 @@ def test_plugin_spec(api):
assert user["tags"] == ["API", "test"]
assert (
user["requestBody"]["content"]["application/json"]["schema"]["$ref"]
== "#/components/schemas/tests.common.JSON"
== f"#/components/schemas/{get_model_path_key('tests.common.JSON')}"
)
assert len(user["responses"]) == 3

Expand Down
101 changes: 50 additions & 51 deletions tests/test_response.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,50 @@
import pytest

from spectree.response import DEFAULT_CODE_DESC, Response

from .common import DemoModel


class NormalClass:
pass


def test_init_response():
for args, kwargs in [
([200], {}),
(["HTTP_110"], {}),
([], {"HTTP_200": NormalClass}),
]:
with pytest.raises(AssertionError):
Response(*args, **kwargs)

resp = Response("HTTP_200", HTTP_201=DemoModel)
assert resp.has_model()
assert resp.find_model(201) == DemoModel
assert DemoModel in resp.models

resp = Response(HTTP_200=None, HTTP_403=DemoModel)
assert resp.has_model()
assert resp.find_model(403) == DemoModel
assert resp.find_model(200) is None
assert DemoModel in resp.models

assert not Response().has_model()


def test_response_spec():
resp = Response("HTTP_200", HTTP_201=DemoModel)
spec = resp.generate_spec()
assert spec["200"]["description"] == DEFAULT_CODE_DESC["HTTP_200"]
assert spec["201"]["description"] == DEFAULT_CODE_DESC["HTTP_201"]
assert spec["422"]["description"] == DEFAULT_CODE_DESC["HTTP_422"]
assert (
spec["201"]["content"]["application/json"]["schema"]["$ref"].split("/")[-1]
== "tests.common.DemoModel"
)
assert (
spec["422"]["content"]["application/json"]["schema"]["$ref"].split("/")[-1]
== "spectree.models.UnprocessableEntity"
)

assert spec.get(200) is None
assert spec.get(404) is None
import pytest

from spectree.response import DEFAULT_CODE_DESC, Response
from spectree.utils import get_model_path_key

from .common import DemoModel


class NormalClass:
pass


def test_init_response():
for args, kwargs in [
([200], {}),
(["HTTP_110"], {}),
([], {"HTTP_200": NormalClass}),
]:
with pytest.raises(AssertionError):
Response(*args, **kwargs)

resp = Response("HTTP_200", HTTP_201=DemoModel)
assert resp.has_model()
assert resp.find_model(201) == DemoModel
assert DemoModel in resp.models

resp = Response(HTTP_200=None, HTTP_403=DemoModel)
assert resp.has_model()
assert resp.find_model(403) == DemoModel
assert resp.find_model(200) is None
assert DemoModel in resp.models

assert not Response().has_model()


def test_response_spec():
resp = Response("HTTP_200", HTTP_201=DemoModel)
spec = resp.generate_spec()
assert spec["200"]["description"] == DEFAULT_CODE_DESC["HTTP_200"]
assert spec["201"]["description"] == DEFAULT_CODE_DESC["HTTP_201"]
assert spec["422"]["description"] == DEFAULT_CODE_DESC["HTTP_422"]
assert spec["201"]["content"]["application/json"]["schema"]["$ref"].split("/")[
-1
] == get_model_path_key("tests.common.DemoModel")
assert spec["422"]["content"]["application/json"]["schema"]["$ref"].split("/")[
-1
] == get_model_path_key("spectree.models.UnprocessableEntity")

assert spec.get(200) is None
assert spec.get(404) is None
14 changes: 9 additions & 5 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from spectree.response import Response
from spectree.spec import SpecTree
from spectree.utils import (
get_model_path_key,
has_model,
parse_code,
parse_comments,
Expand Down Expand Up @@ -83,27 +84,30 @@ def test_parse_resp():
resp_spec = parse_resp(demo_func)

assert resp_spec["422"]["description"] == "Unprocessable Entity"
model_path_key = get_model_path_key("spectree.models.UnprocessableEntity")
assert (
resp_spec["422"]["content"]["application/json"]["schema"]["$ref"]
== "#/components/schemas/spectree.models.UnprocessableEntity"
== f"#/components/schemas/{model_path_key}"
)
model_path_key = get_model_path_key("tests.common.DemoModel")
assert (
resp_spec["200"]["content"]["application/json"]["schema"]["$ref"]
== "#/components/schemas/tests.common.DemoModel"
== f"#/components/schemas/{model_path_key}"
)


def test_parse_request():
model_path_key = get_model_path_key("tests.common.DemoModel")
assert (
parse_request(demo_func)["content"]["application/json"]["schema"]["$ref"]
== "#/components/schemas/tests.common.DemoModel"
== f"#/components/schemas/{model_path_key}"
)
assert parse_request(demo_class.demo_method) == {}


def test_parse_params():
models = {
"tests.common.DemoModel": DemoModel.schema(
get_model_path_key("tests.common.DemoModel"): DemoModel.schema(
ref_template="#/components/schemas/{model}"
)
}
Expand All @@ -122,7 +126,7 @@ def test_parse_params():

def test_parse_params_with_route_param_keywords():
models = {
"tests.common.DemoQuery": DemoQuery.schema(
get_model_path_key("tests.common.DemoQuery"): DemoQuery.schema(
ref_template="#/components/schemas/{model}"
)
}
Expand Down

0 comments on commit 14292a0

Please sign in to comment.