From 9d850a7ab5fd3d43a27e3b5687ac1b06d28bd703 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 18 Feb 2025 10:51:23 +0800 Subject: [PATCH] feat: Add `base_url` for client --- examples/base_url.py | 26 +++ rnet.pyi | 27 +++ src/client.rs | 413 +++++++++++++++++++++++-------------------- src/param/client.rs | 5 + 4 files changed, 281 insertions(+), 190 deletions(-) create mode 100644 examples/base_url.py diff --git a/examples/base_url.py b/examples/base_url.py new file mode 100644 index 00000000..fd267d2e --- /dev/null +++ b/examples/base_url.py @@ -0,0 +1,26 @@ +import asyncio +from rnet import Impersonate, Client + + +async def main(): + client = Client( + base_url="https://httpbin.org", + impersonate=Impersonate.Firefox135, + user_agent="rnet", + ) + resp = await client.get("/stream/20") + print("Status Code: ", resp.status_code) + print("Version: ", resp.version) + print("Response URL: ", resp.url) + print("Headers: ", resp.headers.to_dict()) + print("Content-Length: ", resp.content_length) + print("Encoding: ", resp.encoding) + print("Remote Address: ", resp.remote_addr) + streamer = resp.stream() + async for chunk in streamer: + print("Chunk: ", chunk) + await asyncio.sleep(0.1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/rnet.pyi b/rnet.pyi index f842760a..f4d1a88f 100644 --- a/rnet.pyi +++ b/rnet.pyi @@ -275,6 +275,32 @@ class Client: def websocket(self, url:builtins.str, **kwds) -> typing.Any: r""" Sends a WebSocket request. + + # Arguments + + * `url` - The URL to send the WebSocket request to. + * `**kwds` - Additional WebSocket request parameters. + + # Returns + + A `WebSocket` object representing the WebSocket connection. + + # Examples + + ```python + import rnet + import asyncio + + async def main(): + client = rnet.Client() + ws = await client.websocket("wss://echo.websocket.org") + await ws.send(rnet.Message.from_text("Hello, WebSocket!")) + message = await ws.recv() + print("Received:", message.data) + await ws.close() + + asyncio.run(main()) + ``` """ ... @@ -312,6 +338,7 @@ class ClientParams: impersonate_os: typing.Optional[ImpersonateOS] impersonate_skip_http2: typing.Optional[builtins.bool] impersonate_skip_headers: typing.Optional[builtins.bool] + base_url: typing.Optional[builtins.str] user_agent: typing.Optional[builtins.str] headers_order: typing.Optional[builtins.list[builtins.str]] referer: typing.Optional[builtins.bool] diff --git a/src/client.rs b/src/client.rs index c926ef1b..cdf1a326 100644 --- a/src/client.rs +++ b/src/client.rs @@ -50,196 +50,6 @@ pub struct Client(rquest::Client); #[gen_stub_pymethods] #[pymethods] impl Client { - /// Creates a new Client instance. - /// - /// # Arguments - /// - /// * `params` - Optional request parameters as a dictionary. - /// - /// # Returns - /// - /// A new `Client` instance. - /// - /// # Examples - /// - /// ```python - /// import asyncio - /// import rnet - /// - /// client = rnet.Client( - /// user_agent="my-app/0.0.1", - /// timeout=10, - /// ) - /// response = await client.get('https://httpbin.org/get') - /// print(response.text) - /// ``` - #[new] - #[pyo3(signature = (**kwds))] - fn new(mut kwds: Option) -> PyResult { - let params = kwds.get_or_insert_default(); - let mut builder = rquest::Client::builder(); - - // Impersonation options. - if let Some(impersonate) = params.impersonate.take() { - builder = builder.impersonate( - rquest::Impersonate::builder() - .impersonate(impersonate.into()) - .impersonate_os(params.impersonate_os.unwrap_or_default().into()) - .skip_http2(params.impersonate_skip_http2.unwrap_or(false)) - .skip_headers(params.impersonate_skip_headers.unwrap_or(false)) - .build(), - ); - } - - // User agent options. - apply_option!(apply_if_some, builder, params.user_agent, user_agent); - - // Headers options. - if let Some(default_headers) = params.default_headers.take() { - let mut headers = HeaderMap::with_capacity(default_headers.len()); - for (key, value) in default_headers.into_iter() { - let name = HeaderName::from_bytes(key.as_bytes()) - .map_err(wrap_invali_header_name_error)?; - headers.insert(name, value); - } - } - - // Headers order options. - if let Some(headers_order) = params.headers_order.take() { - let mut names = Vec::with_capacity(headers_order.len()); - for name in headers_order { - let name = HeaderName::from_bytes(name.as_bytes()) - .map_err(wrap_invali_header_name_error)?; - names.push(name); - } - builder = builder.headers_order(names); - } - - // Referer options. - apply_option!(apply_if_some, builder, params.referer, referer); - - // Allow redirects options. - apply_option!( - apply_option_or_default_with_value, - builder, - params.allow_redirects, - redirect, - false, - Policy::default() - ); - - // Cookie store options. - apply_option!(apply_if_some, builder, params.cookie_store, cookie_store); - - // Timeout options. - apply_option!( - apply_transformed_option, - builder, - params.timeout, - timeout, - Duration::from_secs - ); - apply_option!( - apply_transformed_option, - builder, - params.connect_timeout, - connect_timeout, - Duration::from_secs - ); - apply_option!( - apply_transformed_option, - builder, - params.read_timeout, - read_timeout, - Duration::from_secs - ); - apply_option!( - apply_option_or_default, - builder, - params.no_keepalive, - no_keepalive, - false - ); - apply_option!( - apply_transformed_option, - builder, - params.tcp_keepalive, - tcp_keepalive, - Duration::from_secs - ); - apply_option!( - apply_transformed_option, - builder, - params.pool_idle_timeout, - pool_idle_timeout, - Duration::from_secs - ); - apply_option!( - apply_if_some, - builder, - params.pool_max_idle_per_host, - pool_max_idle_per_host - ); - apply_option!(apply_if_some, builder, params.pool_max_size, pool_max_size); - - // Protocol options. - apply_option!( - apply_option_or_default, - builder, - params.http1_only, - http1_only, - false - ); - apply_option!( - apply_option_or_default, - builder, - params.http2_only, - http2_only, - false - ); - apply_option!(apply_if_some, builder, params.https_only, https_only); - apply_option!(apply_if_some, builder, params.tcp_nodelay, tcp_nodelay); - apply_option!( - apply_if_some, - builder, - params.http2_max_retry_count, - http2_max_retry_count - ); - apply_option!(apply_if_some, builder, params.tls_info, tls_info); - apply_option!( - apply_if_some, - builder, - params.danger_accept_invalid_certs, - danger_accept_invalid_certs - ); - - // Network options. - if let Some(proxies) = params.proxies.take() { - for proxy in proxies { - builder = builder.proxy(proxy.into()); - } - } - apply_option!( - apply_option_or_default, - builder, - params.no_proxy, - no_proxy, - false - ); - apply_option!(apply_if_some, builder, params.local_address, local_address); - rquest::cfg_bindable_device!({ - apply_option!(apply_if_some, builder, params.interface, interface); - }); - - // Compression options. - apply_option!(apply_if_some, builder, params.gzip, gzip); - apply_option!(apply_if_some, builder, params.brotli, brotli); - apply_option!(apply_if_some, builder, params.deflate, deflate); - apply_option!(apply_if_some, builder, params.zstd, zstd); - - builder.build().map(Client).map_err(wrap_rquest_error) - } - /// Sends a GET request. /// /// # Arguments @@ -583,6 +393,32 @@ impl Client { } /// Sends a WebSocket request. + /// + /// # Arguments + /// + /// * `url` - The URL to send the WebSocket request to. + /// * `**kwds` - Additional WebSocket request parameters. + /// + /// # Returns + /// + /// A `WebSocket` object representing the WebSocket connection. + /// + /// # Examples + /// + /// ```python + /// import rnet + /// import asyncio + /// + /// async def main(): + /// client = rnet.Client() + /// ws = await client.websocket("wss://echo.websocket.org") + /// await ws.send(rnet.Message.from_text("Hello, WebSocket!")) + /// message = await ws.recv() + /// print("Received:", message.data) + /// await ws.close() + /// + /// asyncio.run(main()) + /// ``` #[pyo3(signature = (url, **kwds))] pub fn websocket<'rt>( &self, @@ -595,6 +431,203 @@ impl Client { } } +#[gen_stub_pymethods] +#[pymethods] +impl Client { + /// Creates a new Client instance. + /// + /// # Arguments + /// + /// * `params` - Optional request parameters as a dictionary. + /// + /// # Returns + /// + /// A new `Client` instance. + /// + /// # Examples + /// + /// ```python + /// import asyncio + /// import rnet + /// + /// client = rnet.Client( + /// user_agent="my-app/0.0.1", + /// timeout=10, + /// ) + /// response = await client.get('https://httpbin.org/get') + /// print(response.text) + /// ``` + #[new] + #[pyo3(signature = (**kwds))] + fn new(mut kwds: Option) -> PyResult { + let params = kwds.get_or_insert_default(); + let mut builder = rquest::Client::builder(); + + // Impersonation options. + if let Some(impersonate) = params.impersonate.take() { + builder = builder.impersonate( + rquest::Impersonate::builder() + .impersonate(impersonate.into()) + .impersonate_os(params.impersonate_os.unwrap_or_default().into()) + .skip_http2(params.impersonate_skip_http2.unwrap_or(false)) + .skip_headers(params.impersonate_skip_headers.unwrap_or(false)) + .build(), + ); + } + + // Base URL options. + apply_option!(apply_if_some, builder, params.base_url, base_url); + + // User agent options. + apply_option!(apply_if_some, builder, params.user_agent, user_agent); + + // Headers options. + if let Some(default_headers) = params.default_headers.take() { + let mut headers = HeaderMap::with_capacity(default_headers.len()); + for (key, value) in default_headers.into_iter() { + let name = HeaderName::from_bytes(key.as_bytes()) + .map_err(wrap_invali_header_name_error)?; + headers.insert(name, value); + } + } + + // Headers order options. + if let Some(headers_order) = params.headers_order.take() { + let mut names = Vec::with_capacity(headers_order.len()); + for name in headers_order { + let name = HeaderName::from_bytes(name.as_bytes()) + .map_err(wrap_invali_header_name_error)?; + names.push(name); + } + builder = builder.headers_order(names); + } + + // Referer options. + apply_option!(apply_if_some, builder, params.referer, referer); + + // Allow redirects options. + apply_option!( + apply_option_or_default_with_value, + builder, + params.allow_redirects, + redirect, + false, + Policy::default() + ); + + // Cookie store options. + apply_option!(apply_if_some, builder, params.cookie_store, cookie_store); + + // Timeout options. + apply_option!( + apply_transformed_option, + builder, + params.timeout, + timeout, + Duration::from_secs + ); + apply_option!( + apply_transformed_option, + builder, + params.connect_timeout, + connect_timeout, + Duration::from_secs + ); + apply_option!( + apply_transformed_option, + builder, + params.read_timeout, + read_timeout, + Duration::from_secs + ); + apply_option!( + apply_option_or_default, + builder, + params.no_keepalive, + no_keepalive, + false + ); + apply_option!( + apply_transformed_option, + builder, + params.tcp_keepalive, + tcp_keepalive, + Duration::from_secs + ); + apply_option!( + apply_transformed_option, + builder, + params.pool_idle_timeout, + pool_idle_timeout, + Duration::from_secs + ); + apply_option!( + apply_if_some, + builder, + params.pool_max_idle_per_host, + pool_max_idle_per_host + ); + apply_option!(apply_if_some, builder, params.pool_max_size, pool_max_size); + + // Protocol options. + apply_option!( + apply_option_or_default, + builder, + params.http1_only, + http1_only, + false + ); + apply_option!( + apply_option_or_default, + builder, + params.http2_only, + http2_only, + false + ); + apply_option!(apply_if_some, builder, params.https_only, https_only); + apply_option!(apply_if_some, builder, params.tcp_nodelay, tcp_nodelay); + apply_option!( + apply_if_some, + builder, + params.http2_max_retry_count, + http2_max_retry_count + ); + apply_option!(apply_if_some, builder, params.tls_info, tls_info); + apply_option!( + apply_if_some, + builder, + params.danger_accept_invalid_certs, + danger_accept_invalid_certs + ); + + // Network options. + if let Some(proxies) = params.proxies.take() { + for proxy in proxies { + builder = builder.proxy(proxy.into()); + } + } + apply_option!( + apply_option_or_default, + builder, + params.no_proxy, + no_proxy, + false + ); + apply_option!(apply_if_some, builder, params.local_address, local_address); + rquest::cfg_bindable_device!({ + apply_option!(apply_if_some, builder, params.interface, interface); + }); + + // Compression options. + apply_option!(apply_if_some, builder, params.gzip, gzip); + apply_option!(apply_if_some, builder, params.brotli, brotli); + apply_option!(apply_if_some, builder, params.deflate, deflate); + apply_option!(apply_if_some, builder, params.zstd, zstd); + + builder.build().map(Client).map_err(wrap_rquest_error) + } +} + /// Executes an HTTP request. async fn execute_request( client: rquest::Client, diff --git a/src/param/client.rs b/src/param/client.rs index d58ae27c..c3c7f3df 100644 --- a/src/param/client.rs +++ b/src/param/client.rs @@ -50,6 +50,10 @@ pub struct ClientParams { #[pyo3(get)] pub impersonate_skip_headers: Option, + /// The base URL to use for the request. + #[pyo3(get)] + pub base_url: Option, + /// The user agent to use for the request. #[pyo3(get)] pub user_agent: Option, @@ -183,6 +187,7 @@ impl<'py> FromPyObject<'py> for ClientParams { extract_option!(ob, params, impersonate_skip_http2); extract_option!(ob, params, impersonate_skip_headers); + extract_option!(ob, params, base_url); extract_option!(ob, params, user_agent); extract_option!(ob, params, default_headers); extract_option!(ob, params, headers_order);