Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/sdk-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,9 @@ jobs:
composer test
;;
python)
pip install -e .
pip install -e .[test]
python -m compileall appwrite/
python -m unittest
;;
ruby)
bundle install
Expand Down
81 changes: 81 additions & 0 deletions src/SDK/Language/Python.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ public function getFiles(): array
'destination' => '{{ spec.title | caseSnake}}/__init__.py',
'template' => 'python/package/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/__init__.py',
'template' => 'python/test/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/utils/deprecated.py',
Expand All @@ -165,26 +170,51 @@ public function getFiles(): array
'destination' => '{{ spec.title | caseSnake}}/permission.py',
'template' => 'python/package/permission.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/test_permission.py',
'template' => 'python/test/test_permission.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/role.py',
'template' => 'python/package/role.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/test_role.py',
'template' => 'python/test/test_role.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/id.py',
'template' => 'python/package/id.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/test_id.py',
'template' => 'python/test/test_id.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/query.py',
'template' => 'python/package/query.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/test_query.py',
'template' => 'python/test/test_query.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/operator.py',
'template' => 'python/package/operator.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/test_operator.py',
'template' => 'python/test/test_operator.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/exception.py',
Expand Down Expand Up @@ -225,6 +255,11 @@ public function getFiles(): array
'destination' => '{{ spec.title | caseSnake}}/enums/__init__.py',
'template' => 'python/package/services/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => 'test/services/__init__.py',
'template' => 'python/test/services/__init__.py.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseSnake}}/encoders/value_class_encoder.py',
Expand All @@ -240,6 +275,11 @@ public function getFiles(): array
'destination' => '{{ spec.title | caseSnake}}/services/{{service.name | caseSnake}}.py',
'template' => 'python/package/services/service.py.twig',
],
[
'scope' => 'service',
'destination' => 'test/services/test_{{service.name | caseSnake}}.py',
'template' => 'python/test/services/test_service.py.twig',
],
[
'scope' => 'method',
'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseKebab}}.md',
Expand Down Expand Up @@ -704,6 +744,41 @@ protected function hasGenericType(?string $model, array $spec): bool
return false;
}

/**
* Creates an example for a response model with the given name
*
* @param string $model
* @param array $spec
* @return string
*/
protected function getResponseModelExample(?string $model, array $spec): mixed
{
if (!$model) {
return (object) [];
}

$modelDef = $spec['definitions'][$model];

$result = [];
foreach ($modelDef['properties'] ?? [] as $property) {
if (!$property['required']) {
continue;
}

$result[$property['name']] = match ($property['type']) {
'object' => (array_key_exists('sub_schema', $property) && $property['sub_schema']) ? ((object) $this->getResponseModelExample($property['sub_schema'], $spec)) : new \stdClass(),
'array' => array(),
'string' => $property['example'] ?? '',
'boolean' => true,
'float' => (float) $property['example'],
'integer' => (float) $property['example'],
default => $property['example'] ?? null,
};
}

return (object) $result;
}

public function getFilters(): array
{
return [
Expand Down Expand Up @@ -793,6 +868,12 @@ public function getFilters(): array
new TwigFilter('requestModelExample', function (array $parameter, array $spec, string $serviceName = '') {
return $this->getRequestModelExample($parameter, $spec, $serviceName);
}),
new TwigFilter('responseModelExample', function (string $model, array $spec) {
$result = $this->getResponseModelExample($model, $spec);
$json = json_encode($result, JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION);

return str_replace([ 'true', 'false', 'null' ], [ "True", "False", "None" ], $json);
})
];
}
}
5 changes: 5 additions & 0 deletions templates/python/pyproject.toml.twig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ classifiers = [
"Programming Language :: Python :: 3.12",
]

[project.optional-dependencies]
test = [
"requests_mock==1.11.0",
]

[project.urls]
Homepage = "{{ spec.contactURL }}"
Repository = "https://github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}"
Expand Down
1 change: 1 addition & 0 deletions templates/python/requirements.txt.twig
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
requests>=2.31,<3
requests_mock==1.11.0
Comment thread
greptile-apps[bot] marked this conversation as resolved.
pydantic>=2,<3
Empty file.
Empty file.
46 changes: 46 additions & 0 deletions templates/python/test/services/test_service.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json
import requests_mock
import unittest

from appwrite.client import Client
from appwrite.input_file import InputFile
from appwrite.models import *
from appwrite.services.{{ service.name | caseSnake }} import {{ service.name | caseUcfirst }}

class {{ service.name | caseUcfirst }}ServiceTest(unittest.TestCase):

def setUp(self):
self.client = Client()
self.{{ service.name | caseSnake }} = {{ service.name | caseUcfirst }}(self.client)

{% for method in service.methods %}
@requests_mock.Mocker()
def test_{{ method.name | caseSnake }}(self, m):
{%~ if method.type == 'webAuth' %}
data = None
{%~ elseif method.type == 'location' %}
data = bytearray()
{%~ else %}
{%~ if method.responseModel and method.responseModel != 'any' %}
data = {{ method.responseModel | responseModelExample(spec) | raw }}
{%~ else %}
data = ''
{%~ endif %}
{%~ endif %}
headers = {'Content-Type': {% if method.type == 'location' %}'application/octet-stream'{% else %}'application/json'{% endif %}}
m.request(requests_mock.ANY, requests_mock.ANY, {% if method.type == 'location' %}body=data{% else %}text=json.dumps(data){% endif %}, headers=headers)

