Skip to content
Open
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
Empty file added example/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion example/fake_api/aiohttp_serpyco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding: utf-8
from datetime import datetime
from datetime import timezone
import json
import time

Expand Down Expand Up @@ -36,7 +37,7 @@ async def about(self, request):
General information about this API.
"""
return AboutResponseSchema(
version="1.2.3", datetime=datetime(2017, 12, 7, 10, 55, 8, 488996)
version="1.2.3", datetime=datetime(2017, 12, 7, 10, 55, 8, 488996, timezone.utc)
)

@hapic.with_api_doc()
Expand Down
3 changes: 2 additions & 1 deletion example/fake_api/bottle_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from datetime import timezone
import json
import time

Expand Down Expand Up @@ -27,7 +28,7 @@ def about(self):
"""
General information about this API.
"""
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996)}
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996, timezone.utc)}

@hapic.with_api_doc()
@hapic.output_body(ListsUserSchema())
Expand Down
3 changes: 2 additions & 1 deletion example/fake_api/flask_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from datetime import timezone
import json
import time

Expand All @@ -26,7 +27,7 @@ def about(self):
"""
General information about this API.
"""
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996)}
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996, timezone.utc)}

@hapic.with_api_doc()
@hapic.output_body(ListsUserSchema())
Expand Down
3 changes: 2 additions & 1 deletion example/fake_api/pyramid_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from datetime import timezone
import json
import time
from wsgiref.simple_server import make_server
Expand Down Expand Up @@ -27,7 +28,7 @@ def about(self, context, request):
"""
General information about this API.
"""
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996)}
return {"version": "1.2.3", "datetime": datetime(2017, 12, 7, 10, 55, 8, 488996, timezone.utc)}

@hapic.with_api_doc()
@hapic.output_body(ListsUserSchema())
Expand Down
2 changes: 1 addition & 1 deletion example/usermanagement/schema_marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class UserDigestSchema(marshmallow.Schema):
"""User representation for listing"""

id = marshmallow.fields.Int(required=True)
display_name = marshmallow.fields.String(required=False, default="")
display_name = marshmallow.fields.String(required=False, dump_default="")


class UserAvatarSchema(marshmallow.Schema):
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_aiohttp_marshmallow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_aiohttp_marshmallow
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_aiohttp_serpyco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_aiohttp_serpyco
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_bottle_marshmallow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_bottle_marshmallow
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_bottle_serpyco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_bottle_serpyco
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_flask_marshmallow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_flask_marshmallow
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_flask_serpyco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_flask_serpyco
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_pyramid_marshmallow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_pyramid_marshmallow
#
from datetime import datetime
import json
import time
Expand Down
4 changes: 3 additions & 1 deletion example/usermanagement/serve_pyramid_serpyco.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

#
# Run this example with python3 -m example.usermanagement.serve_pyramid_serpyco
#
from datetime import datetime
import json
import time
Expand Down
9 changes: 8 additions & 1 deletion hapic/error/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,24 @@ def build_from_exception(self, exception: Exception, include_traceback: bool = F
if not message:
message = type(exception).__name__

code = getattr(exception, "error_code", None)

details = {"error_detail": getattr(exception, "error_detail", {})}
if include_traceback:
details["traceback"] = traceback.format_exc()

return {"message": message, "details": details, "code": None}

return {"message": message, "details": details, "code": code}

def build_from_validation_error(self, error: ProcessValidationError) -> dict:
"""
See hapic.error.ErrorBuilderInterface#build_from_validation_error
docstring
"""
code = getattr(error, "error_code", None)
if not code:
code = getattr(error.original_exception, "error_code", None)

return {"message": error.message, "details": error.details, "code": None}

@abc.abstractmethod
Expand Down
4 changes: 2 additions & 2 deletions hapic/error/marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

class DefaultErrorSchema(marshmallow.Schema):
message = marshmallow.fields.String(required=True)
details = marshmallow.fields.Dict(required=False, missing={})
code = marshmallow.fields.Raw(missing=None)
details = marshmallow.fields.Dict(required=False, load_default={})
code = marshmallow.fields.Raw(load_default=None)


# FIXME BS 2018-12-06: Marshmallow is used as default by hapic. But
Expand Down
1 change: 1 addition & 0 deletions hapic/hapic.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ def input_headers(
processor_factory = self._get_processor_factory(schema, processor)
context = context or self._context_getter

# TODO - D.A - support async mode for (at least) aiohttp - see #76
decoration = InputHeadersControllerWrapper(
context=context,
processor_factory=processor_factory,
Expand Down
109 changes: 48 additions & 61 deletions hapic/processor/marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from apispec_marshmallow_advanced.common import generate_schema_name
from apispec_marshmallow_advanced.common import schema_class_resolver as schema_class_resolver_

from marshmallow import ValidationError as MarshmallowValidationError

from hapic.doc.schema import SchemaUsage
from hapic.error.main import ErrorBuilderInterface
from hapic.error.marshmallow import MarshmallowDefaultErrorBuilder
Expand Down Expand Up @@ -56,7 +58,7 @@ def clean_data(self, data: typing.Any) -> dict:
:param data:
:return:
"""
# Fixes #22: Schemas make not validation if None is given
# Fixes #22: Schemas make no validation if None is given
if data is None:
return {}
return data
Expand All @@ -68,11 +70,12 @@ def get_input_validation_error(self, data_to_validate: typing.Any) -> ProcessVal
:return: ProcessValidationError instance for given data
"""
clean_data = self.clean_data(data_to_validate)
marshmallow_errors = self.schema.load(clean_data).errors

return ProcessValidationError(
message="Validation error of input data", details=marshmallow_errors
)
try:
data = self.schema.load(clean_data)
except MarshmallowValidationError as error:
return ProcessValidationError(
message="Validation error of input data", details=error.messages
)

def get_input_files_validation_error(
self, data_to_validate: typing.Any
Expand All @@ -83,12 +86,14 @@ def get_input_files_validation_error(
:return: ProcessValidationError instance for given data files
"""
clean_data = self.clean_data(data_to_validate)
unmarshall = self.schema.load(clean_data)
errors = unmarshall.errors
additional_errors = self._get_input_files_errors(unmarshall.data)
errors.update(additional_errors)
try:
unmarshall = self.schema.load(clean_data)
except MarshmallowValidationError as error:
return ProcessValidationError(
message="Validation error of input files data", details=error.messages
)

