-
Notifications
You must be signed in to change notification settings - Fork 181
Async Support in Python SDK #453
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
Open
ms7m
wants to merge
17
commits into
appwrite:master
Choose a base branch
from
ms7m:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
5275f5c
feat: update python.php to create aio submodule
ms7m 668d725
fix: use httpx for non-sync client
ms7m f8f6501
chore: lock httpx dependency to 0.22.0
ms7m 5211586
chore: add tests for async python
ms7m 9d19d8e
fix: add await when calling chunked upload
ms7m 7420c95
fix: set follow_redirects to True and move verify to AsyncClient arg
ms7m a58e917
fix: add follow_redirects arg to httpx.request
ms7m b5cf913
fix: exclude duplicate modules in aio
ms7m fdb07c4
fix: import path for service and exceptions for aio
ms7m 4214ba8
feat: Create AsyncClient (inheritied from Client)
ms7m 4005f4f
fix: remove unneeded templates in aio module
ms7m fb5ad46
fix(ci): add Python38Async test
ms7m b0c0588
fix: tests for python aio submodule
ms7m a55374c
Merge branch 'master' into master
ms7m 1de8140
fix: add timeout parameter to .call (default to None)
ms7m 726f7b5
fix: use latest python tests for async
ms7m 27d3210
fix: phpcs formatting issue with tests
ms7m File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import io | ||
import httpx | ||
import os | ||
from ..input_file import InputFile | ||
from ..exception import {{spec.title | caseUcfirst}}Exception | ||
from ..client import Client | ||
|
||
class AsyncClient(Client): | ||
|
||
async def call(self, method, path='', headers=None, params=None, timeout=None): | ||
if headers is None: | ||
headers = {} | ||
|
||
if params is None: | ||
params = {} | ||
|
||
data = {} | ||
json = {} | ||
files = {} | ||
stringify = False | ||
|
||
headers = {**self._global_headers, **headers} | ||
|
||
if method != 'get': | ||
data = params | ||
params = {} | ||
|
||
if headers['content-type'].startswith('application/json'): | ||
json = data | ||
data = {} | ||
|
||
if headers['content-type'].startswith('multipart/form-data'): | ||
del headers['content-type'] | ||
stringify = True | ||
for key in data.copy(): | ||
if isinstance(data[key], InputFile): | ||
files[key] = (data[key].name, data[key].file) | ||
del data[key] | ||
response = None | ||
try: | ||
async with httpx.AsyncClient(verify=(not self._self_signed), follow_redirects=True) as client: | ||
response = await client.request( | ||
method=method, | ||
url=self._endpoint + path, | ||
params=self.flatten(params, stringify=stringify), | ||
data=self.flatten(data), | ||
json=json, | ||
files=files, | ||
headers=headers, | ||
timeout=timeout | ||
) | ||
|
||
response.raise_for_status() | ||
|
||
content_type = response.headers['Content-Type'] | ||
|
||
if content_type.startswith('application/json'): | ||
return response.json() | ||
|
||
return response._content | ||
except Exception as e: | ||
if response != None: | ||
content_type = response.headers['Content-Type'] | ||
if content_type.startswith('application/json'): | ||
raise {{spec.title | caseUcfirst}}Exception(response.json()['message'], response.status_code, response.json().get('type'), response.json()) | ||
else: | ||
raise {{spec.title | caseUcfirst}}Exception(response.text, response.status_code) | ||
else: | ||
raise {{spec.title | caseUcfirst}}Exception(e) | ||
|
||
async def chunked_upload( | ||
self, | ||
path, | ||
headers = None, | ||
params = None, | ||
param_name = '', | ||
on_progress = None, | ||
upload_id = '', | ||
): | ||
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 await self.call( | ||
'post', | ||
path, | ||
headers, | ||
params | ||
) | ||
|
||
input = open(file_path, 'rb') | ||
offset = 0 | ||
counter = 0 | ||
|
||
if upload_id != 'unique()': | ||
try: | ||
result = await self.call('get', path + '/' + upload_id, headers) | ||
counter = result['chunksUploaded'] | ||
except: | ||
pass | ||
|
||
if counter > 0: | ||
offset = counter * self._chunk_size | ||
input.seek(offset) | ||
|
||
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 = await 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: | ||
end = min((((counter * self._chunk_size) + self._chunk_size) - 1), size) | ||
on_progress({ | ||
"$id": result["$id"], | ||
"progress": min(offset, size)/size * 100, | ||
"sizeUploaded": end+1, | ||
"chunksTotal": result["chunksTotal"], | ||
"chunksUploaded": result["chunksUploaded"], | ||
}) | ||
|
||
counter = counter + 1 | ||
|
||
return result | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from ...service import Service | ||
from ...exception import AppwriteException | ||
|
||
class {{ service.name | caseUcfirst }}(Service): | ||
|
||
def __init__(self, client): | ||
super({{ service.name | caseUcfirst }}, self).__init__(client) | ||
{% for method in service.methods %} | ||
|
||
async 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 }}""" | ||
|
||
{% endif %} | ||
{% for parameter in method.parameters.all %} | ||
{% if parameter.required %} | ||
if {{ parameter.name | escapeKeyword | caseSnake }} is None: | ||
raise {{spec.title | caseUcfirst}}Exception('Missing required parameter: "{{ parameter.name | escapeKeyword | caseSnake }}"') | ||
|
||
{% endif %} | ||
{% endfor %} | ||
params = {} | ||
path = '{{ method.path }}' | ||
{% for parameter in method.parameters.path %} | ||
path = path.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | escapeKeyword | caseSnake }}) | ||
{% endfor %} | ||
|
||
{% for parameter in method.parameters.query %} | ||
if {{ parameter.name | escapeKeyword | caseSnake }} is not None: | ||
params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} | ||
|
||
{% endfor %} | ||
{% for parameter in method.parameters.body %} | ||
if {{ parameter.name | escapeKeyword | caseSnake }} is not None: | ||
{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} | ||
params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} | ||
{% else %} | ||
params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} | ||
{% endif %} | ||
{% endfor %} | ||
{% for parameter in method.parameters.formData %} | ||
if {{ parameter.name | escapeKeyword | caseSnake }} is not None: | ||
{% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} | ||
params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} | ||
{% else %} | ||
params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} | ||
{% endif %} | ||
|
||
{% endfor %} | ||
{% if 'multipart/form-data' in method.consumes %} | ||
{% for parameter in method.parameters.all %} | ||
{% if parameter.type == 'file' %} | ||
param_name = '{{ parameter.name }}' | ||
|
||
{% endif %} | ||
{% endfor %} | ||
|
||
upload_id = '' | ||
{% for parameter in method.parameters.all %} | ||
{% if parameter.isUploadID %} | ||
upload_id = {{ parameter.name | escapeKeyword | caseSnake }} | ||
{% endif %} | ||
{% endfor %} | ||
|
||
return await 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, upload_id) | ||
{% else %} | ||
return await self.client.call('{{ method.method | caseLower }}', path, { | ||
{% for parameter in method.parameters.header %} | ||
'{{ parameter.name }}': {{ parameter.name | escapeKeyword | caseSnake }}, | ||
{% endfor %} | ||
{% for key, header in method.headers %} | ||
'{{ key }}': '{{ header }}', | ||
{% endfor %} | ||
}, params) | ||
{% endif %} | ||
{% endfor %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
requests==2.28.1 | ||
httpx==0.22.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
namespace Tests; | ||
|
||
/** | ||
* @group asyncPython | ||
* Tests python | ||
*/ | ||
class Python310AsyncTest extends Base | ||
{ | ||
protected string $sdkName = 'python'; | ||
protected string $sdkPlatform = 'server'; | ||
protected string $sdkLanguage = 'python'; | ||
protected string $version = '0.0.1'; | ||
|
||
protected string $language = 'python'; | ||
protected string $class = 'Appwrite\SDK\Language\Python'; | ||
protected array $build = [ | ||
'cp tests/languages/python/tests_async.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.10 pip install -r tests/sdks/python/requirements.txt --upgrade', | ||
]; | ||
protected string $command = | ||
'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'; | ||
|
||
protected array $expectedOutput = [ | ||
...Base::FOO_RESPONSES, | ||
...Base::BAR_RESPONSES, | ||
...Base::GENERAL_RESPONSES, | ||
...Base::LARGE_FILE_RESPONSES, | ||
...Base::LARGE_FILE_RESPONSES, | ||
...Base::LARGE_FILE_RESPONSES, | ||
...Base::EXCEPTION_RESPONSES, | ||
...Base::QUERY_HELPER_RESPONSES, | ||
...Base::PERMISSION_HELPER_RESPONSES, | ||
...Base::ID_HELPER_RESPONSES | ||
]; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.