Skip to content

Commit 780399d

Browse files
Merge pull request #94 from appwrite/dev
SDK binary support for executions
2 parents dba5c5d + 221040c commit 780399d

21 files changed

+434
-858
lines changed

appwrite/client.py

+21-31
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import io
21
import json
3-
import os
42
import requests
5-
from .input_file import InputFile
3+
from .payload import Payload
4+
from .multipart import MultipartParser
65
from .exception import AppwriteException
76
from .encoders.value_class_encoder import ValueClassEncoder
87

@@ -13,11 +12,11 @@ def __init__(self):
1312
self._endpoint = 'https://cloud.appwrite.io/v1'
1413
self._global_headers = {
1514
'content-type': '',
16-
'user-agent' : 'AppwritePythonSDK/6.1.0 (${os.uname().sysname}; ${os.uname().version}; ${os.uname().machine})',
15+
'user-agent' : 'AppwritePythonSDK/7.0.0 (${os.uname().sysname}; ${os.uname().version}; ${os.uname().machine})',
1716
'x-sdk-name': 'Python',
1817
'x-sdk-platform': 'server',
1918
'x-sdk-language': 'python',
20-
'x-sdk-version': '6.1.0',
19+
'x-sdk-version': '7.0.0',
2120
'X-Appwrite-Response-Format' : '1.6.0',
2221
}
2322

@@ -91,11 +90,15 @@ def call(self, method, path='', headers=None, params=None, response_type='json')
9190

9291
if headers['content-type'].startswith('multipart/form-data'):
9392
del headers['content-type']
93+
headers['accept'] = 'multipart/form-data'
9494
stringify = True
9595
for key in data.copy():
96-
if isinstance(data[key], InputFile):
97-
files[key] = (data[key].filename, data[key].data)
98-
del data[key]
96+
if isinstance(data[key], Payload):
97+
if data[key].filename:
98+
files[key] = (data[key].filename, data[key].to_binary())
99+
del data[key]
100+
else:
101+
data[key] = data[key].to_string()
99102
data = self.flatten(data, stringify=stringify)
100103

101104
response = None
@@ -126,6 +129,9 @@ def call(self, method, path='', headers=None, params=None, response_type='json')
126129
if content_type.startswith('application/json'):
127130
return response.json()
128131

132+
if content_type.startswith('multipart/form-data'):
133+
return MultipartParser(response.content, content_type).to_dict()
134+
129135
return response._content
130136
except Exception as e:
131137
if response != None:
@@ -146,20 +152,10 @@ def chunked_upload(
146152
on_progress = None,
147153
upload_id = ''
148154
):
149-
input_file = params[param_name]
150-
151-
if input_file.source_type == 'path':
152-
size = os.stat(input_file.path).st_size
153-
input = open(input_file.path, 'rb')
154-
elif input_file.source_type == 'bytes':
155-
size = len(input_file.data)
156-
input = input_file.data
157-
158-
if size < self._chunk_size:
159-
if input_file.source_type == 'path':
160-
input_file.data = input.read()
155+
payload = params[param_name]
156+
size = params[param_name].size
161157

