diff --git a/src/SDK/Language/Python.php b/src/SDK/Language/Python.php index 4bffda700..6c7f4437f 100644 --- a/src/SDK/Language/Python.php +++ b/src/SDK/Language/Python.php @@ -166,6 +166,12 @@ public function getFiles() 'template' => 'python/package/exception.py.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseSnake}}/input_file.py', + 'template' => 'python/package/input_file.py.twig', + 'minify' => false, + ], [ 'scope' => 'default', 'destination' => '{{ spec.title | caseSnake}}/service.py', diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 45bdcd0f7..ed83f9d4e 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -1,9 +1,12 @@ import io import requests +import os +from .input_file import InputFile from .exception import {{spec.title | caseUcfirst}}Exception class Client: def __init__(self): + self._chunk_size = 5*1024*1024 self._self_signed = False self._endpoint = '{{spec.endpoint}}' self._global_headers = { @@ -61,8 +64,8 @@ class Client: del headers['content-type'] for key in data.copy(): - if isinstance(data[key], io.BufferedIOBase): - files[key] = data[key] + if isinstance(data[key], InputFile): + files[key] = (data[key].name, data[key].file) del data[key] response = None try: @@ -95,6 +98,54 @@ class Client: else: raise {{spec.title | caseUcfirst}}Exception(e) + def chunked_upload( + self, + path, + headers = None, + params = None, + param_name = '', + on_progress = None, + ): + file_path = str(params[param_name]) + file_name = os.path.basename(file_path) + size = os.stat(file_path).st_size + + if size < self._chunk_size: + slice = open(file_path, 'rb').read() + params[param_name] = InputFile(file_path, file_name, slice) + return self.call( + 'post', + path, + headers, + params + ) + + input = open(file_path, 'rb') + offset = 0 + + while offset < size: + slice = input.read(self._chunk_size) or input.read(size - offset) + + params[param_name] = InputFile(file_path, file_name, slice) + headers["content-range"] = f'bytes {offset}-{min((offset + self._chunk_size) - 1, size)}/{size}' + + result = self.call( + 'post', + path, + headers, + params, + ) + + offset = offset + self._chunk_size + + if "$id" in result: + headers["x-{{ spec.title | caseLower }}-id"] = result["$id"] + + if on_progress is not None: + on_progress(min(offset, size)/size * 100) + + return result + def flatten(self, data, prefix=''): output = {} i = 0 diff --git a/templates/python/package/input_file.py.twig b/templates/python/package/input_file.py.twig new file mode 100644 index 000000000..4351d071a --- /dev/null +++ b/templates/python/package/input_file.py.twig @@ -0,0 +1,5 @@ +class InputFile: + def __init__(self, path, name, file): + self.path = path + self.name = name + self.file = file \ No newline at end of file diff --git a/templates/python/package/services/service.py.twig b/templates/python/package/services/service.py.twig index 2b07e2e2a..f8605512e 100644 --- a/templates/python/package/services/service.py.twig +++ b/templates/python/package/services/service.py.twig @@ -7,7 +7,7 @@ class {{ service.name | caseUcfirst }}(Service): super({{ service.name | caseUcfirst }}, self).__init__(client) {% for method in service.methods %} - def {{ method.name | caseSnake }}(self{% if method.parameters.all|length > 0 %}, {% endif %}{% for parameter in method.parameters.all %}{{ parameter.name | escapeKeyword | caseSnake }}{% if not parameter.required %} = None{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}): + def {{ method.name | caseSnake }}(self{% if method.parameters.all|length > 0 %}, {% endif %}{% for parameter in method.parameters.all %}{{ parameter.name | escapeKeyword | caseSnake }}{% if not parameter.required %} = None{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, on_progress = None{% endif %}): {% if method.title %} """{{ method.title }}""" @@ -40,6 +40,22 @@ class {{ service.name | caseUcfirst }}(Service): params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% endfor %} +{% if 'multipart/form-data' in method.consumes %} +{% for parameter in method.parameters.all %} +{% if parameter.type == 'file' %} + param_name = '{{ parameter.name }}' + +{% endif %} +{% endfor %} + return self.client.chunked_upload(path, { +{% for parameter in method.parameters.header %} + '{{ parameter.name }}': {{ parameter.name | escapeKeyword | caseSnake }}, +{% endfor %} +{% for key, header in method.headers %} + '{{ key }}': '{{ header }}', +{% endfor %} + }, params, param_name, on_progress) +{% else %} return self.client.call('{{ method.method | caseLower }}', path, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': {{ parameter.name | escapeKeyword | caseSnake }}, @@ -48,4 +64,5 @@ class {{ service.name | caseUcfirst }}(Service): '{{ key }}': '{{ header }}', {% endfor %} }, params) +{% endif %} {% endfor %} diff --git a/tests/SDKTest.php b/tests/SDKTest.php index 4a47cf7ec..d21049a94 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -289,18 +289,19 @@ class SDKTest extends TestCase 'build' => [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.8 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.10-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ], 'envs' => [ - 'python-3.9' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.8-alpine python tests/sdks/python/test.py', - 'python-3.8' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.7-alpine python tests/sdks/python/test.py', + 'python-3.10' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.10-alpine python tests/sdks/python/test.py', + 'python-3.9' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.9-alpine python tests/sdks/python/test.py', + 'python-3.8' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.8-alpine python tests/sdks/python/test.py', 'python-3.7' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.7-alpine python tests/sdks/python/test.py', - 'python-3.6' => 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.7-alpine python tests/sdks/python/test.py', ], 'expectedOutput' => [ ...FOO_RESPONSES, ...BAR_RESPONSES, ...GENERAL_RESPONSES, + 'POST:/v1/mock/tests/general/upload:passed', ...EXCEPTION_RESPONSES, ], ], diff --git a/tests/languages/python/tests.py b/tests/languages/python/tests.py index b741283cc..de8baa976 100644 --- a/tests/languages/python/tests.py +++ b/tests/languages/python/tests.py @@ -55,7 +55,10 @@ response = general.redirect() print(response['result']) -response = general.upload('string', 123, ['string in array'], open('./tests/resources/file.png', 'rb')) +response = general.upload('string', 123, ['string in array'], './tests/resources/file.png') +print(response['result']) + +response = general.upload('string', 123, ['string in array'], './tests/resources/large_file.mp4') print(response['result']) try: diff --git a/tests/resources/large_file.mp4 b/tests/resources/large_file.mp4 new file mode 100644 index 000000000..5a275ab90 Binary files /dev/null and b/tests/resources/large_file.mp4 differ