Skip to content

Commit

Permalink
Merge pull request #26 from algoo/feature/input_output_files
Browse files Browse the repository at this point in the history
Feature/input output files
  • Loading branch information
buxx committed Nov 2, 2017
2 parents 7fecf1a + 303a59a commit 5bd357f
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 87 deletions.
22 changes: 22 additions & 0 deletions hapic/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from hapic.description import InputBodyDescription
from hapic.description import InputHeadersDescription
from hapic.description import InputFormsDescription
from hapic.description import InputFilesDescription
from hapic.description import OutputBodyDescription
from hapic.description import OutputFileDescription
from hapic.description import OutputHeadersDescription
from hapic.description import ErrorDescription
from hapic.exception import AlreadyDecoratedException
Expand Down Expand Up @@ -73,6 +75,16 @@ def input_forms(self, description: InputFormsDescription) -> None:
raise AlreadyDecoratedException()
self._description.input_forms = description

@property
def input_files(self) -> InputFilesDescription:
return self._description.input_files

@input_files.setter
def input_files(self, description: InputFilesDescription) -> None:
if self._description.input_files is not None:
raise AlreadyDecoratedException()
self._description.input_files = description

@property
def output_body(self) -> OutputBodyDescription:
return self._description.output_body
Expand All @@ -83,6 +95,16 @@ def output_body(self, description: OutputBodyDescription) -> None:
raise AlreadyDecoratedException()
self._description.output_body = description

@property
def output_file(self) -> OutputFileDescription:
return self._description.output_file

@output_file.setter
def output_file(self, description: OutputFileDescription) -> None:
if self._description.output_file is not None:
raise AlreadyDecoratedException()
self._description.output_file = description

@property
def output_headers(self) -> OutputHeadersDescription:
return self._description.output_headers
Expand Down
1 change: 1 addition & 0 deletions hapic/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ def __init__(self):
self.query = {}
self.headers = {}
self.forms = {}
self.files = {}
81 changes: 38 additions & 43 deletions hapic/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
parameters_data = self.get_parameters_data(request_parameters)
processed_data = self.processor.process(parameters_data)
return processed_data

def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
raise NotImplementedError()

