Skip to content

Commit

Permalink
SDK binary support for executions
Browse files Browse the repository at this point in the history
  • Loading branch information
Meldiron committed Sep 18, 2024
1 parent fd53dd4 commit 4e673d0
Show file tree
Hide file tree
Showing 20 changed files with 422 additions and 857 deletions.
50 changes: 20 additions & 30 deletions appwrite/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import io
import json
import os
import requests
from .input_file import InputFile
from .payload import Payload
from .multipart import MultipartParser
from .exception import AppwriteException
from .encoders.value_class_encoder import ValueClassEncoder

Expand All @@ -13,11 +12,11 @@ def __init__(self):
self._endpoint = 'https://cloud.appwrite.io/v1'
self._global_headers = {
'content-type': '',
'user-agent' : 'AppwritePythonSDK/6.1.0 (${os.uname().sysname}; ${os.uname().version}; ${os.uname().machine})',
'user-agent' : 'AppwritePythonSDK/7.0.0-rc1 (${os.uname().sysname}; ${os.uname().version}; ${os.uname().machine})',
'x-sdk-name': 'Python',
'x-sdk-platform': 'server',
'x-sdk-language': 'python',
'x-sdk-version': '6.1.0',
'x-sdk-version': '7.0.0-rc1',
'X-Appwrite-Response-Format' : '1.6.0',
}

Expand Down Expand Up @@ -91,10 +90,14 @@ def call(self, method, path='', headers=None, params=None, response_type='json')

if headers['content-type'].startswith('multipart/form-data'):
del headers['content-type']
headers['accept'] = 'multipart/form-data'
stringify = True
for key in data.copy():
if isinstance(data[key], InputFile):
files[key] = (data[key].filename, data[key].data)
if isinstance(data[key], Payload):
if data[key].filename:
files[key] = (data[key].filename, data[key].to_binary())
else:
data[key] = data[key].to_binary()
del data[key]
data = self.flatten(data, stringify=stringify)

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

if content_type.startswith('multipart/form-data'):
return MultipartParser(response.content, content_type).to_dict()

return response._content
except Exception as e:
if response != None:
Expand All @@ -146,20 +152,10 @@ def chunked_upload(
on_progress = None,
upload_id = ''
):
input_file = params[param_name]

if input_file.source_type == 'path':
size = os.stat(input_file.path).st_size
input = open(input_file.path, 'rb')
elif input_file.source_type == 'bytes':
size = len(input_file.data)
input = input_file.data

if size < self._chunk_size:
if input_file.source_type == 'path':
input_file.data = input.read()
payload = params[param_name]
size = params[param_name].size

params[param_name] = input_file
if size < self._chunk_size:
return self.call(
'post',
path,
Expand All @@ -182,16 +178,10 @@ def chunked_upload(
input.seek(offset)

while offset < size:
if input_file.source_type == 'path':
input_file.data = input.read(self._chunk_size) or input.read(size - offset)
elif input_file.source_type == 'bytes':
if offset + self._chunk_size < size:
end = offset + self._chunk_size
else:
end = size - offset
input_file.data = input[offset:end]

params[param_name] = input_file
params[param_name] = Payload.from_binary(
payload.to_binary(offset, min(self._chunk_size, size - offset)),
payload.filename
)
headers["content-range"] = f'bytes {offset}-{min((offset + self._chunk_size) - 1, size - 1)}/{size}'

result = self.call(
Expand Down
3 changes: 3 additions & 0 deletions appwrite/enums/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class Runtime(Enum):
PYTHON_3_11 = "python-3.11"
PYTHON_3_12 = "python-3.12"
PYTHON_ML_3_11 = "python-ml-3.11"
DENO_1_21 = "deno-1.21"
DENO_1_24 = "deno-1.24"
DENO_1_35 = "deno-1.35"
DENO_1_40 = "deno-1.40"
DART_2_15 = "dart-2.15"
DART_2_16 = "dart-2.16"
Expand Down
21 changes: 0 additions & 21 deletions appwrite/input_file.py

This file was deleted.

48 changes: 48 additions & 0 deletions appwrite/multipart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from email.parser import BytesParser
from email.policy import default
from .payload import Payload

class MultipartParser:
def __init__(self, multipart_bytes, content_type):
self.multipart_bytes = multipart_bytes
self.content_type = content_type
self.parts = {}
self.parse()

def parse(self):
# Create a message object
headers = f'Content-Type: {self.content_type}\r\n\r\n'.encode('ascii')
msg = BytesParser(policy=default).parsebytes(headers + self.multipart_bytes)

# Process each part
for part in msg.walk():
if part.is_multipart():
continue

# Get the name from Content-Disposition
content_disposition = part.get("Content-Disposition", "")
name = part.get_param("name", header="content-disposition")
if not name:
name = f"unnamed_part_{len(self.parts)}"

# Store the parsed data
self.parts[name] = {
"contents": part.get_payload(decode=True),
"headers": dict(part.items())
}

def to_dict(self):
result = {}
for name, part in self.parts.items():
if name == "responseBody":
result[name] = Payload.from_binary(part["contents"])
elif name == "responseHeaders":
headers_str = part["contents"].decode('utf-8', errors='replace')
result[name] = dict(line.split(": ", 1) for line in headers_str.split("\r\n") if line)
elif name == "responseStatusCode":
result[name] = int(part["contents"])
elif name == "duration":
result[name] = float(part["contents"])
else:
result[name] = part["contents"].decode('utf-8', errors='replace')
return result
63 changes: 63 additions & 0 deletions appwrite/payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Optional, Dict, Any
import os, json

class Payload:
size: int
filename: Optional[str] = None

_path: Optional[str] = None
_data: Optional[bytes] = None

def __init__(self, path: Optional[str] = None, data: Optional[bytes] = None, filename: Optional[str] = None):
if not path and not data:
raise ValueError("One of path or data must be provided")

self._path = path
self._data = data

self.filename = filename
if not self._data:
self.size = os.path.getsize(self._path)
else:
self.size = len(self._data)

def to_binary(self, offset: Optional[int] = 0, length: Optional[int] = None) -> bytes:
if not length:
length = self.size

if not self._data:
with open(self._path, 'rb') as f:
f.seek(offset)
return f.read(length)

return self._data[offset:offset + length]

def to_string(self) -> str:
return str(self.to_binary())

def to_json(self) -> Dict[str, Any]:
return json.loads(self.to_string())

def to_file(self, path: str) -> None: # in the client SDKs, this is def to_file() -> File:
with open(path, 'wb') as f:
return f.write(self.to_binary())

@classmethod
def from_binary(cls, data: bytes, filename: Optional[str] = None) -> 'Payload':
return cls(data=data, filename=filename)

@classmethod
def from_string(cls, data: str) -> 'Payload':
return cls(data=data.encode())

@classmethod
def from_file(cls, path: str, filename: Optional[str] = None) -> 'Payload':
if not os.path.exists(path):
raise FileNotFoundError(f"File {path} not found")
if not filename:
filename = os.path.basename(path)
return cls(path=path, filename=filename)

@classmethod
def from_json(cls, json: Dict[str, Any]) -> 'Payload':
return cls(data=json.dumps(json))
Loading

0 comments on commit 4e673d0

Please sign in to comment.