11# This module defines a Workers API for Python. It is similar to the API provided by
22# JS Workers, but with changes and additions to be more idiomatic to the Python
33# programming language.
4+ import http .client
5+ import json
46from collections .abc import Generator , Iterable , MutableMapping
57from contextlib import ExitStack , contextmanager
68from enum import StrEnum
1820 "js.ReadableStream | js.URLSearchParams"
1921)
2022Body = "str | FormData | JSBody"
21- Headers = dict [str , str ] | list [tuple [str , str ]]
23+ Headers = " dict[str, str] | list[tuple[str, str]] | js.Headers"
2224
2325
2426# https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties
@@ -42,7 +44,7 @@ class RequestInitCfProperties(TypedDict, total=False):
4244# This matches the Request options:
4345# https://developers.cloudflare.com/workers/runtime-apis/request/#options
4446class FetchKwargs (TypedDict , total = False ):
45- headers : Headers | None
47+ headers : " Headers | None"
4648 body : "Body | None"
4749 method : HTTPMethod = HTTPMethod .GET
4850 redirect : str | None
@@ -53,16 +55,14 @@ class FetchKwargs(TypedDict, total=False):
5355# duplicates are lost, we should fix that so it returns a http.client.HTTPMessage
5456class FetchResponse (pyodide .http .FetchResponse ):
5557 # TODO: Consider upstreaming the `body` attribute
58+ # TODO: Behind a compat flag make this return a native stream (StreamReader?), or perhaps
59+ # behind a different name, maybe `stream`?
5660 @property
57- def body (self ) -> Body :
61+ def body (self ) -> "js.ReadableStream" :
5862 """
59- Returns the body from the JavaScript Response instance.
63+ Returns the body as a JavaScript ReadableStream from the JavaScript Response instance.
6064 """
61- b = self .js_response .body
62- if b .constructor .name == "FormData" :
63- return FormData (b )
64- else :
65- return b
65+ return self .js_response .body
6666
6767 @property
6868 def js_object (self ) -> "js.Response" :
@@ -74,14 +74,16 @@ def js_object(self) -> "js.Response":
7474 Some methods are implemented by `FetchResponse`, these include `buffer`
7575 (replacing JavaScript's `arrayBuffer`), `bytes`, `json`, and `text`.
7676
77- Some methods are intentionally not implemented, these include `blob`.
78-
7977 There are also some additional methods implemented by `FetchResponse`.
8078 See https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse
8179 for details.
8280 """
8381
84- async def formData (self ) -> "FormData" :
82+ async def formData (self ) -> "FormData" : # TODO: Remove after certain compat date.
83+ return await self .form_data ()
84+
85+ async def form_data (self ) -> "FormData" :
86+ self ._raise_if_failed ()
8587 try :
8688 return FormData (await self .js_response .formData ())
8789 except JsException as exc :
@@ -96,6 +98,10 @@ def replace_body(self, body: Body) -> "FetchResponse":
9698 js_resp = js .Response .new (b , self .js_response )
9799 return FetchResponse (js_resp .url , js_resp )
98100
101+ async def blob (self ) -> "Blob" :
102+ self ._raise_if_failed ()
103+ return Blob (await self .js_object .blob ())
104+
99105
100106async def fetch (
101107 resource : str ,
@@ -125,6 +131,18 @@ def _manage_pyproxies():
125131 destroy_proxies (proxies )
126132
127133
134+ def _to_js_headers (headers : Headers ):
135+ if isinstance (headers , list ):
136+ # We should have a list[tuple[str, str]]
137+ return js .Headers .new (headers )
138+ elif isinstance (headers , dict ):
139+ return js .Headers .new (headers .items ())
140+ elif hasattr (headers , "constructor" ) and headers .constructor .name == "Headers" :
141+ return headers
142+ else :
143+ raise TypeError ("Received unexpected type for headers argument" )
144+
145+
128146class Response (FetchResponse ):
129147 def __init__ (
130148 self ,
@@ -160,13 +178,7 @@ def _create_options(
160178 if status_text :
161179 options ["statusText" ] = status_text
162180 if headers :
163- if isinstance (headers , list ):
164- # We should have a list[tuple[str, str]]
165- options ["headers" ] = js .Headers .new (headers )
166- elif isinstance (headers , dict ):
167- options ["headers" ] = js .Headers .new (headers .items ())
168- else :
169- raise TypeError ("Received unexpected type for headers argument" )
181+ options ["headers" ] = _to_js_headers (headers )
170182
171183 return options
172184
@@ -457,3 +469,138 @@ def name(self) -> str:
457469 @property
458470 def last_modified (self ) -> int :
459471 return self ._js_blob .last_modified
472+
473+
474+ class Request :
475+ def __init__ (self , input : "Request | str" , ** other_options : Unpack [FetchKwargs ]):
476+ if "method" in other_options and isinstance (
477+ other_options ["method" ], HTTPMethod
478+ ):
479+ other_options ["method" ] = other_options ["method" ].value
480+
481+ if "headers" in other_options :
482+ other_options ["headers" ] = _to_js_headers (other_options ["headers" ])
483+ self ._js_request = js .Request .new (
484+ input ._js_request if isinstance (input , Request ) else input , ** other_options
485+ )
486+
487+ @property
488+ def js_object (self ) -> "js.Request" :
489+ return self ._js_request
490+
491+ # TODO: expose `body` as a native Python stream in the future, follow how we define `Response`
492+ @property
493+ def body (self ) -> "js.ReadableStream" :
494+ return self .js_object .body
495+
496+ @property
497+ def body_used (self ) -> bool :
498+ return self .js_object .bodyUsed
499+
500+ @property
501+ def cache (self ) -> str :
502+ return self .js_object .cache
503+
504+ @property
505+ def credentials (self ) -> str :
506+ return self .js_object .credentials
507+
508+ @property
509+ def destination (self ) -> str :
510+ return self .js_object .destination
511+
512+ @property
513+ def headers (self ) -> http .client .HTTPMessage :
514+ result = http .client .HTTPMessage ()
515+
516+ for key , val in self .js_object .headers :
517+ for subval in val .split ("," ):
518+ result [key ] = subval .strip ()
519+
520+ return result
521+
522+ @property
523+ def integrity (self ) -> str :
524+ return self .js_object .integrity
525+
526+ @property
527+ def is_history_navigation (self ) -> bool :
528+ return self .js_object .isHistoryNavigation
529+
530+ @property
531+ def keepalive (self ) -> bool :
532+ return self .js_object .keepalive
533+
534+ @property
535+ def method (self ) -> HTTPMethod :
536+ return HTTPMethod [self .js_object .method ]
537+
538+ @property
539+ def mode (self ) -> str :
540+ return self .js_object .mode
541+
542+ @property
543+ def redirect (self ) -> str :
544+ return self .js_object .redirect
545+
546+ @property
547+ def referrer (self ) -> str :
548+ return self .js_object .referrer
549+
550+ @property
551+ def referrer_policy (self ) -> str :
552+ return self .js_object .referrerPolicy
553+
554+ @property
555+ def url (self ) -> str :
556+ return self .js_object .url
557+
558+ def _raise_if_failed (self ) -> None :
559+ # TODO: https://github.com/pyodide/pyodide/blob/a53c17fd8/src/py/pyodide/http.py#L252
560+ if self .body_used :
561+ # TODO: Use BodyUsedError in newer Pyodide versions.
562+ raise OSError ("Body already used" )
563+
564+ """
565+ Instance methods defined below.
566+
567+ The naming of these methods should match Request's methods when possible.
568+
569+ TODO: AbortController support.
570+ """
571+
572+ async def buffer (self ) -> "js.ArrayBuffer" :
573+ # The naming of this method matches that of Response.
574+ self ._raise_if_failed ()
575+ return await self .js_object .arrayBuffer ()
576+
577+ async def form_data (self ) -> "FormData" :
578+ self ._raise_if_failed ()
579+ try :
580+ return FormData (await self .js_object .formData ())
581+ except JsException as exc :
582+ raise _to_python_exception (exc ) from exc
583+
584+ async def blob (self ) -> Blob :
585+ self ._raise_if_failed ()
586+ return Blob (await self .js_object .blob ())
587+
588+ async def bytes (self ) -> bytes :
589+ self ._raise_if_failed ()
590+ return (await self .buffer ()).to_bytes ()
591+
592+ def clone (self ) -> "Request" :
593+ if self .body_used :
594+ # TODO: Use BodyUsedError in newer Pyodide versions.
595+ raise OSError ("Body already used" )
596+ return Request (
597+ self .js_object .clone (),
598+ )
599+
600+ async def json (self , ** kwargs : Any ) -> Any :
601+ self ._raise_if_failed ()
602+ return json .loads (await self .text (), ** kwargs )
603+
604+ async def text (self ) -> str :
605+ self ._raise_if_failed ()
606+ return await self .js_object .text ()
0 commit comments