def update_hapic_data(
Expand All @@ -163,9 +168,8 @@ def get_error_response(
self,
request_parameters: RequestParameters,
) -> typing.Any:
error = self.processor.get_validation_error(
request_parameters.body_parameters,
)
parameters_data = self.get_parameters_data(request_parameters)
error = self.processor.get_validation_error(parameters_data)
error_response = self.context.get_validation_error_response(
error,
http_code=self.error_http_code,
Expand Down Expand Up @@ -249,21 +253,25 @@ class OutputHeadersControllerWrapper(OutputControllerWrapper):
pass


class OutputFileControllerWrapper(ControllerWrapper):
def __init__(
self,
output_type: str,
default_http_code: HTTPStatus=HTTPStatus.OK,
) -> None:
self.output_type = output_type
self.default_http_code = default_http_code


class InputPathControllerWrapper(InputControllerWrapper):
def update_hapic_data(
self, hapic_data: HapicData,
processed_data: typing.Any,
) -> None:
hapic_data.path = processed_data

def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
processed_data = self.processor.process(
request_parameters.path_parameters,
)
return processed_data
def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.path_parameters


class InputQueryControllerWrapper(InputControllerWrapper):
Expand All @@ -273,14 +281,8 @@ def update_hapic_data(
) -> None:
hapic_data.query = processed_data

def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
processed_data = self.processor.process(
request_parameters.query_parameters,
)
return processed_data
def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.query_parameters


class InputBodyControllerWrapper(InputControllerWrapper):
Expand All @@ -290,14 +292,8 @@ def update_hapic_data(
) -> None:
hapic_data.body = processed_data

def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
processed_data = self.processor.process(
request_parameters.body_parameters,
)
return processed_data
def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.body_parameters


class InputHeadersControllerWrapper(InputControllerWrapper):
Expand All @@ -307,14 +303,8 @@ def update_hapic_data(
) -> None:
hapic_data.headers = processed_data

def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
processed_data = self.processor.process(
request_parameters.header_parameters,
)
return processed_data
def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.header_parameters


class InputFormsControllerWrapper(InputControllerWrapper):
Expand All @@ -324,14 +314,19 @@ def update_hapic_data(
) -> None:
hapic_data.forms = processed_data

def get_processed_data(
self,
request_parameters: RequestParameters,
) -> typing.Any:
processed_data = self.processor.process(
request_parameters.form_parameters,
)
return processed_data
def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.form_parameters


class InputFilesControllerWrapper(InputControllerWrapper):
def update_hapic_data(
self, hapic_data: HapicData,
processed_data: typing.Any,
) -> None:
hapic_data.files = processed_data

def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
return request_parameters.files_parameters


class ExceptionHandlerControllerWrapper(ControllerWrapper):
Expand Down
15 changes: 13 additions & 2 deletions hapic/description.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@ class InputHeadersDescription(Description):


class InputFormsDescription(Description):
def __init__(self, wrapper: 'ControllerWrapper') -> None:
self.wrapper = wrapper
pass


class InputFilesDescription(Description):
pass


class OutputBodyDescription(Description):
pass


class OutputFileDescription(Description):
pass


class OutputHeadersDescription(Description):
pass

Expand All @@ -51,7 +58,9 @@ def __init__(
input_body: InputBodyDescription=None,
input_headers: InputHeadersDescription=None,
input_forms: InputFormsDescription=None,
input_files: InputFilesDescription=None,
output_body: OutputBodyDescription=None,
output_file: OutputFileDescription=None,
output_headers: OutputHeadersDescription=None,
errors: typing.List[ErrorDescription]=None,
):
Expand All @@ -60,6 +69,8 @@ def __init__(
self.input_body = input_body
self.input_headers = input_headers
self.input_forms = input_forms
self.input_files = input_files
self.output_body = output_body
self.output_file = output_file
self.output_headers = output_headers
self.errors = errors or []
21 changes: 20 additions & 1 deletion hapic/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def find_bottle_route(
app: bottle.Bottle,
):
if not app.routes:
raise NoRoutesException('There is no routes in yout bottle app')
raise NoRoutesException('There is no routes in your bottle app')

reference = decorated_controller.reference
for route in app.routes:
Expand Down Expand Up @@ -74,6 +74,15 @@ def bottle_generate_operations(
}
}

if description.output_file:
method_operations.setdefault('produce', []).append(
description.output_file.wrapper.output_type
)
method_operations.setdefault('responses', {})\
[int(description.output_file.wrapper.default_http_code)] = {
'description': str(description.output_file.wrapper.default_http_code), # nopep8
}

if description.errors:
for error in description.errors:
schema_class = type(error.wrapper.schema)
Expand Down Expand Up @@ -109,6 +118,16 @@ def bottle_generate_operations(
'type': schema['type']
})

if description.input_files:
method_operations.setdefault('consume', []).append('multipart/form-data')
for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
method_operations.setdefault('parameters', []).append({
'in': 'formData',
'name': field_name,
'required': field.required,
'type': 'file',
})

operations = {
bottle_route.method.lower(): method_operations,
}
Expand Down
4 changes: 4 additions & 0 deletions hapic/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class HapicException(Exception):
pass


class ConfigurationException(HapicException):
pass


class WorkflowException(HapicException):
pass

Expand Down
45 changes: 45 additions & 0 deletions hapic/hapic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
from hapic.decorator import InputHeadersControllerWrapper
from hapic.decorator import InputPathControllerWrapper
from hapic.decorator import InputQueryControllerWrapper
from hapic.decorator import InputFilesControllerWrapper
from hapic.decorator import OutputBodyControllerWrapper
from hapic.decorator import OutputHeadersControllerWrapper
from hapic.decorator import OutputFileControllerWrapper
from hapic.description import InputBodyDescription
from hapic.description import ErrorDescription
from hapic.description import InputFormsDescription
from hapic.description import InputHeadersDescription
from hapic.description import InputPathDescription
from hapic.description import InputQueryDescription
from hapic.description import InputFilesDescription
from hapic.description import OutputBodyDescription
from hapic.description import OutputHeadersDescription
from hapic.description import OutputFileDescription
from hapic.doc import DocGenerator
from hapic.processor import ProcessorInterface
from hapic.processor import MarshmallowInputProcessor
from hapic.processor import MarshmallowInputFilesProcessor
from hapic.processor import MarshmallowOutputProcessor


Expand Down Expand Up @@ -148,6 +153,22 @@ def decorator(func):
return decoration.get_wrapper(func)
return decorator

# TODO BS 20171102: Think about possibilities to validate output ? (with mime type, or validator)
def output_file(
self,
output_type: str,
default_http_code: HTTPStatus = HTTPStatus.OK,
) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
decoration = OutputFileControllerWrapper(
output_type=output_type,
default_http_code=default_http_code,
)

def decorator(func):
self._buffer.output_file = OutputFileDescription(decoration)
return decoration.get_wrapper(func)
return decorator

def input_headers(
self,
schema: typing.Any,
Expand Down Expand Up @@ -268,6 +289,30 @@ def decorator(func):
return decoration.get_wrapper(func)
return decorator

def input_files(
self,
schema: typing.Any,
processor: ProcessorInterface=None,
context: ContextInterface=None,
error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
default_http_code: HTTPStatus = HTTPStatus.OK,
) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
processor = processor or MarshmallowInputFilesProcessor()
processor.schema = schema
context = context or self._context_getter

decoration = InputFilesControllerWrapper(
context=context,
processor=processor,
error_http_code=error_http_code,
default_http_code=default_http_code,
)

def decorator(func):
self._buffer.input_files = InputFilesDescription(decoration)
return decoration.get_wrapper(func)
return decorator

def handle_exception(
self,
handled_exception_class: typing.Type[Exception],
Expand Down

0 comments on commit 5bd357f

Please sign in to comment.