Skip to content
Open
Show file tree
Hide file tree
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
30 changes: 30 additions & 0 deletions src/SDK/Language/Python.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ public function getFiles(): array
'destination' => 'test/__init__.py',
'template' => 'python/test/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => 'test_async/__init__.py',
'template' => 'python/test_async/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ namespace | caseSnake}}/utils/deprecated.py',
Expand All @@ -166,6 +171,11 @@ public function getFiles(): array
'destination' => '{{ namespace | caseSnake}}/client.py',
'template' => 'python/package/client.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ namespace | caseSnake}}/async_client.py',
'template' => 'python/package/async_client.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ namespace | caseSnake}}/permission.py',
Expand Down Expand Up @@ -271,6 +281,11 @@ public function getFiles(): array
'destination' => 'test/services/__init__.py',
'template' => 'python/test/services/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => 'test_async/services/__init__.py',
'template' => 'python/test_async/services/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ namespace | caseSnake}}/encoders/value_class_encoder.py',
Expand All @@ -286,11 +301,26 @@ public function getFiles(): array
'destination' => '{{ namespace | caseSnake}}/services/{{service.name | caseSnake}}.py',
'template' => 'python/package/services/service.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ namespace | caseSnake}}/async_services/__init__.py',
'template' => 'python/package/async_services/__init__.py.twig',
],
[
'scope' => 'service',
'destination' => '{{ namespace | caseSnake}}/async_services/{{service.name | caseSnake}}.py',
'template' => 'python/package/async_services/service.py.twig',
],
[
'scope' => 'service',
'destination' => 'test/services/test_{{service.name | caseSnake}}.py',
'template' => 'python/test/services/test_service.py.twig',
],
[
'scope' => 'service',
'destination' => 'test_async/services/test_{{service.name | caseSnake}}.py',
'template' => 'python/test_async/services/test_service.py.twig',
],
[
'scope' => 'method',
'destination' => 'docs/examples/{{service.name | caseLower}}/{{(method | methodName) | caseKebab}}.md',
Expand Down
39 changes: 39 additions & 0 deletions templates/python/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ Or with [uv](https://docs.astral.sh/uv/):
```bash
uv add {{ language.params.pipPackage }}
```

## Async usage

The SDK ships a synchronous client, which is the default and needs no extra setup:

```python
from {{ namespace | caseSnake }}.client import Client
from {{ namespace | caseSnake }}.services.users import Users

client = Client()
users = Users(client)

user = users.create(user_id='unique()', email='email@example.com', password='password')
```

The asynchronous client is opt-in and ships with the `async` extra:

```bash
pip install {{ language.params.pipPackage }}[async]
```

```python
import asyncio

from {{ namespace | caseSnake }}.async_client import AsyncClient
from {{ namespace | caseSnake }}.async_services.users import AsyncUsers


async def main():
client = AsyncClient()
users = AsyncUsers(client)

user = await users.create(user_id='unique()', email='email@example.com', password='password')


asyncio.run(main())
```

Both clients talk to the same API and expose the same methods. The asynchronous ones are coroutines, so they have to be awaited.
{% if sdk.gettingStarted %}

{{ sdk.gettingStarted|trim|raw }}
Expand Down
2 changes: 1 addition & 1 deletion templates/python/base/requests/api.twig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
response = self.client.call(
response = {% if isAsync %}await {% endif %}self.client.call(
'{{ method.method.value | caseLower }}',
api_path,
{
Expand Down
2 changes: 1 addition & 1 deletion templates/python/base/requests/file.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
upload_id = {{ uploadIdParameter.name | escapeKeyword | caseSnake }}
{% endif %}

response = self.client.chunked_upload(
response = {% if isAsync %}await {% endif %}self.client.chunked_upload(
api_path,
{
{%~ for key, security in (method | securityHeaders) %}
Expand Down
173 changes: 173 additions & 0 deletions templates/python/base/service_class.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from ..service import Service
from urllib.parse import quote
from typing import Any, Dict, List, Optional, Union{% set hasGenericModel = false %}{% for method in (service | operations) %}{% if (method | responseModel) and (method | responseModel) != 'any' and (method | responseModel) | hasGenericType(spec) %}{% set hasGenericModel = true %}{% endif %}{% endfor %}{% if hasGenericModel %}, Type, TypeVar{% endif %}

from ..exception import AppwriteException
from {{ namespace | caseSnake }}.utils.deprecated import deprecated
{% set added = [] %}
{% for method in (service | operations) %}
{% for parameter in (method | parameters('all')) %}
{% if (parameter | schemaType) == 'file' and (parameter | schemaType) not in added %}
from ..input_file import InputFile
{% set added = added|merge(['InputFile']) %}
{% endif %}
{% if (parameter | enumValues) is not empty%}
{% if (parameter | enumName) not in added %}
{% set enumRef = parameter | getServiceEnumName(service, spec) %}
from ..enums.{{ (parameter | enumName) | caseSnake }} import {{ (parameter | enumName) | caseUcfirst }}{% if enumRef != ((parameter | enumName) | caseUcfirst) %} as {{ enumRef }}{% endif %}

{% set added = added|merge([(parameter | enumName)]) %}
{% endif %}
{% endif %}
{% set modelName = (parameter | schemaModel) %}
{% if modelName %}
{% if modelName not in added %}
{% if (modelName | caseUcfirst) == (service.name | caseUcfirst) %}
from ..models.{{ modelName | caseSnake }} import {{ modelName | caseUcfirst }} as {{ modelName | caseUcfirst }}Model
{% else %}
from ..models.{{ modelName | caseSnake }} import {{ modelName | caseUcfirst }}
{% endif %}
{% set added = added|merge([modelName]) %}
{% endif %}
{% endif %}
{% endfor %}
{% if (method | responseModel) and (method | responseModel) != 'any' %}
{% if (method | responseModel) not in added %}
{% if ((method | responseModel) | caseUcfirst) == (service.name | caseUcfirst) %}
from ..models.{{ (method | responseModel) | caseSnake }} import {{ (method | responseModel) | caseUcfirst }} as {{ (method | responseModel) | caseUcfirst }}Model
{% else %}
from ..models.{{ (method | responseModel) | caseSnake }} import {{ (method | responseModel) | caseUcfirst }}
{% endif %}
{% set added = added|merge([(method | responseModel)]) %}
{% endif %}
{% endif %}
{% if (method | responseModels)|length > 1 %}
{% for responseModel in (method | responseModels) %}
{% if responseModel and responseModel != 'any' and responseModel not in added %}
{% if (responseModel | caseUcfirst) == (service.name | caseUcfirst) %}
from ..models.{{ responseModel | caseSnake }} import {{ responseModel | caseUcfirst }} as {{ responseModel | caseUcfirst }}Model
{% else %}
from ..models.{{ responseModel | caseSnake }} import {{ responseModel | caseUcfirst }}
{% endif %}
{% set added = added|merge([responseModel]) %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% if hasGenericModel %}

T = TypeVar('T')
{% endif %}


class {% if isAsync %}Async{% endif %}{{ service.name | caseUcfirst }}(Service):

def __init__(self, client) -> None:
super({% if isAsync %}Async{% endif %}{{ service.name | caseUcfirst }}, self).__init__(client)
{% for method in (service | operations) %}
{% set methodNameSnake = (method | methodName) | caseSnake %}
{# Check if this method should be skipped (is deprecated and has a non-deprecated duplicate) #}
{% set shouldSkip = false %}
{% if method.deprecated %}
{% for otherMethod in (service | operations) %}
{% if not otherMethod.deprecated and ((otherMethod | methodName) | caseSnake) == methodNameSnake %}
{% set shouldSkip = true %}
{% endif %}
{% endfor %}
{% endif %}
{% if not shouldSkip %}
{% set validResponseModels = [] %}
{% if (method | responseModels)|length > 1 %}
{% for responseModel in (method | responseModels) %}
{% if responseModel and responseModel != 'any' %}
{% set validResponseModels = validResponseModels|merge([responseModel]) %}
{% endif %}
{% endfor %}
{% endif %}
{% set isGenericResponse = (method | responseModel) and (method | responseModel) != 'any' and (method | responseModel) | hasGenericType(spec) %}

{% if method.deprecated %}
{% if (method | appwrite('deprecated', {})['since'] ?? '') and (method | appwrite('deprecated', {})['replaceWith'] ?? '') %}
@deprecated("This API has been deprecated since {{ (method | appwrite('deprecated', {})['since'] ?? '') }}. Please use `{{ (method | appwrite('deprecated', {})['replaceWith'] ?? '') | caseSnakeExceptFirstDot }}` instead.")
{% else %}
@deprecated("This API has been deprecated.")
{% endif %}
{% endif %}
{% set returnType %}
{% if (method | methodType) == 'webAuth' %}
str
{% elseif (method | methodType) == 'location' %}
bytes
{% elseif (method | textResponse) %}
str
{% elseif validResponseModels|length > 1 %}
Union[{% for responseModel in validResponseModels %}{% if not loop.first %}, {% endif %}{% if (responseModel | caseUcfirst) == (service.name | caseUcfirst) %}{{ responseModel | caseUcfirst }}Model{% else %}{{ responseModel | caseUcfirst }}{% endif %}{% endfor %}]
{% elseif validResponseModels|length == 1 %}
{% if (validResponseModels[0] | caseUcfirst) == (service.name | caseUcfirst) %}{{ validResponseModels[0] | caseUcfirst }}Model{% else %}{{ validResponseModels[0] | caseUcfirst }}{% endif %}
{% elseif (method | responseModel) and (method | responseModel) != 'any' %}
{% if isGenericResponse %}{% if ((method | responseModel) | caseUcfirst) == (service.name | caseUcfirst) %}{{ (method | responseModel) | caseUcfirst }}Model{% else %}{{ (method | responseModel) | caseUcfirst }}{% endif %}[T]{% elseif ((method | responseModel) | caseUcfirst) == (service.name | caseUcfirst) %}{{ (method | responseModel) | caseUcfirst }}Model{% else %}{{ (method | responseModel) | caseUcfirst }}{% endif %}
{% else %}
Dict[str, Any]
{% endif %}
{% endset %}
{% if isAsync %}async {% endif %}def {{ (method | methodName) | caseSnake }}(
self,
{% for parameter in (method | parameters('all')) %} {{ parameter.name | escapeKeyword | caseSnake }}: {{ parameter | getServicePropertyType(service, spec) | raw }}{% if not parameter.required %} = None{% endif %},
{% endfor %}{% if 'multipart/form-data' in (method | consumes) %} on_progress=None,
{% endif %}{% if isGenericResponse %} model_type: Type[T] = dict,
{% endif %} ) -> {{ returnType | trim | formatModelFieldType | raw }}:
"""
{% if method.description %} {% autoescape false %}{{ method.description | formatDocstring(8) }}{% endautoescape %}

{% endif %}{% if (method | parameters('all'))|length > 0 or 'multipart/form-data' in (method | consumes) or isGenericResponse %}

{% if method.deprecated %}
{% if (method | appwrite('deprecated', {})['since'] ?? '') and (method | appwrite('deprecated', {})['replaceWith'] ?? '') %} .. deprecated::{{ (method | appwrite('deprecated', {})['since'] ?? '') }}
This API has been deprecated since {{ (method | appwrite('deprecated', {})['since'] ?? '') }}. Please use `{{ (method | appwrite('deprecated', {})['replaceWith'] ?? '') | caseSnakeExceptFirstDot }}` instead.
{% else %} .. deprecated::
This API has been deprecated.
{% endif %}
{% endif %} Parameters
----------
{% for parameter in (method | parameters('all')) %} {{ parameter.name | escapeKeyword | caseSnake }} : {{ parameter | getServicePropertyType(service, spec) | raw }}
{% autoescape false %}{{ parameter.description | formatDocstring(12) }}{% endautoescape %}

{% endfor %}{% if 'multipart/form-data' in (method | consumes) %} on_progress : callable, optional
Optional callback for upload progress

{% endif %}{% if isGenericResponse %} model_type : Type[T], optional
Pydantic model class for the user-defined data. Defaults to dict for backward compatibility.

{% endif %}{% endif %} Returns
-------
{% if (method | methodType) == 'webAuth' %} str
Authentication response as a string
{% elseif (method | methodType) == 'location' %} bytes
Response as bytes
{% elseif (method | textResponse) %} str
Response body as text
{% elseif validResponseModels|length > 1 %} {{ method | getResponseType(service.name) }}
API response as one of the typed response models
{% elseif validResponseModels|length == 1 %} {{ method | getResponseType(service.name) }}
API response as a typed Pydantic model
{% elseif (method | responseModel) and (method | responseModel) != 'any' %}{% if isGenericResponse %} {% if ((method | responseModel) | caseUcfirst) == (service.name | caseUcfirst) %}{{ (method | responseModel) | caseUcfirst }}Model{% else %}{{ (method | responseModel) | caseUcfirst }}{% endif %}[T]
{% else %} {{ method | getResponseType(service.name) }}
{% endif %} API response as a typed Pydantic model
{% else %} Dict[str, Any]
API response as a dictionary
{% endif %}

Raises
------
AppwriteException
If API request fails
"""

api_path = '{{ method.path }}'
{%~ for security in (method | securityQueries) %}
api_path += '{% if loop.first %}?{% else %}&{% endif %}{{ security.name }}=' + quote(str(self.client.get_config('{{ security.name | caseLower }}')), safe='')
{%~ endfor %}
{{ include('python/base/params.twig') }}
{% if 'multipart/form-data' in (method | consumes) %}
{{ include('python/base/requests/file.twig') }}{% else %}{{ include('python/base/requests/api.twig') }}{% endif %}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Loading