Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(robot-server): fix typing_extensions bug #5281

Merged
merged 1 commit into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


# TODO(isk: 3/24/20): remove this enum and replace with typing.Literal
# after migration to python 3.8
class ResourceTypes(str, Enum):
"""Resource object types"""
item = "item"
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

from .request import json_api_request, RequestModel
from .response import json_api_response, ResponseModel
from . import ResourceTypes


def generate_json_api_models(
type_string: str,
resource_type: ResourceTypes,
attributes_model: Any,
*,
list_response: bool = False
) -> Tuple[Type[RequestModel], Type[ResponseModel]]:
return (
json_api_request(type_string, attributes_model),
json_api_request(resource_type, attributes_model),
json_api_response(
type_string, attributes_model, use_list=list_response
resource_type, attributes_model, use_list=list_response
),
)
16 changes: 7 additions & 9 deletions robot-server/robot_server/service/models/json_api/request.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Generic, TypeVar, Optional, Any, Type
from typing_extensions import Literal
from pydantic import Field
from pydantic.generics import GenericModel

TypeT = TypeVar('TypeT')
from . import ResourceTypes

AttributesT = TypeVar('AttributesT')


class RequestDataModel(GenericModel, Generic[TypeT, AttributesT]):
class RequestDataModel(GenericModel, Generic[AttributesT]):
"""
"""
id: Optional[str] = \
Expand All @@ -16,7 +16,7 @@ class RequestDataModel(GenericModel, Generic[TypeT, AttributesT]):
" required when the resource object originates at"
" the client and represents a new resource to be"
" created on the server.")
type: TypeT = \
type: ResourceTypes = \
Field(...,
description="type member is used to describe resource objects"
" that share common attributes.")
Expand All @@ -42,13 +42,11 @@ def attributes(self):

# Note(isk: 3/13/20): formats and returns request model
def json_api_request(
type_string: str,
resource_type: ResourceTypes,
attributes_model: Any
) -> Type[RequestModel]:
request_data_model = RequestDataModel[
Literal[type_string], # type: ignore
attributes_model, # type: ignore
]
type_string = resource_type.value
request_data_model = RequestDataModel[attributes_model] # type: ignore
request_data_model.__name__ = f'RequestData[{type_string}]'
request_model = RequestModel[request_data_model]
request_model.__name__ = f'Request[{type_string}]'
Expand Down
24 changes: 12 additions & 12 deletions robot-server/robot_server/service/models/json_api/response.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from typing import Generic, TypeVar, Optional, List, \
Dict, Any, Type, get_type_hints
from typing_extensions import Literal
from pydantic import Field
from pydantic.generics import GenericModel

from .resource_links import ResourceLinks
from . import ResourceTypes


TypeT = TypeVar('TypeT', bound=str)
AttributesT = TypeVar('AttributesT')


