Skip to content

Commit 4e673d0

Browse files
committed
SDK binary support for executions
1 parent fd53dd4 commit 4e673d0

20 files changed

+422
-857
lines changed

appwrite/client.py

Lines changed: 20 additions & 30 deletions
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-rc1 (${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-rc1',
2120
'X-Appwrite-Response-Format' : '1.6.0',
2221
}
2322

@@ -91,10 +90,14 @@ 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)
96+
if isinstance(data[key], Payload):
97+
if data[key].filename:
98+
files[key] = (data[key].filename, data[key].to_binary())
99+
else:
100+
data[key] = data[key].to_binary()
98101
del data[key]
99102
data = self.flatten(data, stringify=stringify)
100103

@@ -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/runtime.py

Lines changed: 3 additions & 0 deletions
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

Lines changed: 0 additions & 21 deletions
This file was deleted.

appwrite/multipart.py

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

appwrite/payload.py

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

0 commit comments

Comments
 (0)