return ProcessValidationError(message="Validation error of input data", details=errors)
return None

def get_output_validation_error(self, data_to_validate: typing.Any) -> ProcessValidationError:
"""
Expand All @@ -97,10 +102,11 @@ def get_output_validation_error(self, data_to_validate: typing.Any) -> ProcessVa
:return: ProcessValidationError instance for given output data
"""
clean_data = self.clean_data(data_to_validate)
dump_data = self.schema.dump(clean_data).data
errors = self.schema.load(dump_data).errors

return ProcessValidationError(message="Validation error of output data", details=errors)
try:
self.schema.load(clean_data)
except MarshmallowValidationError as error:
return ProcessValidationError(message="Validation error of output data", details=error.messages)
return None

def get_output_file_validation_error(
self, data_to_validate: typing.Any
Expand All @@ -126,11 +132,15 @@ def load(self, data: typing.Any) -> typing.Any:
:return: updated data (like with default values)
"""
clean_data = self.clean_data(data)
unmarshall = self.schema.load(clean_data)
if unmarshall.errors:
raise ValidationException("Error when loading: {}".format(str(unmarshall.errors)))
unmarshall = None
try:
unmarshall = self.schema.load(clean_data)
except MarshmallowValidationError as error:
raise ValidationException("Error when loading: {}".format(str(error.messages)))

return unmarshall


return unmarshall.data

def dump(self, data: typing.Any) -> typing.Any:
"""
Expand All @@ -140,14 +150,16 @@ def dump(self, data: typing.Any) -> typing.Any:
:return: dumped data
"""
clean_data = self.clean_data(data)
dump_data = self.schema.dump(clean_data).data
try:
serialized = self.schema.dump(clean_data)
# TODO - make this optionnal as it slows down the processing
# This could be done by defining a strict hapic mode
self.schema.load(serialized)
except MarshmallowValidationError as err:
raise ValidationException("Error when dumping: {}".format(str(err.messages)))
else:
return serialized

# Re-validate with dumped data
errors = self.schema.load(dump_data).errors
if errors:
raise ValidationException("Error when dumping: {}".format(str(errors)))

return dump_data

def load_files_input(self, input_data: typing.Any) -> typing.Any:
"""
Expand All @@ -156,36 +168,13 @@ def load_files_input(self, input_data: typing.Any) -> typing.Any:
:return:
"""
clean_data = self.clean_data(input_data)
unmarshall = self.schema.load(clean_data)
additional_errors = self._get_input_files_errors(unmarshall.data)

if unmarshall.errors or additional_errors:
try:
return self.schema.load(clean_data)
except MarshmallowValidationError as error:
raise OutputValidationException(
"Error when validate ouput: {}".format(
", ".join([str(unmarshall.errors), str(additional_errors)])
)
"Error when validate ouput: {}".format(", ".join(error.messages))
)

return unmarshall.data

def _get_input_files_errors(self, validated_data: dict) -> typing.Dict[str, str]:
"""
Additional check of data
:param validated_data: previously validated data by marshmallow schema
:return: list of error if any
"""
errors = {}

for field_name, field in self.schema.fields.items():
# Currenlty just check if value not empty
# TODO BS 20171102: Think about case where test file content is
# more complicated
if field.required and (
field_name not in validated_data or not validated_data[field_name]
):
errors.setdefault(field_name, []).append("Missing data for required field")

return errors

def dump_output(self, output_data: typing.Any) -> typing.Union[typing.Dict, typing.List]:
"""
Expand All @@ -194,14 +183,12 @@ def dump_output(self, output_data: typing.Any) -> typing.Union[typing.Dict, typi
:return: given data
"""
clean_data = self.clean_data(output_data)
dump_data = self.schema.dump(clean_data).data

# Validate
errors = self.schema.load(dump_data).errors
if errors:
raise OutputValidationException("Error when validate input: {}".format(str(errors)))

return dump_data
try:
self.schema.load(clean_data)
except MarshmallowValidationError as error:
raise OutputValidationException("Error when validate input: {}".format(str(error.messages)))
else:
return self.schema.dump(clean_data)

def dump_output_file(self, output_file: typing.Any) -> typing.Any:
"""
Expand Down
2 changes: 1 addition & 1 deletion hapic/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class LowercaseKeysDict(dict):
"""
Like a dict but try to use lowercase version of given keys.
Must give string lowercase key to ths dict when fill it.
Must give string lowercase key to the dict when fill it.
"""

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[tool.black]
line-length = 100
exclude = '/(\..*)/'
[tool.pytest.ini_options]
asyncio_mode = "auto"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"pyyaml",
]
marshmallow_require = [
"marshmallow==2.21.0",
"marshmallow==3.24.2",
"apispec_marshmallow_advanced==0.4",
]
serpyco_require = [
Expand Down
3 changes: 0 additions & 3 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,3 @@ class Base(object):
pass


serpyco_compatible_python = pytest.mark.skipif(
sys.version_info < (3, 6), reason="serpyco dataclasses required python>3.6"
)
Loading