Skip to content

Commit

Permalink
Merge pull request #129 from iterait/dev
Browse files Browse the repository at this point in the history
Release 0.9.10
  • Loading branch information
Jan Buchar authored Jun 3, 2021
2 parents 92a7cb8 + bade2c3 commit d8551ae
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 15 deletions.
11 changes: 10 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,14 @@ jobs:
docker:
- image: circleci/python:3.8

test_py_39:
<<: *test_job
docker:
- image: circleci/python:3.9

coverage:
docker:
- image: circleci/python:3.8
- image: circleci/python:3.9
working_directory: ~/apistrap
steps:
- checkout
Expand All @@ -75,17 +80,20 @@ workflows:
jobs:
- test_py_37
- test_py_38
- test_py_39
- coverage:
requires:
- test_py_37
- test_py_38
- test_py_39
- deploy:
filters:
branches:
only: master
requires:
- test_py_37
- test_py_38
- test_py_39
- coverage

nightly-build:
Expand All @@ -100,3 +108,4 @@ workflows:
jobs:
- test_py_37
- test_py_38
- test_py_39
5 changes: 4 additions & 1 deletion apistrap/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ async def _stream_file_response(
if response.attachment_filename is None:
raise TypeError("Missing attachment filename")

headers["Content-Disposition"] = f"attachment,filename={response.attachment_filename}"
if "," in response.attachment_filename:
raise ValueError("Filename should not contain commas")

headers["Content-Disposition"] = f'attachment; filename="{response.attachment_filename}"'

if isinstance(response.filename_or_fp, str) or isinstance(response.filename_or_fp, Path):
return web.FileResponse(response.filename_or_fp, headers=headers, status=code)
Expand Down
3 changes: 2 additions & 1 deletion apistrap/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

if TYPE_CHECKING: # pragma: no cover
from apistrap.extension import SecurityScheme
from apistrap.utils import StringLike


class IgnoreDecorator:
Expand Down Expand Up @@ -82,5 +83,5 @@ class SecurityDecorator:
Enforces user authentication and authorization.
"""

scopes: Sequence[str]
scopes: Sequence[StringLike]
security_scheme: Optional[SecurityScheme] = None
9 changes: 7 additions & 2 deletions apistrap/extension.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import abc
from abc import ABCMeta
from dataclasses import dataclass
from functools import partial
from itertools import chain
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, Union
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, Union, TYPE_CHECKING

from apispec import APISpec
from apispec.utils import OpenAPIVersion
Expand All @@ -23,6 +25,9 @@
from apistrap.schemas import ErrorResponse
from apistrap.tags import TagData

if TYPE_CHECKING: # pragma: no cover
from apistrap.utils import StringLike


class SecurityScheme(metaclass=ABCMeta):
"""
Expand Down Expand Up @@ -421,7 +426,7 @@ def ignore_params(self, *ignored_params: str):
"""
return partial(self._decorate, IgnoreParamsDecorator(ignored_params))

def security(self, *scopes: str, scheme: SecurityScheme = None):
def security(self, *scopes: StringLike, scheme: SecurityScheme = None):
"""
A decorator that enforces user authentication and authorization.
"""
Expand Down
2 changes: 1 addition & 1 deletion apistrap/operation_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ def _get_required_scopes(self) -> Generator[Tuple[SecurityScheme, Sequence[str]]
def _postprocess_response(self, response: Union[Model, Tuple[Model, int]]) -> Tuple[Model, int, Optional[str]]:
"""
Check response type and code and add the code if necessary.
:param response: response received from a view handler
:return: a response and status code
"""
Expand Down
35 changes: 30 additions & 5 deletions apistrap/schematics_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ def _field_to_schema_object(field: BaseType, apistrap: Optional[Apistrap]) -> Op
return _model_dict_to_schema_object(field, apistrap)
elif isinstance(field.field, UnionType):
return _union_dict_to_schema_object(field, apistrap)
elif isinstance(field.field, ListType) and isinstance(field.field.field, ModelType):
return _dict_of_model_lists_to_schema_object(field, apistrap)
elif isinstance(field.field, BaseType):
return _primitive_dict_to_schema_object(field)
elif isinstance(field, StringType):
Expand Down Expand Up @@ -310,18 +312,22 @@ def _union_dict_to_schema_object(field: DictType, apistrap: Optional[Apistrap])
return schema


def _primitive_array_to_schema_object(field: ListType) -> Dict[str, Any]:
def _dict_of_model_lists_to_schema_object(field: DictType, apistrap: Optional[Apistrap]) -> Dict[str, Any]:
"""
Get a SchemaObject for a list of primitive types
Get a SchemaObject for a dictionary of model lists
:param field: the field that determines the value type
:return: a SchemaObject
"""

schema = {
"type": "array",
"title": f"List of {field.field.__class__.__name__}",
"items": _field_to_schema_object(field.field, None),
"type": "object",
"title": f"Dictionary of {field.field.field._model_class.__name__} lists",
"additionalProperties": {
"type": "array",
"title": f"List of {field.field.field._model_class.__name__}",
"items": _field_to_schema_object(field.field.field, apistrap),
},
}

schema.update(_extract_model_description(field))
Expand All @@ -348,6 +354,25 @@ def _primitive_dict_to_schema_object(field: DictType) -> Dict[str, Any]:
return schema


def _primitive_array_to_schema_object(field: ListType) -> Dict[str, Any]:
"""
Get a SchemaObject for a list of primitive types
:param field: the field that determines the value type
:return: a SchemaObject
"""

schema = {
"type": "array",
"title": f"List of {field.field.__class__.__name__}",
"items": _field_to_schema_object(field.field, None),
}

schema.update(_extract_model_description(field))

return schema


def _model_field_to_schema_object(field: ModelType, apistrap: Optional[Apistrap]) -> Dict[str, Any]:
"""
Get a SchemaObject for a model field.
Expand Down
15 changes: 14 additions & 1 deletion apistrap/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import inspect
import traceback
from typing import Any, Callable, Dict, Mapping, Type, Union
from typing import Any, Callable, Dict, Mapping, Type, Union, TYPE_CHECKING

from more_itertools import flatten


try:
from typing import Protocol

class StringLike(Protocol):
def __str__(self) -> str:
...
except ImportError:
Protocol = None

if not TYPE_CHECKING and Protocol is None: # pragma: no cover
StringLike = str


def format_exception(exception: Exception) -> Mapping[str, Any]:
"""
Format an exception into a dict containing exception information such as class name, message and traceback.
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from setuptools import find_packages, setup


setup(name='apistrap',
version='0.9.9',
description='Iterait REST API utilities',
Expand All @@ -23,10 +24,10 @@
},
zip_safe=False,
setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-flask', 'pytest-aiohttp', 'flask', 'aiohttp', 'numpy'],
tests_require=['pytest', 'pytest-mock', 'pytest-flask', 'pytest-aiohttp', 'flask<=1.1.4', 'aiohttp', 'numpy'],
install_requires=['apispec==1.2', 'schematics', 'more_itertools', 'Werkzeug', 'jinja2', 'docstring_parser>=0.5'],
extras_require={
'flask': ['flask'],
'flask': ['flask<=1.1.4'],
'aiohttp': ['aiohttp'],
'NonNanFloatType': ['numpy']
})
83 changes: 82 additions & 1 deletion tests/test_schematics_converters.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import pytest
from schematics import Model
from schematics.types import DictType, IntType, ListType, ModelType, StringType

from apistrap.flask import FlaskApistrap
from apistrap.schematics_converters import schematics_model_to_schema_object


@pytest.fixture(scope="function")
def apistrap_extension():
yield FlaskApistrap()


class ExampleModel(Model):
string = StringType()

Expand Down Expand Up @@ -140,7 +147,11 @@ class ModelWithDescriptions(Model):
def test_schematics_to_schema_object_descriptions():
assert schematics_model_to_schema_object(ModelWithDescriptions) == {
"properties": {
"primitive": {"type": "integer", "title": "Primitive title", "description": "Primitive description",},
"primitive": {
"type": "integer",
"title": "Primitive title",
"description": "Primitive description",
},
"list": {
"type": "array",
"items": {"type": "string"},
Expand Down Expand Up @@ -175,3 +186,73 @@ def test_enum():
"type": "object",
"properties": {"enum_field": {"type": "string", "enum": ["member_a", "member_b"]}},
}


class ModelWithDictOfLists(Model):
data = DictType(ListType(StringType()))


def test_schematics_to_schema_object_dict_of_lists():
assert schematics_model_to_schema_object(ModelWithDictOfLists) == {
"type": "object",
"title": "ModelWithDictOfLists",
"properties": {
"data": {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}, "title": "List of StringType"},
"title": "Dictionary of ListType",
}
},
}


class ModelWithDictOfListsOfModels(Model):
data = DictType(ListType(ModelType(ExampleModel)))


def test_schematics_to_schema_object_dict_of_lists_of_models():
assert schematics_model_to_schema_object(ModelWithDictOfListsOfModels) == {
"type": "object",
"title": "ModelWithDictOfListsOfModels",
"properties": {
"data": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {"type": "object", "title": "ExampleModel", "properties": {"string": {"type": "string"}}},
"title": "List of ExampleModel",
},
"title": "Dictionary of ExampleModel lists",
}
},
}


def test_schematics_to_schema_object_dict_of_lists_of_models_ref(apistrap_extension):
assert schematics_model_to_schema_object(ModelWithDictOfListsOfModels, apistrap_extension) == {
"$ref": "#/components/schemas/ModelWithDictOfListsOfModels",
}

definitions = apistrap_extension.to_openapi_dict()["components"]["schemas"]

assert definitions["ModelWithDictOfListsOfModels"] == {
"type": "object",
"title": "ModelWithDictOfListsOfModels",
"properties": {
"data": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {"$ref": "#/components/schemas/ExampleModel"},
"title": "List of ExampleModel",
},
"title": "Dictionary of ExampleModel lists",
}
},
}

assert definitions["ExampleModel"] == {
"type": "object",
"title": "ExampleModel",
"properties": {"string": {"type": "string"}},
}

0 comments on commit d8551ae

Please sign in to comment.