Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add XMLHttpRequest API and browser-style EventTarget #168

Merged
merged 22 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb0523c
feat: implement browser-style EventTarget
Xmader Aug 25, 2023
a32bb80
refactor: move DOMException into the `dom-exception` builtin_module
Xmader Aug 25, 2023
15048e0
feat(XHR): initial implementation of the XMLHttpRequest (XHR) API
Xmader Aug 25, 2023
c12de3a
fix(EventTarget): `this` in listener function should be the event target
Xmader Aug 25, 2023
df73b36
feat(XHR): status, statusText, getResponseHeader, getAllResponseHeaders
Xmader Aug 25, 2023
fcd197e
feat(XHR): get `XMLHttpRequest` working
Xmader Aug 25, 2023
4671dc1
feat(XHR): make `XMLHttpRequest` API globally available
Xmader Aug 25, 2023
ff00b01
feat(XHR): implement `responseType == "text"`
Xmader Aug 25, 2023
a0e00de
feat(XHR): pass `processRequestBodyChunkLength` and `processRequestEn…
Xmader Aug 25, 2023
024f63e
feat(XHR): implement upload progress monitoring
Xmader Aug 25, 2023
8b3517e
fix(XHR): typings
Xmader Aug 31, 2023
01cb084
fix(XHR): typings for `processResponse` callback
Xmader Aug 31, 2023
b730be5
feat(XHR): handle timeout
Xmader Sep 1, 2023
ed83f88
feat(XHR): `xhr.onerror` to handle network error
Xmader Sep 1, 2023
92fda8a
feat(XHR): print out network errors even then the error will be handl…
Xmader Sep 1, 2023
bc230b9
feat(XHR): implement `xhr.abort()`
Xmader Sep 1, 2023
c51fd0a
fix(XHR): readyState constants should be defined on the class instanc…
Xmader Sep 1, 2023
de658c8
fix(event-loop): keep loop running until the Python awaitable is done
Xmader Sep 1, 2023
0ded282
fix(XHR): do not send request body if empty
Xmader Sep 1, 2023
0b8c97a
feat(XHR): set default `User-Agent` header
Xmader Sep 1, 2023
30f17f7
test(xhr): add testing for XHR
caleb-distributive Sep 29, 2023
c6c22ef
feat(xhr): add all properties of Event class
caleb-distributive Sep 29, 2023
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
788 changes: 787 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ include = [
[tool.poetry.dependencies]
python = "^3.8"
pyreadline3 = { version = "^3.4.1", platform = "win32" }
aiohttp = { version = "^3.8.5", extras = ["speedups"] }
pminit = { version = "*", allow-prereleases = true }


Expand Down
1 change: 1 addition & 0 deletions python/pythonmonkey/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require("base64")
require("timers")
require("url")
require("XMLHttpRequest")

# Add the `.keys()` method on `Object.prototype` to get JSObjectProxy dict() conversion working
# Conversion from a dict-subclass to a strict dict by `dict(subclass)` internally calls the .keys() method to read the dictionary keys,
Expand Down
52 changes: 52 additions & 0 deletions python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @file XMLHttpRequest-internal.d.ts
* @brief TypeScript type declarations for the internal XMLHttpRequest helpers
* @author Tom Tang <xmader@distributive.network>
* @date August 2023
*/

/**
* `processResponse` callback's argument type
*/
export declare interface XHRResponse {
/** Response URL */
url: string;
/** HTTP status */
status: number;
/** HTTP status message */
statusText: string;
/** The `Content-Type` header value */
contentLength: number;
/** Implementation of the `xhr.getResponseHeader` method */
getResponseHeader(name: string): string | undefined;
/** Implementation of the `xhr.getAllResponseHeaders` method */
getAllResponseHeaders(): string;
/** Implementation of the `xhr.abort` method */
abort(): void;
}

/**
* Send request
*/
export declare function request(
method: string,
url: string,
headers: Record<string, string>,
body: string | Uint8Array,
timeoutMs: number,
// callbacks for request body progress
processRequestBodyChunkLength: (bytesLength: number) => void,
processRequestEndOfBody: () => void,
// callbacks for response progress
processResponse: (response: XHRResponse) => void,
processBodyChunk: (bytes: Uint8Array) => void,
processEndOfBody: () => void,
// callbacks for known exceptions
onTimeoutError: (err: Error) => void,
onNetworkError: (err: Error) => void,
): Promise<void>;

/**
* Decode data using the codec registered for encoding.
*/
export declare function decodeStr(data: Uint8Array, encoding?: string): string;
118 changes: 118 additions & 0 deletions python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# @file XMLHttpRequest-internal.py
# @brief internal helper functions for XMLHttpRequest
# @author Tom Tang <xmader@distributive.network>
# @date August 2023

import asyncio
import aiohttp
import yarl
Xmader marked this conversation as resolved.
Show resolved Hide resolved
import io
import platform
import pythonmonkey as pm
from typing import Union, ByteString, Callable, TypedDict

class XHRResponse(TypedDict, total=True):
"""
See definitions in `XMLHttpRequest-internal.d.ts`
"""
url: str
status: int
statusText: str
contentLength: int
getResponseHeader: Callable[[str], Union[str, None]]
getAllResponseHeaders: Callable[[], str]
abort: Callable[[], None]

async def request(
method: str,
url: str,
headers: dict,
body: Union[str, ByteString],
timeoutMs: float,
# callbacks for request body progress
processRequestBodyChunkLength: Callable[[int], None],
processRequestEndOfBody: Callable[[], None],
# callbacks for response progress
processResponse: Callable[[XHRResponse], None],
processBodyChunk: Callable[[bytearray], None],
processEndOfBody: Callable[[], None],
# callbacks for known exceptions
onTimeoutError: Callable[[asyncio.TimeoutError], None],
onNetworkError: Callable[[aiohttp.ClientError], None],
/
):
class BytesPayloadWithProgress(aiohttp.BytesPayload):
_chunkMaxLength = 2**16 # aiohttp default

async def write(self, writer) -> None:
buf = io.BytesIO(self._value)
chunk = buf.read(self._chunkMaxLength)
while chunk:
await writer.write(chunk)
processRequestBodyChunkLength(len(chunk))
chunk = buf.read(self._chunkMaxLength)
processRequestEndOfBody()

if isinstance(body, str):
body = bytes(body, "utf-8")
Xmader marked this conversation as resolved.
Show resolved Hide resolved

# set default headers
headers=dict(headers)
headers.setdefault("user-agent", f"Python/{platform.python_version()} PythonMonkey/{pm.__version__}")

if timeoutMs > 0:
timeoutOptions = aiohttp.ClientTimeout(total=timeoutMs/1000) # convert to seconds
else:
timeoutOptions = aiohttp.ClientTimeout() # default timeout

try:
async with aiohttp.request(method=method,
url=yarl.URL(url, encoded=True),
headers=headers,
data=BytesPayloadWithProgress(body) if body else None,
timeout=timeoutOptions,
) as res:
def getResponseHeader(name: str):
return res.headers.get(name)
def getAllResponseHeaders():
headers = []
for name, value in res.headers.items():
headers.append(f"{name.lower()}: {value}")
headers.sort()
return "\r\n".join(headers)
def abort():
res.close()

# readyState HEADERS_RECEIVED
responseData: XHRResponse = { # FIXME: PythonMonkey bug: the dict will be GCed if directly as an argument
'url': str(res.real_url),
'status': res.status,
'statusText': str(res.reason or ''),

'getResponseHeader': getResponseHeader,
'getAllResponseHeaders': getAllResponseHeaders,
'abort': abort,

'contentLength': res.content_length or 0,
}
processResponse(responseData)

# readyState LOADING
async for data in res.content.iter_any():
processBodyChunk(bytearray(data)) # PythonMonkey only accepts the mutable bytearray type

# readyState DONE
processEndOfBody()
except asyncio.TimeoutError as e:
onTimeoutError(e)
raise # rethrow
except aiohttp.ClientError as e:
onNetworkError(e)
raise # rethrow

def decodeStr(data: bytes, encoding='utf-8'): # XXX: Remove this once we get proper TextDecoder support
return str(data, encoding=encoding)

# Module exports
exports['request'] = request # type: ignore
exports['decodeStr'] = decodeStr # type: ignore
Loading
Loading