11import asyncio
22import base64
33import json
4- from typing import Any , Dict , TypedDict , cast
4+ from typing import Any , Dict , Literal , Optional , TypedDict , cast
55from websockets import ConnectionClosed
66
77from deno_sandbox .bridge import AsyncBridge
8- from deno_sandbox .errors import RpcValidationError , UnknownRpcMethod , ZodErrorRaw
8+ from deno_sandbox .errors import (
9+ HTTPStatusError ,
10+ ProcessAlreadyExited ,
11+ RpcValidationError ,
12+ UnknownRpcMethod ,
13+ ZodErrorRaw ,
14+ )
915from deno_sandbox .transport import WebSocketTransport
1016from deno_sandbox .utils import (
1117 convert_to_camel_case ,
@@ -41,6 +47,7 @@ def __init__(self, transport: WebSocketTransport):
4147 self ._listen_task : asyncio .Task [Any ] | None = None
4248 self ._pending_processes : Dict [int , asyncio .StreamReader ] = {}
4349 self ._loop : asyncio .AbstractEventLoop | None = None
50+ self ._signal_id = 0
4451
4552 async def close (self ):
4653 await self ._transport .close ()
@@ -89,7 +96,16 @@ async def call(self, method: str, params: Dict[str, Any]) -> Any:
8996 raise Exception (response ["error" ])
9097
9198 if response .get ("result" ) and response ["result" ].get ("error" ):
92- raise Exception (f"Application Error: { response ['result' ]['error' ]} " )
99+ err = response ["result" ]["error" ]
100+ if (
101+ "constructor_name" in err
102+ and err ["constructor_name" ] == "TypeError"
103+ and "code" in err
104+ and err ["code" ] == "ENOENT"
105+ ):
106+ raise ProcessAlreadyExited ("Process has already exited" )
107+
108+ raise Exception (f"Application Error: { err } " )
93109 return response ["result" ]["ok" ] if response .get ("result" ) else None
94110
95111 async def _listener (self ) -> None :
@@ -128,6 +144,34 @@ async def _listener(self) -> None:
128144 if not future .done ():
129145 future .set_exception (e )
130146
147+ async def fetch (
148+ self ,
149+ url : str ,
150+ method : Optional [str ] = "GET" ,
151+ headers : Optional [dict [str , str ]] = None ,
152+ redirect : Literal ["follow" , "manual" ] = None ,
153+ pid : Optional [int ] = None ,
154+ ) -> AsyncFetchResponse :
155+ self ._signal_id += 1
156+ signal_id = self ._signal_id
157+
158+ params : FetchParams = {
159+ "url" : url ,
160+ "method" : method or "GET" ,
161+ "headers" : list (headers .items ()) if headers else [],
162+ "redirect" : redirect or "follow" ,
163+ "abortId" : signal_id ,
164+ }
165+
166+ if pid is not None :
167+ params ["pid" ] = pid
168+
169+ response_data = await self .call ("fetch" , params )
170+ response = cast (FetchResponseData , response_data )
171+
172+ fetch_response = AsyncFetchResponse (self , response )
173+ return fetch_response
174+
131175
132176class RpcClient :
133177 def __init__ (self , async_client : AsyncRpcClient , bridge : AsyncBridge ):
@@ -137,5 +181,154 @@ def __init__(self, async_client: AsyncRpcClient, bridge: AsyncBridge):
137181 def call (self , method : str , params : Dict [str , Any ]) -> Any :
138182 return self ._bridge .run (self ._async_client .call (method , params ))
139183
184+ def fetch (
185+ self ,
186+ url : str ,
187+ method : Optional [str ] = "GET" ,
188+ headers : Optional [dict [str , str ]] = None ,
189+ redirect : Literal ["follow" , "manual" ] = None ,
190+ pid : Optional [int ] = None ,
191+ ) -> FetchResponse :
192+ response = self ._bridge .run (
193+ self ._async_client .fetch (url , method , headers , redirect , pid )
194+ )
195+
196+ return FetchResponse (self , response )
197+
140198 def close (self ):
141199 self ._bridge .run (self ._async_client .close ())
200+
201+
202+ class FetchParams (TypedDict ):
203+ method : str
204+ url : str
205+ headers : list [tuple [str , str ]]
206+ redirect : str
207+ pid : int
208+ abortId : int
209+
210+
211+ class FetchResponseData (TypedDict ):
212+ status : int
213+ status_text : str
214+ headers : list [tuple [str , str ]]
215+ body_stream_id : int
216+
217+
218+ class AsyncFetchResponse :
219+ def __init__ (self , rpc : AsyncRpcClient , response : FetchResponseData ):
220+ self ._rpc = rpc
221+ self ._response = response
222+
223+ def raise_for_status (self ) -> HTTPStatusError | None :
224+ if self .is_success :
225+ return
226+
227+ message = "{self} resulted in a {error_type} (status code: {self.status})"
228+ status_class = self .status // 100
229+ error_types = {
230+ 1 : "Informational response" ,
231+ 3 : "Redirect response" ,
232+ 4 : "Client error" ,
233+ 5 : "Server error" ,
234+ }
235+ error_type = error_types .get (status_class , "Invalid status code" )
236+ message = message .format (self , error_type = error_type )
237+
238+ return HTTPStatusError (self .status , message )
239+
240+ @property
241+ def headers (self ) -> list [tuple [str , str ]]:
242+ return self ._response ["headers" ]
243+
244+ @property
245+ def status_code (self ) -> int :
246+ return self ._response ["status" ]
247+
248+ @property
249+ def is_informational (self ) -> int :
250+ return 100 <= self .status_code <= 199
251+
252+ @property
253+ def is_success (self ) -> int :
254+ return 200 <= self .status_code <= 299
255+
256+ @property
257+ def is_redirect (self ) -> int :
258+ return 300 <= self .status_code <= 399
259+
260+ @property
261+ def is_client_error (self ) -> int :
262+ return 400 <= self .status_code <= 499
263+
264+ @property
265+ def is_server_error (self ) -> int :
266+ return 500 <= self .status_code <= 599
267+
268+ @property
269+ def is_error (self ) -> int :
270+ return 400 <= self .status_code <= 599
271+
272+ @property
273+ def has_redirect_location (self ) -> bool :
274+ return (
275+ self .status_code in (301 , 302 , 303 , 307 , 308 )
276+ and "location" in self ._response ["headers" ]
277+ )
278+
279+ async def cancel (self ) -> None :
280+ await self ._rpc .call ("abort" , {"abortId" : self ._response ["abortId" ]})
281+
282+ def __repr__ (self ) -> str :
283+ return f"<Response [{ self .status_code } ]>"
284+
285+
286+ class FetchResponse (AsyncFetchResponse ):
287+ def __init__ (self , rpc : AsyncRpcClient , async_res : AsyncFetchResponse ):
288+ self ._rpc = rpc
289+ self ._async = async_res
290+
291+ def raise_for_status (self ) -> HTTPStatusError | None :
292+ return self ._async .raise_for_status ()
293+
294+ @property
295+ def headers (self ) -> list [tuple [str , str ]]:
296+ return self ._async .headers
297+
298+ @property
299+ def status_code (self ) -> int :
300+ return self ._async .status_code
301+
302+ @property
303+ def is_informational (self ) -> int :
304+ return self ._async .is_informational
305+
306+ @property
307+ def is_success (self ) -> int :
308+ return self ._async .is_success
309+
310+ @property
311+ def is_redirect (self ) -> int :
312+ return self ._async .is_redirect
313+
314+ @property
315+ def is_client_error (self ) -> int :
316+ return self ._async .is_client_error
317+
318+ @property
319+ def is_server_error (self ) -> int :
320+ return self ._async .is_server_error
321+
322+ @property
323+ def is_error (self ) -> int :
324+ return self ._async .is_error
325+
326+ @property
327+ def has_redirect_location (self ) -> bool :
328+ return self ._async .has_redirect_location
329+
330+ def cancel (self ) -> None :
331+ self ._rpc ._bridge .run (self ._async .cancel ())
332+
333+ def __repr__ (self ) -> str :
334+ return f"<Response [{ self .status_code } ]>"
0 commit comments