diff --git a/hapic/data.py b/hapic/data.py index cd984b9..588bd9e 100644 --- a/hapic/data.py +++ b/hapic/data.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import typing import urllib.parse +from datetime import datetime class HapicData(object): @@ -20,6 +21,8 @@ def __init__( file_object: typing.Any = None, mimetype: typing.Any = None, filename: str = None, + content_length: int = None, + last_modified: datetime = None, as_attachment: bool = False, ): self.file_path = file_path @@ -27,6 +30,8 @@ def __init__( self.filename = filename self.mimetype = mimetype self.as_attachment = as_attachment + self.content_length = content_length + self.last_modified = last_modified def get_content_disposition_header_value(self) -> str: disposition = "inline" diff --git a/hapic/ext/pyramid/context.py b/hapic/ext/pyramid/context.py index 38b362f..e6fdf4c 100644 --- a/hapic/ext/pyramid/context.py +++ b/hapic/ext/pyramid/context.py @@ -111,6 +111,11 @@ def get_file_response(self, file_response: HapicFile, http_code: int): response.content_type = file_response.mimetype response.app_iter = FileIter(file_response.file_object) + if file_response.content_length: + response.content_length = file_response.content_length + if file_response.last_modified: + response.last_modified = file_response.last_modified + response.status_code = http_code response.content_disposition = ( file_response.get_content_disposition_header_value() diff --git a/hapic/processor/main.py b/hapic/processor/main.py index f055359..5aab8ad 100644 --- a/hapic/processor/main.py +++ b/hapic/processor/main.py @@ -1,6 +1,7 @@ import abc import os import typing +from datetime import datetime from apispec import BasePlugin from multidict.__init__ import MultiDict @@ -213,6 +214,12 @@ def _get_ouput_file_validation_error_message( error_message = "File path is not correct, file do not exist" elif data.file_object and not data.mimetype: error_message = "File object should have explicit mimetype" + elif data.content_length and not isinstance(data.content_length, int): + error_message = "Content length should be integer" + elif data.content_length and data.content_length < 0: + error_message = "Content length should positive or null integer" + elif data.last_modified and not isinstance(data.last_modified, datetime): + error_message = "Last-modified value should be datetime type" return error_message # NOTE BS 2018-12-20: the decorators order is not semantically right, diff --git a/tests/unit/test_processor.py b/tests/unit/test_processor.py index 35926d5..0cd9795 100644 --- a/tests/unit/test_processor.py +++ b/tests/unit/test_processor.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from datetime import datetime from io import BytesIO import os @@ -26,7 +27,7 @@ def test_unit_file_output_processor_ok__process_success_filepath(self): data = processor.dump_output_file(tested_data) assert data == tested_data - def test_unit_file_output_processor_ok__process_success_fileobject(self): + def test_unit_file_output_processor_ok__process_success_fileobject_as_stream(self): processor = MarshmallowProcessor() file = BytesIO() image = Image.new("RGBA", size=(1000, 1000), color=(0, 0, 0)) @@ -37,6 +38,50 @@ def test_unit_file_output_processor_ok__process_success_fileobject(self): data = processor.dump_output_file(tested_data) assert data == tested_data + def test_unit_file_output_processor_ok__process_success_fileobject_as_sized_file(self): + processor = MarshmallowProcessor() + file = BytesIO() + image = Image.new("RGBA", size=(1000, 1000), color=(0, 0, 0)) + image.save(file, "png") + file.name = "test_image.png" + file.seek(0) + tested_data = HapicFile(file_object=file, mimetype="image/png", content_length=file.getbuffer().nbytes, last_modified=datetime.utcnow()) + data = processor.dump_output_file(tested_data) + assert data == tested_data + + def test_unit_file_output_processor__err__bad_last_modified_type(self): + processor = MarshmallowProcessor() + file = BytesIO() + image = Image.new("RGBA", size=(1000, 1000), color=(0, 0, 0)) + image.save(file, "png") + file.name = "test_image.png" + file.seek(0) + tested_data = HapicFile(file_object=file, mimetype="image/png", last_modified='uncorrect_type') + with pytest.raises(ValidationException): + data = processor.dump_output_file(tested_data) + + def test_unit_file_output_processor__err__bad_content_length_type(self): + processor = MarshmallowProcessor() + file = BytesIO() + image = Image.new("RGBA", size=(1000, 1000), color=(0, 0, 0)) + image.save(file, "png") + file.name = "test_image.png" + file.seek(0) + tested_data = HapicFile(file_object=file, mimetype="image/png", content_length='uncorrect_type') + with pytest.raises(ValidationException): + data = processor.dump_output_file(tested_data) + + def test_unit_file_output_processor__err__negative_content_length(self): + processor = MarshmallowProcessor() + file = BytesIO() + image = Image.new("RGBA", size=(1000, 1000), color=(0, 0, 0)) + image.save(file, "png") + file.name = "test_image.png" + file.seek(0) + tested_data = HapicFile(file_object=file, mimetype="image/png", content_length=-1) + with pytest.raises(ValidationException): + data = processor.dump_output_file(tested_data) + def test_unit_file_output_processor_err__wrong_type(self): processor = MarshmallowProcessor() file = BytesIO()