Skip to content

Commit f434da9

Browse files
authored
Merge pull request #162 from Distributive-Network/Xmader/feat/url
`URL`/`URLSearchParams` APIs
2 parents a1c135e + 24ed010 commit f434da9

19 files changed

+3781
-1
lines changed

poetry.lock

+787-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ include = [
2929
[tool.poetry.dependencies]
3030
python = "^3.8"
3131
pyreadline3 = { version = "^3.4.1", platform = "win32" }
32+
aiohttp = { version = "^3.8.5", extras = ["speedups"] }
3233
pminit = { version = "*", allow-prereleases = true }
3334

3435

python/pminit/pythonmonkey/package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/pminit/pythonmonkey/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"homepage": "https://github.com/Distributive-Network/PythonMonkey#readme",
2323
"dependencies": {
24+
"core-js": "^3.32.0",
2425
"ctx-module": "^1.0.14"
2526
}
2627
}

python/pythonmonkey/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
del importlib
1010

1111
# Load the module by default to expose global APIs
12+
## builtin_modules
1213
require("console")
1314
require("base64")
1415
require("timers")
16+
require("url")
17+
require("XMLHttpRequest")
1518

1619
# Add the `.keys()` method on `Object.prototype` to get JSObjectProxy dict() conversion working
1720
# Conversion from a dict-subclass to a strict dict by `dict(subclass)` internally calls the .keys() method to read the dictionary keys,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @file XMLHttpRequest-internal.d.ts
3+
* @brief TypeScript type declarations for the internal XMLHttpRequest helpers
4+
* @author Tom Tang <[email protected]>
5+
* @date August 2023
6+
*/
7+
8+
/**
9+
* `processResponse` callback's argument type
10+
*/
11+
export declare interface XHRResponse {
12+
/** Response URL */
13+
url: string;
14+
/** HTTP status */
15+
status: number;
16+
/** HTTP status message */
17+
statusText: string;
18+
/** The `Content-Type` header value */
19+
contentLength: number;
20+
/** Implementation of the `xhr.getResponseHeader` method */
21+
getResponseHeader(name: string): string | undefined;
22+
/** Implementation of the `xhr.getAllResponseHeaders` method */
23+
getAllResponseHeaders(): string;
24+
/** Implementation of the `xhr.abort` method */
25+
abort(): void;
26+
}
27+
28+
/**
29+
* Send request
30+
*/
31+
export declare function request(
32+
method: string,
33+
url: string,
34+
headers: Record<string, string>,
35+
body: string | Uint8Array,
36+
timeoutMs: number,
37+
// callbacks for request body progress
38+
processRequestBodyChunkLength: (bytesLength: number) => void,
39+
processRequestEndOfBody: () => void,
40+
// callbacks for response progress
41+
processResponse: (response: XHRResponse) => void,
42+
processBodyChunk: (bytes: Uint8Array) => void,
43+
processEndOfBody: () => void,
44+
// callbacks for known exceptions
45+
onTimeoutError: (err: Error) => void,
46+
onNetworkError: (err: Error) => void,
47+
): Promise<void>;
48+
49+
/**
50+
* Decode data using the codec registered for encoding.
51+
*/
52+
export declare function decodeStr(data: Uint8Array, encoding?: string): string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# @file XMLHttpRequest-internal.py
2+
# @brief internal helper functions for XMLHttpRequest
3+
# @author Tom Tang <[email protected]>
4+
# @date August 2023
5+
6+
import asyncio
7+
import aiohttp
8+
import yarl
9+
import io
10+
import platform
11+
import pythonmonkey as pm
12+
from typing import Union, ByteString, Callable, TypedDict
13+
14+
class XHRResponse(TypedDict, total=True):
15+
"""
16+
See definitions in `XMLHttpRequest-internal.d.ts`
17+
"""
18+
url: str
19+
status: int
20+
statusText: str
21+
contentLength: int
22+
getResponseHeader: Callable[[str], Union[str, None]]
23+
getAllResponseHeaders: Callable[[], str]
24+
abort: Callable[[], None]
25+
26+
async def request(
27+
method: str,
28+
url: str,
29+
headers: dict,
30+
body: Union[str, ByteString],
31+
timeoutMs: float,
32+
# callbacks for request body progress
33+
processRequestBodyChunkLength: Callable[[int], None],
34+
processRequestEndOfBody: Callable[[], None],
35+
# callbacks for response progress
36+
processResponse: Callable[[XHRResponse], None],
37+
processBodyChunk: Callable[[bytearray], None],
38+
processEndOfBody: Callable[[], None],
39+
# callbacks for known exceptions
40+
onTimeoutError: Callable[[asyncio.TimeoutError], None],
41+
onNetworkError: Callable[[aiohttp.ClientError], None],
42+
/
43+
):
44+
class BytesPayloadWithProgress(aiohttp.BytesPayload):
45+
_chunkMaxLength = 2**16 # aiohttp default
46+
47+
async def write(self, writer) -> None:
48+
buf = io.BytesIO(self._value)
49+
chunk = buf.read(self._chunkMaxLength)
50+
while chunk:
51+
await writer.write(chunk)
52+
processRequestBodyChunkLength(len(chunk))
53+
chunk = buf.read(self._chunkMaxLength)
54+
processRequestEndOfBody()
55+
56+
if isinstance(body, str):
57+
body = bytes(body, "utf-8")
58+
59+
# set default headers
60+
headers=dict(headers)
61+
headers.setdefault("user-agent", f"Python/{platform.python_version()} PythonMonkey/{pm.__version__}")
62+
63+
if timeoutMs > 0:
64+
timeoutOptions = aiohttp.ClientTimeout(total=timeoutMs/1000) # convert to seconds
65+
else:
66+
timeoutOptions = aiohttp.ClientTimeout() # default timeout
67+
68+
try:
69+
async with aiohttp.request(method=method,
70+
url=yarl.URL(url, encoded=True),
71+
headers=headers,
72+
data=BytesPayloadWithProgress(body) if body else None,
73+
timeout=timeoutOptions,
74+
) as res:
75+
def getResponseHeader(name: str):
76+
return res.headers.get(name)
77+
def getAllResponseHeaders():
78+
headers = []
79+
for name, value in res.headers.items():
80+
headers.append(f"{name.lower()}: {value}")
81+
headers.sort()
82+
return "\r\n".join(headers)
83+
def abort():
84+
res.close()
85+
86+
# readyState HEADERS_RECEIVED
87+
responseData: XHRResponse = { # FIXME: PythonMonkey bug: the dict will be GCed if directly as an argument
88+
'url': str(res.real_url),
89+
'status': res.status,
90+
'statusText': str(res.reason or ''),
91+
92+
'getResponseHeader': getResponseHeader,
93+
'getAllResponseHeaders': getAllResponseHeaders,
94+
'abort': abort,
95+
96+
'contentLength': res.content_length or 0,
97+
}
98+
processResponse(responseData)
99+
100+
# readyState LOADING
101+
async for data in res.content.iter_any():
102+
processBodyChunk(bytearray(data)) # PythonMonkey only accepts the mutable bytearray type
103+
104+
# readyState DONE
105+
processEndOfBody()
106+
except asyncio.TimeoutError as e:
107+
onTimeoutError(e)
108+
raise # rethrow
109+
except aiohttp.ClientError as e:
110+
onNetworkError(e)
111+
raise # rethrow
112+
113+
def decodeStr(data: bytes, encoding='utf-8'): # XXX: Remove this once we get proper TextDecoder support
114+
return str(data, encoding=encoding)
115+
116+
# Module exports
117+
exports['request'] = request # type: ignore
118+
exports['decodeStr'] = decodeStr # type: ignore

0 commit comments

Comments
 (0)