Skip to content
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

Async Support in Python SDK #453

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -41,8 +41,11 @@ env:
- SDK=PHP74
- SDK=PHP80
- SDK=Python38
- SDK=Python38Async
- SDK=Python39
- SDK=Python39Async
- SDK=Python310
- SDK=Python310Async
- SDK=Ruby27
- SDK=Ruby30
- SDK=Ruby31
28 changes: 28 additions & 0 deletions src/SDK/Language/Python.php
Original file line number Diff line number Diff line change
@@ -184,6 +184,34 @@ public function getFiles(): array
'destination' => '.travis.yml',
'template' => 'python/.travis.yml.twig',
],

/* Async */
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/aio/__init__.py',
'template' => 'python/package/aio/__init__.py.twig',
'minify' => false,
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/aio/client.py',
'template' => 'python/package/aio/client.py.twig',
'minify' => false,
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/aio/services/__init__.py',
'template' => 'python/package/aio/services/__init__.py.twig',
'minify' => false,
],
[
'scope' => 'service',
'destination' => '{{ spec.title | caseSnake}}/aio/services/{{service.name | caseSnake}}.py',
'template' => 'python/package/aio/services/service.py.twig',
'minify' => false,
],


];
}

1 change: 1 addition & 0 deletions templates/python/package/aio/__init__.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

141 changes: 141 additions & 0 deletions templates/python/package/aio/client.py.twig
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


1 change: 1 addition & 0 deletions templates/python/package/aio/services/__init__.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

83 changes: 83 additions & 0 deletions templates/python/package/aio/services/service.py.twig
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 %}
8 changes: 5 additions & 3 deletions templates/python/package/client.py.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import io
import requests
import httpx
import os
from .input_file import InputFile
from .exception import {{spec.title | caseUcfirst}}Exception
@@ -42,7 +42,7 @@ class Client:
return self
{% endfor %}

def call(self, method, path='', headers=None, params=None):
def call(self, method, path='', headers=None, params=None, timeout=None):
if headers is None:
headers = {}

@@ -73,7 +73,7 @@ class Client:
del data[key]
response = None
try:
response = requests.request( # call method dynamically https://stackoverflow.com/a/4246075/2299554
response = httpx.request(
method=method,
url=self._endpoint + path,
params=self.flatten(params, stringify=stringify),
@@ -82,6 +82,8 @@ class Client:
files=files,
headers=headers,
verify=(not self._self_signed),
follow_redirects=True,
timeout=timeout
)

response.raise_for_status()
2 changes: 1 addition & 1 deletion templates/python/requirements.txt.twig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
requests==2.28.1
httpx==0.22.0
2 changes: 1 addition & 1 deletion templates/python/setup.py.twig
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ setuptools.setup(
download_url='https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/archive/{{sdk.version}}.tar.gz',
# keywords = ['SOME', 'MEANINGFULL', 'KEYWORDS'],
install_requires=[
'requests',
'httpx',
],
classifiers=[
'Development Status :: 5 - Production/Stable',
38 changes: 38 additions & 0 deletions tests/Python310AsyncTest.php
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
];
}
4 changes: 4 additions & 0 deletions tests/Python310Test.php
Original file line number Diff line number Diff line change
@@ -2,6 +2,10 @@

namespace Tests;

/**
* @group python
* Tests python
*/
class Python310Test extends Base
{
protected string $sdkName = 'python';
Loading