class ResponseDataModel(GenericModel, Generic[TypeT, AttributesT]):
class ResponseDataModel(GenericModel, Generic[AttributesT]):
"""
"""
id: str = \
Field(...,
description="id member represents a resource object.")
type: TypeT = \
type: ResourceTypes = \
Field(...,
description="type member is used to describe resource objects"
" that share common attributes.")
Expand Down Expand Up @@ -64,9 +64,9 @@ def resource_object(
# a list, if not return None.
if getattr(data_type, '__origin__', None) is list:
data_type = data_type.__args__[0]
# Note(isk: 3/13/20): get type name from data object
# using get_type_hints
typename = get_type_hints(data_type)['type'].__args__[0]
# Note(isk: 3/24/20): get type name from _resource_type private
# variable set in json_api_response
typename = cls._resource_type # type: ignore
return data_type(
id=id,
type=typename,
Expand All @@ -77,23 +77,23 @@ def resource_object(
# Note(isk: 3/13/20): returns response based on whether
# the data object is a list or not
def json_api_response(
type_string: str,
resource_type: ResourceTypes,
attributes_model: Any,
*,
use_list: bool = False
) -> Type[ResponseModel]:
response_data_model = ResponseDataModel[
Literal[type_string], # type: ignore
attributes_model, # type: ignore
]
type_string = resource_type.value
response_data_model = ResponseDataModel[attributes_model] # type: ignore
if use_list:
response_data_model = List[response_data_model] # type: ignore
response_data_model.__name__ = f'ListResponseData[{type_string}]'
response_model_list = ResponseModel[response_data_model]
response_model_list.__name__ = f'ListResponse[{type_string}]'
response_model_list._resource_type = type_string # type: ignore
return response_model_list
else:
response_data_model.__name__ = f'ResponseData[{type_string}]'
response_model = ResponseModel[response_data_model]
response_model.__name__ = f'Response[{type_string}]'
response_model._resource_type = type_string # type: ignore
return response_model
6 changes: 4 additions & 2 deletions robot-server/robot_server/service/routers/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
from robot_server.service.models.json_api.factory import \
generate_json_api_models
from robot_server.service.models.json_api.errors import ErrorResponse
from robot_server.service.models.json_api import ResourceTypes

# https://github.com/encode/starlette/blob/master/starlette/status.py
from starlette.status import HTTP_400_BAD_REQUEST, \
HTTP_422_UNPROCESSABLE_ENTITY

router = APIRouter()

ITEM_TYPE_NAME = "item"
ItemRequest, ItemResponse = generate_json_api_models(ITEM_TYPE_NAME, Item)
ITEM_TYPE = ResourceTypes.item
ItemRequest, ItemResponse = generate_json_api_models(ITEM_TYPE, Item)


@router.get("/items/{item_id}",
Expand Down
5 changes: 3 additions & 2 deletions robot-server/tests/service/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from uuid import uuid4

from robot_server.service.models.json_api.request import json_api_request
from robot_server.service.models.json_api import ResourceTypes


@dataclass
Expand All @@ -19,5 +20,5 @@ class ItemModel(BaseModel):
price: float


item_type_name = 'item'
ItemRequest = json_api_request(item_type_name, ItemModel)
item_type = ResourceTypes.item
ItemRequest = json_api_request(item_type, ItemModel)
5 changes: 3 additions & 2 deletions robot-server/tests/service/models/json_api/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ def test_transform_validation_error_to_json_api_errors():
'errors': [
{
'status': str(HTTP_422_UNPROCESSABLE_ENTITY),
'detail': "unexpected value; permitted: 'item'",
'detail': "value is not a valid enumeration member; permitted:"
" 'item'",
'source': {
'pointer': '/data/type'
},
'title': 'value_error.const'
'title': 'type_error.enum'
},
{
'status': str(HTTP_422_UNPROCESSABLE_ENTITY),
Expand Down
16 changes: 9 additions & 7 deletions robot-server/tests/service/models/json_api/test_factory.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
from typing import List
from typing_extensions import Literal

from robot_server.service.models.json_api.factory import \
generate_json_api_models
from robot_server.service.models.json_api.request import RequestModel, \
RequestDataModel
from robot_server.service.models.json_api.response import ResponseModel, \
ResponseDataModel
from robot_server.service.models.json_api import ResourceTypes
from tests.service.helpers import ItemModel

ITEM_TYPE = ResourceTypes.item


def test_json_api_model():
ItemRequest, ItemResponse = generate_json_api_models('item', ItemModel)
ItemRequest, ItemResponse = generate_json_api_models(ITEM_TYPE, ItemModel)
assert ItemRequest == RequestModel[
RequestDataModel[Literal['item'], ItemModel]
RequestDataModel[ItemModel]
]
assert ItemResponse == ResponseModel[
ResponseDataModel[Literal['item'], ItemModel]
ResponseDataModel[ItemModel]
]


def test_json_api_model__list_response():
ItemRequest, ItemResponse = generate_json_api_models(
'item',
ITEM_TYPE,
ItemModel,
list_response=True
)
assert ItemRequest == RequestModel[
RequestDataModel[Literal['item'], ItemModel]
RequestDataModel[ItemModel]
]
assert ItemResponse == ResponseModel[
List[ResponseDataModel[Literal['item'], ItemModel]]
List[ResponseDataModel[ItemModel]]
]
25 changes: 15 additions & 10 deletions robot-server/tests/service/models/json_api/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
from pydantic import ValidationError

from robot_server.service.models.json_api.request import json_api_request
from robot_server.service.models.json_api import ResourceTypes
from tests.service.helpers import ItemModel


ITEM_TYPE = ResourceTypes.item


def test_attributes_as_dict():
DictRequest = json_api_request('item', dict)
DictRequest = json_api_request(ITEM_TYPE, dict)
obj_to_validate = {
'data': {'type': 'item', 'attributes': {}}
}
Expand All @@ -22,7 +26,7 @@ def test_attributes_as_dict():


def test_attributes_as_item_model():
ItemRequest = json_api_request('item', ItemModel)
ItemRequest = json_api_request(ITEM_TYPE, ItemModel)
obj_to_validate = {
'data': {
'type': 'item',
Expand All @@ -39,7 +43,7 @@ def test_attributes_as_item_model():


def test_attributes_as_item_model__empty_dict():
ItemRequest = json_api_request('item', ItemModel)
ItemRequest = json_api_request(ITEM_TYPE, ItemModel)
obj_to_validate = {
'data': {
'type': 'item',
Expand Down Expand Up @@ -67,7 +71,7 @@ def test_attributes_as_item_model__empty_dict():


def test_type_invalid_string():
MyRequest = json_api_request('item', dict)
MyRequest = json_api_request(ITEM_TYPE, dict)
obj_to_validate = {
'data': {'type': 'not_an_item', 'attributes': {}}
}
Expand All @@ -77,15 +81,16 @@ def test_type_invalid_string():
assert e.value.errors() == [
{
'loc': ('data', 'type'),
'msg': "unexpected value; permitted: 'item'",
'type': 'value_error.const',
'ctx': {'given': 'not_an_item', 'permitted': ('item',)},
'msg': "value is not a valid enumeration member;"
" permitted: 'item'",
'type': 'type_error.enum',
'ctx': {'enum_values': [ITEM_TYPE]},
},
]


def test_attributes_required():
MyRequest = json_api_request('item', dict)
MyRequest = json_api_request(ITEM_TYPE, dict)
obj_to_validate = {
'data': {'type': 'item', 'attributes': None}
}
Expand All @@ -102,7 +107,7 @@ def test_attributes_required():


def test_data_required():
MyRequest = json_api_request('item', dict)
MyRequest = json_api_request(ITEM_TYPE, dict)
obj_to_validate = {
'data': None
}
Expand All @@ -119,7 +124,7 @@ def test_data_required():


def test_request_with_id():
MyRequest = json_api_request('item', dict)
MyRequest = json_api_request(ITEM_TYPE, dict)
obj_to_validate = {
'data': {
'type': 'item',
Expand Down
Loading