162-
params[param_name] = input_file
158+
if size < self._chunk_size:
163159
return self.call(
164160
'post',
165161
path,
@@ -182,16 +178,10 @@ def chunked_upload(
182178
input.seek(offset)
183179

184180
while offset < size:
185-
if input_file.source_type == 'path':
186-
input_file.data = input.read(self._chunk_size) or input.read(size - offset)
187-
elif input_file.source_type == 'bytes':
188-
if offset + self._chunk_size < size:
189-
end = offset + self._chunk_size
190-
else:
191-
end = size - offset
192-
input_file.data = input[offset:end]
193-
194-
params[param_name] = input_file
181+
params[param_name] = Payload.from_binary(
182+
payload.to_binary(offset, min(self._chunk_size, size - offset)),
183+
payload.filename
184+
)
195185
headers["content-range"] = f'bytes {offset}-{min((offset + self._chunk_size) - 1, size - 1)}/{size}'
196186

197187
result = self.call(

appwrite/enums/image_format.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ class ImageFormat(Enum):
66
GIF = "gif"
77
PNG = "png"
88
WEBP = "webp"
9+
AVIF = "avif"

appwrite/enums/runtime.py

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class Runtime(Enum):
2121
PYTHON_3_11 = "python-3.11"
2222
PYTHON_3_12 = "python-3.12"
2323
PYTHON_ML_3_11 = "python-ml-3.11"
24+
DENO_1_21 = "deno-1.21"
25+
DENO_1_24 = "deno-1.24"
26+
DENO_1_35 = "deno-1.35"
2427
DENO_1_40 = "deno-1.40"
2528
DART_2_15 = "dart-2.15"
2629
DART_2_16 = "dart-2.16"

appwrite/input_file.py

-21
This file was deleted.

appwrite/multipart.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from email.parser import BytesParser
2+
from email.policy import default
3+
from .payload import Payload
4+
import json
5+
6+
class MultipartParser:
7+
def __init__(self, multipart_bytes, content_type):
8+
self.multipart_bytes = multipart_bytes
9+
self.content_type = content_type
10+
self.parts = {}
11+
self.parse()
12+
13+
def parse(self):
14+
# Create a message object
15+
headers = f'Content-Type: {self.content_type}\r\n\r\n'.encode('ascii')
16+
msg = BytesParser(policy=default).parsebytes(headers + self.multipart_bytes)
17+
18+
# Process each part
19+
for part in msg.walk():
20+
if part.is_multipart():
21+
continue
22+
23+
# Get the name from Content-Disposition
24+
content_disposition = part.get("Content-Disposition", "")
25+
name = part.get_param("name", header="content-disposition")
26+
if not name:
27+
name = f"unnamed_part_{len(self.parts)}"
28+
29+
# Store the parsed data
30+
self.parts[name] = {
31+
"contents": part.get_payload(decode=True),
32+
"headers": dict(part.items())
33+
}
34+
35+
def to_dict(self):
36+
result = {}
37+
for name, part in self.parts.items():
38+
if name == "responseBody":
39+
result[name] = Payload.from_binary(part["contents"])
40+
elif name == "responseHeaders":
41+
headers_str = part["contents"].decode('utf-8', errors='replace')
42+
result[name] = json.loads(headers_str)
43+
elif name == "responseStatusCode":
44+
result[name] = int(part["contents"])
45+
elif name == "duration":
46+
result[name] = float(part["contents"])
47+
else:
48+
result[name] = part["contents"].decode('utf-8', errors='replace')
49+
return result

appwrite/payload.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Optional, Dict, Any
2+
import os, json
3+
4+
class Payload:
5+
filename: Optional[str] = None
6+
7+
_path: Optional[str] = None
8+
_data: Optional[bytes] = None
9+
_size: int = 0
10+
11+
@property
12+
def size(self) -> int:
13+
return self._size
14+
15+
def __init__(self, path: Optional[str] = None, data: Optional[bytes] = None, filename: Optional[str] = None):
16+
if path is None and data is None:
17+
raise ValueError("One of path or data must be provided")
18+
19+
self._path = path
20+
self._data = data
21+
22+
self.filename = filename
23+
if self._data is None:
24+
self._size = os.path.getsize(self._path)
25+
else:
26+
self._size = len(self._data)
27+
28+
def to_binary(self, offset: Optional[int] = 0, length: Optional[int] = None) -> bytes:
29+
if length is None:
30+
length = self._size
31+
32+
if self._data is None:
33+
with open(self._path, 'rb') as f:
34+
f.seek(offset)
35+
return f.read(length)
36+
37+
return self._data[offset:offset + length]
38+
39+
def to_string(self, encoding="utf-8") -> str:
40+
return self.to_binary().decode(encoding)
41+
42+
def __str__(self) -> str:
43+
return self.to_string()
44+
45+
def to_json(self) -> Dict[str, Any]:
46+
return json.loads(self.to_string())
47+
48+
def to_file(self, path: str) -> None:
49+
os.makedirs(os.path.dirname(path), exist_ok=True)
50+
51+
with open(path, 'wb') as f:
52+
return f.write(self.to_binary())
53+
54+
@classmethod
55+
def from_binary(cls, data: bytes, filename: Optional[str] = None) -> 'Payload':
56+
return cls(data=data, filename=filename)
57+
58+
@classmethod
59+
def from_string(cls, data: str) -> 'Payload':
60+
return cls(data=data.encode())
61+
62+
@classmethod
63+
def from_file(cls, path: str, filename: Optional[str] = None) -> 'Payload':
64+
if not os.path.exists(path):
65+
raise FileNotFoundError(f"File {path} not found")
66+
if not filename:
67+
filename = os.path.basename(path)
68+
return cls(path=path, filename=filename)
69+
70+
@classmethod
71+
def from_json(cls, data: Dict[str, Any]) -> 'Payload':
72+
return cls.from_string(json.dumps(data))

0 commit comments

Comments
 (0)