response = self.{{ service.name | caseSnake }}.{{ method.name | caseSnake }}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%}
{% if parameter.type == 'object' %}{}{% elseif parameter.type == 'array' %}[]{% elseif parameter.type == 'file' %}InputFile.from_bytes(bytearray(), "example.file"){% elseif parameter.type == 'boolean' %}True{% elseif parameter.type == 'string' %}'{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}'{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%}
)

{%~ if method.type != 'webAuth' and method.responseModel and method.responseModel != 'any' %}
{%~ if method.responseModel == 'row' or method.responseModel == 'document' or method.responseModel == 'preferences' %}
data['data'] = {}
{%~ endif %}
self.assertEqual(response.to_dict(), data)
{%~ else %}
self.assertEqual(response, data)
{%~ endif %}

{% endfor %}
11 changes: 11 additions & 0 deletions templates/python/test/test_id.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import unittest

from appwrite.id import ID

class TestIDMethods(unittest.TestCase):

def test_unique(self):
self.assertEqual(len(ID.unique()), 20)

def test_custom(self):
self.assertEqual(ID.custom('custom'), 'custom')
80 changes: 80 additions & 0 deletions templates/python/test/test_operator.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import unittest

from appwrite.operator import Operator, Condition

class TestOperatorMethods(unittest.TestCase):

def test_increment(self):
self.assertEqual(Operator.increment(1), '{"method":"increment","values":[1]}')

def test_increment_with_max(self):
self.assertEqual(Operator.increment(5, 100), '{"method":"increment","values":[5,100]}')

def test_decrement(self):
self.assertEqual(Operator.decrement(1), '{"method":"decrement","values":[1]}')

def test_decrement_with_min(self):
self.assertEqual(Operator.decrement(3, 0), '{"method":"decrement","values":[3,0]}')

def test_multiply(self):
self.assertEqual(Operator.multiply(2), '{"method":"multiply","values":[2]}')

def test_multiply_with_max(self):
self.assertEqual(Operator.multiply(3, 1000), '{"method":"multiply","values":[3,1000]}')

def test_divide(self):
self.assertEqual(Operator.divide(2), '{"method":"divide","values":[2]}')

def test_divide_with_min(self):
self.assertEqual(Operator.divide(4, 1), '{"method":"divide","values":[4,1]}')

def test_modulo(self):
self.assertEqual(Operator.modulo(5), '{"method":"modulo","values":[5]}')

def test_power(self):
self.assertEqual(Operator.power(2), '{"method":"power","values":[2]}')

def test_power_with_max(self):
self.assertEqual(Operator.power(3, 100), '{"method":"power","values":[3,100]}')

def test_array_append(self):
self.assertEqual(Operator.array_append(['item1', 'item2']), '{"method":"arrayAppend","values":["item1","item2"]}')

def test_array_prepend(self):
self.assertEqual(Operator.array_prepend(['first', 'second']), '{"method":"arrayPrepend","values":["first","second"]}')

def test_array_insert(self):
self.assertEqual(Operator.array_insert(0, 'newItem'), '{"method":"arrayInsert","values":[0,"newItem"]}')

def test_array_remove(self):
self.assertEqual(Operator.array_remove('oldItem'), '{"method":"arrayRemove","values":["oldItem"]}')

def test_array_unique(self):
self.assertEqual(Operator.array_unique(), '{"method":"arrayUnique","values":[]}')

def test_array_intersect(self):
self.assertEqual(Operator.array_intersect(['a', 'b', 'c']), '{"method":"arrayIntersect","values":["a","b","c"]}')

def test_array_diff(self):
self.assertEqual(Operator.array_diff(['x', 'y']), '{"method":"arrayDiff","values":["x","y"]}')

def test_array_filter(self):
self.assertEqual(Operator.array_filter(Condition.EQUAL, 'test'), '{"method":"arrayFilter","values":["equal","test"]}')

def test_string_concat(self):
self.assertEqual(Operator.string_concat('suffix'), '{"method":"stringConcat","values":["suffix"]}')

def test_string_replace(self):
self.assertEqual(Operator.string_replace('old', 'new'), '{"method":"stringReplace","values":["old","new"]}')

def test_toggle(self):
self.assertEqual(Operator.toggle(), '{"method":"toggle","values":[]}')

def test_date_add_days(self):
self.assertEqual(Operator.date_add_days(7), '{"method":"dateAddDays","values":[7]}')

def test_date_sub_days(self):
self.assertEqual(Operator.date_sub_days(3), '{"method":"dateSubDays","values":[3]}')

def test_date_set_now(self):
self.assertEqual(Operator.date_set_now(), '{"method":"dateSetNow","values":[]}')
21 changes: 21 additions & 0 deletions templates/python/test/test_permission.py.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest

from appwrite.permission import Permission
from appwrite.role import Role

class TestPermissionMethods(unittest.TestCase):

def test_read(self):
self.assertEqual(Permission.read(Role.any()), 'read("any")')

def test_write(self):
self.assertEqual(Permission.write(Role.any()), 'write("any")')

def test_create(self):
self.assertEqual(Permission.create(Role.any()), 'create("any")')

def test_update(self):
self.assertEqual(Permission.update(Role.any()), 'update("any")')

def test_delete(self):
self.assertEqual(Permission.delete(Role.any()), 'delete("any")')
Loading
Loading