From c26757f168a81d26d8b777adf7c7094254598d6a Mon Sep 17 00:00:00 2001 From: gngpp Date: Mon, 14 Apr 2025 23:10:44 +0800 Subject: [PATCH 1/5] init --- Cargo.toml | 2 +- src/async_impl/client.rs | 46 ++++++++++++++++++++++++++++--------- src/async_impl/request.rs | 11 ++++----- src/blocking/client.rs | 37 +++++++++++++++++++++++++---- src/typing/headers.rs | 46 ++++++++++++++++++++++++++----------- src/typing/ipaddr.rs | 22 +++++++++++------- src/typing/mod.rs | 26 +++++++++++++++++++-- src/typing/param/client.rs | 8 +++---- src/typing/param/request.rs | 4 ++-- src/typing/param/ws.rs | 5 ++-- src/typing/proxy.rs | 20 ++++++++++++++++ 11 files changed, 171 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c086fb5..8da03a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ name = "stubgen" path = "src/stubgen.rs" [features] -default = [] +default = ["docs"] docs = ["dep:pyo3-stub-gen"] [dependencies] diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index b8365e53..5a08a2e9 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -5,7 +5,8 @@ use crate::{ dns, error::Error, typing::{ - Cookie, HeaderMap, Method, SslVerify, TlsVersion, + Cookie, HeaderMap, HeaderMapExtractor, HeadersOrderExtractor, ImpersonateExtractor, + IpAddrExtractor, Method, ProxyListExtractor, SslVerify, TlsVersion, param::{ClientParams, RequestParams, UpdateClientParams, WebSocketParams}, }, }; @@ -14,8 +15,8 @@ use pyo3_async_runtimes::tokio::future_into_py; #[cfg(feature = "docs")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use rquest::{CertStore, Url, redirect::Policy}; +use std::ops::Deref; use std::time::Duration; -use std::{net::IpAddr, ops::Deref}; /// A client for making HTTP requests. #[cfg_attr(feature = "docs", gen_stub_pyclass)] @@ -597,11 +598,10 @@ impl Client { false ); apply_option!( - apply_transformed_option, + apply_if_some_inner, builder, params.local_address, - local_address, - IpAddr::from + local_address ); #[cfg(any( target_os = "android", @@ -783,10 +783,35 @@ impl Client { /// proxies=[rnet.Proxy.all("http://proxy.example.com:8080")], /// ) /// ``` - #[pyo3(signature = (**kwds))] - pub fn update(&self, py: Python, mut kwds: Option) -> PyResult<()> { + #[pyo3(signature = ( + impersonate=None, + headers=None, + headers_order=None, + proxies=None, + local_address=None, + interface=None, + ))] + pub fn update( + &self, + py: Python, + impersonate: Option, + headers: Option, + headers_order: Option, + proxies: Option, + local_address: Option, + interface: Option, + ) -> PyResult<()> { py.allow_threads(|| { - let params = kwds.get_or_insert_default(); + // Create a new client with the current configuration. + let mut params = UpdateClientParams { + impersonate, + headers, + headers_order, + proxies, + local_address, + interface, + }; + let mut update = self.0.update(); // Impersonation options. @@ -810,11 +835,10 @@ impl Client { // Network options. apply_option!(apply_if_some_inner, update, params.proxies, proxies); apply_option!( - apply_transformed_option, + apply_if_some_inner, update, params.local_address, - local_address, - IpAddr::from + local_address ); #[cfg(any( target_os = "android", diff --git a/src/async_impl/request.rs b/src/async_impl/request.rs index ebd02aad..c0b88ca3 100644 --- a/src/async_impl/request.rs +++ b/src/async_impl/request.rs @@ -9,7 +9,6 @@ use crate::{ use pyo3::PyResult; use rquest::redirect::Policy; use rquest::{Client, header}; -use std::net::IpAddr; use std::time::Duration; /// Executes an HTTP request. @@ -67,11 +66,10 @@ where // Network options. apply_option!(apply_if_some_inner, builder, params.proxy, proxy); apply_option!( - apply_transformed_option, + apply_if_some_inner, builder, params.local_address, - local_address, - IpAddr::from + local_address ); #[cfg(any( target_os = "android", @@ -199,11 +197,10 @@ where // Network options. apply_option!(apply_if_some_inner, builder, params.proxy, proxy); apply_option!( - apply_transformed_option, + apply_if_some_inner, builder, params.local_address, - local_address, - IpAddr::from + local_address ); #[cfg(any( target_os = "android", diff --git a/src/blocking/client.rs b/src/blocking/client.rs index 2cefb560..48f3a583 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -1,8 +1,11 @@ use super::{BlockingResponse, BlockingWebSocket}; use crate::{ async_impl::{self, execute_request, execute_websocket_request}, - typing::param::{ClientParams, RequestParams, UpdateClientParams, WebSocketParams}, - typing::{Cookie, HeaderMap, Method}, + typing::{ + Cookie, HeaderMap, HeaderMapExtractor, HeadersOrderExtractor, ImpersonateExtractor, + IpAddrExtractor, Method, ProxyListExtractor, + param::{ClientParams, RequestParams, WebSocketParams}, + }, }; use pyo3::{prelude::*, pybacked::PyBackedStr}; #[cfg(feature = "docs")] @@ -520,9 +523,33 @@ impl BlockingClient { /// proxies=[rnet.Proxy.all("http://proxy.example.com:8080")], /// ) /// ``` - #[pyo3(signature = (**kwds))] + #[pyo3(signature = ( + impersonate=None, + headers=None, + headers_order=None, + proxies=None, + local_address=None, + interface=None, + ))] #[inline(always)] - fn update(&self, py: Python, kwds: Option) -> PyResult<()> { - self.0.update(py, kwds) + fn update( + &self, + py: Python, + impersonate: Option, + headers: Option, + headers_order: Option, + proxies: Option, + local_address: Option, + interface: Option, + ) -> PyResult<()> { + self.0.update( + py, + impersonate, + headers, + headers_order, + proxies, + local_address, + interface, + ) } } diff --git a/src/typing/headers.rs b/src/typing/headers.rs index 89687f78..e4056db0 100644 --- a/src/typing/headers.rs +++ b/src/typing/headers.rs @@ -226,7 +226,20 @@ impl FromPyObject<'_> for HeaderMapExtractor { } } -#[cfg(feature = "docs")] +impl<'py> FromPyObject<'py> for HeadersOrderExtractor { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let list = ob.downcast::()?; + list.iter() + .try_fold(Vec::with_capacity(list.len()), |mut order, name| { + let name = name.extract::()?; + let name = HeaderName::from_bytes(name.as_bytes()).map_err(Error::from)?; + order.push(name); + Ok(order) + }) + .map(Self) + } +} + impl<'py> IntoPyObject<'py> for HeaderMapExtractor { type Target = HeaderMap; @@ -234,8 +247,20 @@ impl<'py> IntoPyObject<'py> for HeaderMapExtractor { type Error = PyErr; - fn into_pyobject(self, py: Python<'py>) -> Result { - HeaderMap(self.0).into_pyobject(py) + fn into_pyobject(self, _: Python<'py>) -> Result { + todo!("HeaderMapExtractor::into_pyobject is not implemented yet"); + } +} + +impl<'py> IntoPyObject<'py> for HeadersOrderExtractor { + type Target = Vec; + + type Output = Bound<'py, Self::Target>; + + type Error = PyErr; + + fn into_pyobject(self, _: Python<'py>) -> Result { + todo!("HeadersOrderExtractor::into_pyobject is not implemented yet"); } } @@ -249,16 +274,9 @@ impl PyStubType for HeaderMapExtractor { } } -impl<'py> FromPyObject<'py> for HeadersOrderExtractor { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let list = ob.downcast::()?; - list.iter() - .try_fold(Vec::with_capacity(list.len()), |mut order, name| { - let name = name.extract::()?; - let name = HeaderName::from_bytes(name.as_bytes()).map_err(Error::from)?; - order.push(name); - Ok(order) - }) - .map(Self) +#[cfg(feature = "docs")] +impl pyo3_stub_gen::PyStubType for HeadersOrderExtractor { + fn type_output() -> pyo3_stub_gen::TypeInfo { + pyo3_stub_gen::TypeInfo::with_module("typing.Optional[typing.List[str]]", "typing".into()) } } diff --git a/src/typing/ipaddr.rs b/src/typing/ipaddr.rs index 94a61bb3..70f40480 100644 --- a/src/typing/ipaddr.rs +++ b/src/typing/ipaddr.rs @@ -7,22 +7,28 @@ use pyo3_stub_gen::{ /// An IP address. #[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct IpAddr(std::net::IpAddr); +pub struct IpAddrExtractor(pub std::net::IpAddr); -impl From for std::net::IpAddr { - fn from(ip: IpAddr) -> Self { - ip.0 +impl FromPyObject<'_> for IpAddrExtractor { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + ob.extract().map(IpAddrExtractor) } } -impl FromPyObject<'_> for IpAddr { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - ob.extract().map(IpAddr) +impl<'py> IntoPyObject<'py> for IpAddrExtractor { + type Target = IpAddrExtractor; + + type Output = Bound<'py, Self::Target>; + + type Error = PyErr; + + fn into_pyobject(self, _: Python<'py>) -> Result { + todo!("IpAddrExtractor::into_pyobject is not implemented yet"); } } #[cfg(feature = "docs")] -impl PyStubType for IpAddr { +impl PyStubType for IpAddrExtractor { fn type_output() -> TypeInfo { TypeInfo::with_module( "typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]", diff --git a/src/typing/mod.rs b/src/typing/mod.rs index c1d5d06a..0f79a339 100644 --- a/src/typing/mod.rs +++ b/src/typing/mod.rs @@ -18,10 +18,10 @@ pub use self::{ HeaderMap, HeaderMapExtractor, HeaderMapItemsIter, HeaderMapKeysIter, HeaderMapValuesIter, HeadersOrderExtractor, }, - ipaddr::{IpAddr, SocketAddr}, + ipaddr::{IpAddrExtractor, SocketAddr}, json::Json, multipart::{Multipart, Part}, - proxy::{Proxy, ProxyExtractor}, + proxy::{Proxy, ProxyExtractor, ProxyListExtractor}, ssl::SslVerify, status::StatusCode, }; @@ -82,6 +82,28 @@ impl FromPyObject<'_> for ImpersonateExtractor { } } +impl<'py> IntoPyObject<'py> for ImpersonateExtractor { + type Target = EmulationOption; + + type Output = Bound<'py, Self::Target>; + + type Error = PyErr; + + fn into_pyobject(self, _: Python<'_>) -> Result { + todo!("ImpersonateExtractor::into_pyobject is not implemented yet"); + } +} + +#[cfg(feature = "docs")] +impl pyo3_stub_gen::PyStubType for ImpersonateExtractor { + fn type_output() -> pyo3_stub_gen::TypeInfo { + pyo3_stub_gen::TypeInfo::with_module( + "typing.Optional[Impersonate | ImpersonateOption]", + "typing".into(), + ) + } +} + /// A struct to represent the `ImpersonateOption` class. #[cfg_attr(feature = "docs", gen_stub_pyclass)] #[pyclass] diff --git a/src/typing/param/client.rs b/src/typing/param/client.rs index 0c4b33ac..99585abd 100644 --- a/src/typing/param/client.rs +++ b/src/typing/param/client.rs @@ -1,8 +1,8 @@ use crate::{ extract_option, typing::{ - HeaderMapExtractor, HeadersOrderExtractor, ImpersonateExtractor, IpAddr, LookupIpStrategy, - SslVerify, TlsVersion, proxy::ProxyListExtractor, + HeaderMapExtractor, HeadersOrderExtractor, ImpersonateExtractor, IpAddrExtractor, + LookupIpStrategy, SslVerify, TlsVersion, proxy::ProxyListExtractor, }, }; use pyo3::{prelude::*, pybacked::PyBackedStr}; @@ -101,7 +101,7 @@ pub struct ClientParams { pub proxies: Option, /// Bind to a local IP Address. - pub local_address: Option, + pub local_address: Option, /// Bind to an interface by `SO_BINDTODEVICE`. pub interface: Option, @@ -137,7 +137,7 @@ pub struct UpdateClientParams { pub proxies: Option, /// Bind to a local IP Address. - pub local_address: Option, + pub local_address: Option, /// Bind to an interface by `SO_BINDTODEVICE`. pub interface: Option, diff --git a/src/typing/param/request.rs b/src/typing/param/request.rs index f0402f6a..91a22325 100644 --- a/src/typing/param/request.rs +++ b/src/typing/param/request.rs @@ -1,7 +1,7 @@ use crate::{ extract_option, typing::{ - BodyExtractor, CookieExtractor, HeaderMapExtractor, IpAddr, Json, ProxyExtractor, + BodyExtractor, CookieExtractor, HeaderMapExtractor, IpAddrExtractor, Json, ProxyExtractor, UrlEncodedValuesExtractor, Version, multipart::MultipartExtractor, }, }; @@ -16,7 +16,7 @@ pub struct RequestParams { pub proxy: Option, /// Bind to a local IP Address. - pub local_address: Option, + pub local_address: Option, /// Bind to an interface by `SO_BINDTODEVICE`. pub interface: Option, diff --git a/src/typing/param/ws.rs b/src/typing/param/ws.rs index 6922eb1e..f13e5bfa 100644 --- a/src/typing/param/ws.rs +++ b/src/typing/param/ws.rs @@ -1,7 +1,8 @@ use crate::{ extract_option, typing::{ - CookieExtractor, HeaderMapExtractor, IpAddr, ProxyExtractor, UrlEncodedValuesExtractor, + CookieExtractor, HeaderMapExtractor, IpAddrExtractor, ProxyExtractor, + UrlEncodedValuesExtractor, }, }; use pyo3::{prelude::*, pybacked::PyBackedStr}; @@ -15,7 +16,7 @@ pub struct WebSocketParams { pub proxy: Option, /// Bind to a local IP Address. - pub local_address: Option, + pub local_address: Option, /// Bind to an interface by `SO_BINDTODEVICE`. pub interface: Option, diff --git a/src/typing/proxy.rs b/src/typing/proxy.rs index d1b8b673..eab7023e 100644 --- a/src/typing/proxy.rs +++ b/src/typing/proxy.rs @@ -188,6 +188,7 @@ impl FromPyObject<'_> for ProxyExtractor { } pub struct ProxyListExtractor(pub Vec); + impl FromPyObject<'_> for ProxyListExtractor { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { let proxies = ob.downcast::()?; @@ -202,3 +203,22 @@ impl FromPyObject<'_> for ProxyListExtractor { .map(Self) } } + +impl<'py> IntoPyObject<'py> for ProxyListExtractor { + type Target = ProxyListExtractor; + + type Output = Bound<'py, Self::Target>; + + type Error = PyErr; + + fn into_pyobject(self, _: Python<'py>) -> Result { + todo!("ProxyListExtractor::into_pyobject is not implemented yet"); + } +} + +#[cfg(feature = "docs")] +impl pyo3_stub_gen::PyStubType for ProxyListExtractor { + fn type_output() -> pyo3_stub_gen::TypeInfo { + pyo3_stub_gen::TypeInfo::with_module("typing.Optional[typing.List[Proxy]]", "typing".into()) + } +} From d81f0eaec149988d932a27c32846e9f2b93859f5 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 15 Apr 2025 16:08:23 +0800 Subject: [PATCH 2/5] update --- examples/client.py | 21 ++++---- rnet.pyi | 29 ++++++++++- src/async_impl/client.rs | 13 +++-- src/typing/enums.rs | 69 +------------------------ src/typing/headers.rs | 51 ++++-------------- src/typing/ipaddr.rs | 32 +++--------- src/typing/macros.rs | 103 +++++++++++++++++++++++++++++++++++++ src/typing/mod.rs | 29 ++++------- src/typing/param/client.rs | 21 -------- src/typing/proxy.rs | 31 +++++------ 10 files changed, 189 insertions(+), 210 deletions(-) create mode 100644 src/typing/macros.rs diff --git a/examples/client.py b/examples/client.py index 93de4b51..a266866e 100644 --- a/examples/client.py +++ b/examples/client.py @@ -30,17 +30,18 @@ async def main(): client = Client( impersonate=Impersonate.Firefox133, user_agent="rnet", - proxies=[ - Proxy.http("socks5h://abc:def@127.0.0.1:6152"), - Proxy.https(url="socks5h://127.0.0.1:6153", username="abc", password="def"), - Proxy.http(url="http://abc:def@127.0.0.1:6152", custom_http_auth="abcedf"), - Proxy.all( - url="socks5h://abc:def@127.0.0.1:6153", - exclusion="google.com, facebook.com, twitter.com", - ), - ], + allow_redirects=True, + # proxies=[ + # Proxy.http("socks5h://abc:def@127.0.0.1:6152"), + # Proxy.https(url="socks5h://127.0.0.1:6153", username="abc", password="def"), + # Proxy.http(url="http://abc:def@127.0.0.1:6152", custom_http_auth="abcedf"), + # Proxy.all( + # url="socks5h://abc:def@127.0.0.1:6153", + # exclusion="google.com, facebook.com, twitter.com", + # ), + # ], ) - resp = await client.get("https://api.ip.sb/ip") + resp = await client.get("https://money-tourism.gr/en") print("Status Code: ", resp.status_code) print("Version: ", resp.version) print("Response URL: ", resp.url) diff --git a/rnet.pyi b/rnet.pyi index 4b741666..2fdfdf21 100644 --- a/rnet.pyi +++ b/rnet.pyi @@ -3,6 +3,7 @@ import builtins import datetime +import ipaddress import typing from enum import Enum, auto @@ -71,7 +72,19 @@ class BlockingClient: """ ... - def update(self, **kwds) -> None: + def update( + self, + impersonate: typing.Optional[ + typing.Union[Impersonate, ImpersonateOption] + ] = None, + headers: typing.Optional[typing.Union[typing.Dict[str, str], HeaderMap]] = None, + headers_order: typing.Optional[typing.List[str]] = None, + proxies: typing.Optional[typing.List[Proxy]] = None, + local_address: typing.Optional[ + typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] + ] = None, + interface: typing.Optional[builtins.str] = None, + ) -> None: r""" Updates the client with the given parameters. @@ -836,7 +849,19 @@ class Client: """ ... - def update(self, **kwds) -> None: + def update( + self, + impersonate: typing.Optional[ + typing.Union[Impersonate, ImpersonateOption] + ] = None, + headers: typing.Optional[typing.Union[typing.Dict[str, str], HeaderMap]] = None, + headers_order: typing.Optional[typing.List[str]] = None, + proxies: typing.Optional[typing.List[Proxy]] = None, + local_address: typing.Optional[ + typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] + ] = None, + interface: typing.Optional[builtins.str] = None, + ) -> None: r""" Updates the client with the given parameters. diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 5a08a2e9..2c384b10 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -762,14 +762,13 @@ impl Client { /// Updates the client with the given parameters. /// /// # Arguments - /// * `**kwds` - The parameters to update the client with. /// - /// impersonate: typing.Optional[typing.Union[Impersonate, ImpersonateOption]] - /// headers: typing.Optional[typing.Dict[str, bytes]] - /// headers_order: typing.Optional[typing.List[str]] - /// proxies: typing.Optional[builtins.list[Proxy]] - /// local_address: typing.Optional[typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]] - /// interface: typing.Optional[builtins.str] + /// * `impersonate` - The impersonation settings for the request. + /// * `headers` - The headers to use for the request. + /// * `headers_order` - The order of the headers to use for the request. + /// * `proxies` - The proxy to use for the request. + /// * `local_address` - The local IP address to bind to. + /// * `interface` - The interface to bind to. /// /// # Examples /// diff --git a/src/typing/enums.rs b/src/typing/enums.rs index d97de12d..8ffac230 100644 --- a/src/typing/enums.rs +++ b/src/typing/enums.rs @@ -1,72 +1,5 @@ +use crate::define_enum_with_conversion; use pyo3::prelude::*; -#[cfg(feature = "docs")] -use pyo3_stub_gen::derive::gen_stub_pyclass_enum; - -macro_rules! define_enum_with_conversion { - ($(#[$meta:meta])* $enum_type:ident, $ffi_type:ty, $($variant:ident),* $(,)?) => { - define_enum_with_conversion!($(#[$meta])* $enum_type, $ffi_type, $( ($variant, $variant) ),*); - }; - - ($(#[$meta:meta])* const, $enum_type:ident, $ffi_type:ty, $($variant:ident),* $(,)?) => { - define_enum_with_conversion!($(#[$meta])* const, $enum_type, $ffi_type, $( ($variant, $variant) ),*); - }; - - ($(#[$meta:meta])* $enum_type:ident, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { - $(#[$meta])* - #[cfg_attr(feature = "docs", gen_stub_pyclass_enum)] - #[pyclass(eq, eq_int)] - #[derive(Clone, Copy, PartialEq, Eq, Hash)] - #[allow(non_camel_case_types)] - #[allow(clippy::upper_case_acronyms)] - pub enum $enum_type { - $($rust_variant),* - } - - impl $enum_type { - pub fn into_ffi(self) -> $ffi_type { - match self { - $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* - } - } - - pub fn from_ffi(ffi: $ffi_type) -> Self { - #[allow(unreachable_patterns)] - match ffi { - $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* - _ => unreachable!(), - } - } - } - }; - - ($(#[$meta:meta])* const, $enum_type:ident, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { - $(#[$meta])* - #[cfg_attr(feature = "docs", gen_stub_pyclass_enum)] - #[pyclass(eq, eq_int)] - #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] - #[allow(non_camel_case_types)] - #[allow(clippy::upper_case_acronyms)] - pub enum $enum_type { - $($rust_variant),* - } - - impl $enum_type { - pub const fn into_ffi(self) -> $ffi_type { - match self { - $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* - } - } - - pub const fn from_ffi(ffi: $ffi_type) -> Self { - #[allow(unreachable_patterns)] - match ffi { - $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* - _ => unreachable!(), - } - } - } - }; -} define_enum_with_conversion!( /// An HTTP version. diff --git a/src/typing/headers.rs b/src/typing/headers.rs index e4056db0..7135dd8c 100644 --- a/src/typing/headers.rs +++ b/src/typing/headers.rs @@ -1,5 +1,6 @@ use crate::{ buffer::{HeaderNameBuffer, HeaderValueBuffer, PyBufferProtocol}, + define_into_pyobject_todo, define_py_stub_gen, error::Error, }; use pyo3::{ @@ -8,10 +9,7 @@ use pyo3::{ types::{PyDict, PyList}, }; #[cfg(feature = "docs")] -use pyo3_stub_gen::{ - PyStubType, TypeInfo, - derive::{gen_stub_pyclass, gen_stub_pymethods}, -}; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use rquest::header::{self, HeaderName, HeaderValue}; /// A HTTP header map. @@ -240,43 +238,14 @@ impl<'py> FromPyObject<'py> for HeadersOrderExtractor { } } -impl<'py> IntoPyObject<'py> for HeaderMapExtractor { - type Target = HeaderMap; - - type Output = Bound<'py, Self::Target>; - - type Error = PyErr; +define_into_pyobject_todo!(HeaderMapExtractor); - fn into_pyobject(self, _: Python<'py>) -> Result { - todo!("HeaderMapExtractor::into_pyobject is not implemented yet"); - } -} +define_into_pyobject_todo!(HeadersOrderExtractor); -impl<'py> IntoPyObject<'py> for HeadersOrderExtractor { - type Target = Vec; +define_py_stub_gen!( + HeaderMapExtractor, + "typing.Union[typing.Dict[str, str], HeaderMap]", + "typing" +); - type Output = Bound<'py, Self::Target>; - - type Error = PyErr; - - fn into_pyobject(self, _: Python<'py>) -> Result { - todo!("HeadersOrderExtractor::into_pyobject is not implemented yet"); - } -} - -#[cfg(feature = "docs")] -impl PyStubType for HeaderMapExtractor { - fn type_output() -> TypeInfo { - TypeInfo::with_module( - "typing.Union[typing.Dict[str, str], HeaderMap]", - "typing".into(), - ) - } -} - -#[cfg(feature = "docs")] -impl pyo3_stub_gen::PyStubType for HeadersOrderExtractor { - fn type_output() -> pyo3_stub_gen::TypeInfo { - pyo3_stub_gen::TypeInfo::with_module("typing.Optional[typing.List[str]]", "typing".into()) - } -} +define_py_stub_gen!(HeadersOrderExtractor, "typing.List[str]", "typing"); diff --git a/src/typing/ipaddr.rs b/src/typing/ipaddr.rs index 70f40480..7c8c95f4 100644 --- a/src/typing/ipaddr.rs +++ b/src/typing/ipaddr.rs @@ -1,9 +1,7 @@ +use crate::{define_into_pyobject_todo, define_py_stub_gen}; use pyo3::{IntoPyObjectExt, prelude::*}; #[cfg(feature = "docs")] -use pyo3_stub_gen::{ - PyStubType, TypeInfo, - derive::{gen_stub_pyclass, gen_stub_pymethods}, -}; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; /// An IP address. #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -15,27 +13,13 @@ impl FromPyObject<'_> for IpAddrExtractor { } } -impl<'py> IntoPyObject<'py> for IpAddrExtractor { - type Target = IpAddrExtractor; +define_into_pyobject_todo!(IpAddrExtractor); - type Output = Bound<'py, Self::Target>; - - type Error = PyErr; - - fn into_pyobject(self, _: Python<'py>) -> Result { - todo!("IpAddrExtractor::into_pyobject is not implemented yet"); - } -} - -#[cfg(feature = "docs")] -impl PyStubType for IpAddrExtractor { - fn type_output() -> TypeInfo { - TypeInfo::with_module( - "typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]", - "ipaddress".into(), - ) - } -} +define_py_stub_gen!( + IpAddrExtractor, + "typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]", + "ipaddress" +); /// A IP socket address. #[cfg_attr(feature = "docs", gen_stub_pyclass)] diff --git a/src/typing/macros.rs b/src/typing/macros.rs new file mode 100644 index 00000000..5bf66eaa --- /dev/null +++ b/src/typing/macros.rs @@ -0,0 +1,103 @@ +#[macro_export] +macro_rules! define_enum_with_conversion { + ($(#[$meta:meta])* $enum_type:ident, $ffi_type:ty, $($variant:ident),* $(,)?) => { + define_enum_with_conversion!($(#[$meta])* $enum_type, $ffi_type, $( ($variant, $variant) ),*); + }; + + ($(#[$meta:meta])* const, $enum_type:ident, $ffi_type:ty, $($variant:ident),* $(,)?) => { + define_enum_with_conversion!($(#[$meta])* const, $enum_type, $ffi_type, $( ($variant, $variant) ),*); + }; + + ($(#[$meta:meta])* $enum_type:ident, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { + $(#[$meta])* + #[cfg_attr(feature = "docs", pyo3_stub_gen::derive::gen_stub_pyclass_enum)] + #[pyclass(eq, eq_int)] + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + #[allow(non_camel_case_types)] + #[allow(clippy::upper_case_acronyms)] + pub enum $enum_type { + $($rust_variant),* + } + + impl $enum_type { + pub fn into_ffi(self) -> $ffi_type { + match self { + $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* + } + } + + pub fn from_ffi(ffi: $ffi_type) -> Self { + #[allow(unreachable_patterns)] + match ffi { + $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* + _ => unreachable!(), + } + } + } + }; + + ($(#[$meta:meta])* const, $enum_type:ident, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => { + $(#[$meta])* + #[cfg_attr(feature = "docs", pyo3_stub_gen::derive::gen_stub_pyclass_enum)] + #[pyclass(eq, eq_int)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] + #[allow(non_camel_case_types)] + #[allow(clippy::upper_case_acronyms)] + pub enum $enum_type { + $($rust_variant),* + } + + impl $enum_type { + pub const fn into_ffi(self) -> $ffi_type { + match self { + $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)* + } + } + + pub const fn from_ffi(ffi: $ffi_type) -> Self { + #[allow(unreachable_patterns)] + match ffi { + $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)* + _ => unreachable!(), + } + } + } + }; +} + +#[macro_export] +macro_rules! define_py_stub_gen { + ($type_name:ty, $type_str:expr, $module:expr) => { + #[cfg(feature = "docs")] + impl pyo3_stub_gen::PyStubType for $type_name { + fn type_output() -> pyo3_stub_gen::TypeInfo { + pyo3_stub_gen::TypeInfo::with_module($type_str, $module.into()) + } + } + }; +} + +#[macro_export] +macro_rules! define_into_pyobject_todo { + ($type_name:ty, $msg:expr) => { + impl<'py> pyo3::conversion::IntoPyObject<'py> for $type_name { + type Target = $type_name; + type Output = pyo3::prelude::Bound<'py, Self::Target>; + type Error = pyo3::PyErr; + + fn into_pyobject(self, _: pyo3::Python<'py>) -> Result { + todo!($msg); + } + } + }; + + ($type_name:ty) => { + define_into_pyobject_todo!( + $type_name, + concat!( + stringify!($type_name), + "::into_pyobject is not implemented yet" + ) + ); + }; +} diff --git a/src/typing/mod.rs b/src/typing/mod.rs index 0f79a339..0f1b3f48 100644 --- a/src/typing/mod.rs +++ b/src/typing/mod.rs @@ -4,12 +4,15 @@ mod enums; mod headers; mod ipaddr; mod json; +mod macros; mod multipart; pub mod param; mod proxy; mod ssl; mod status; +use crate::{define_into_pyobject_todo, define_py_stub_gen}; + pub use self::{ body::BodyExtractor, cookie::{Cookie, CookieExtractor}, @@ -82,27 +85,13 @@ impl FromPyObject<'_> for ImpersonateExtractor { } } -impl<'py> IntoPyObject<'py> for ImpersonateExtractor { - type Target = EmulationOption; - - type Output = Bound<'py, Self::Target>; - - type Error = PyErr; +define_into_pyobject_todo!(ImpersonateExtractor); - fn into_pyobject(self, _: Python<'_>) -> Result { - todo!("ImpersonateExtractor::into_pyobject is not implemented yet"); - } -} - -#[cfg(feature = "docs")] -impl pyo3_stub_gen::PyStubType for ImpersonateExtractor { - fn type_output() -> pyo3_stub_gen::TypeInfo { - pyo3_stub_gen::TypeInfo::with_module( - "typing.Optional[Impersonate | ImpersonateOption]", - "typing".into(), - ) - } -} +define_py_stub_gen!( + ImpersonateExtractor, + "typing.Union[Impersonate, ImpersonateOption]", + "typing" +); /// A struct to represent the `ImpersonateOption` class. #[cfg_attr(feature = "docs", gen_stub_pyclass)] diff --git a/src/typing/param/client.rs b/src/typing/param/client.rs index 99585abd..347fa8c5 100644 --- a/src/typing/param/client.rs +++ b/src/typing/param/client.rs @@ -121,7 +121,6 @@ pub struct ClientParams { } /// The parameters for updating a client. -#[derive(Default)] pub struct UpdateClientParams { /// The impersonation settings for the request. pub impersonate: Option, @@ -188,29 +187,9 @@ impl<'py> FromPyObject<'py> for ClientParams { } } -impl<'py> FromPyObject<'py> for UpdateClientParams { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let mut params = Self::default(); - extract_option!(ob, params, impersonate); - extract_option!(ob, params, headers); - extract_option!(ob, params, headers_order); - extract_option!(ob, params, proxies); - extract_option!(ob, params, local_address); - extract_option!(ob, params, interface); - Ok(params) - } -} - #[cfg(feature = "docs")] impl PyStubType for ClientParams { fn type_output() -> TypeInfo { TypeInfo::with_module("typing.Dict[str, typing.Any]", "typing".into()) } } - -#[cfg(feature = "docs")] -impl PyStubType for UpdateClientParams { - fn type_output() -> TypeInfo { - TypeInfo::with_module("typing.Dict[str, typing.Any]", "typing".into()) - } -} diff --git a/src/typing/proxy.rs b/src/typing/proxy.rs index eab7023e..970ec38c 100644 --- a/src/typing/proxy.rs +++ b/src/typing/proxy.rs @@ -1,7 +1,7 @@ -use crate::error::Error; +use crate::{define_into_pyobject_todo, define_py_stub_gen, error::Error}; use super::HeaderMapExtractor; -use pyo3::{prelude::*, types::PyList}; +use pyo3::{prelude::*, pybacked::PyBackedStr, types::PyList}; #[cfg(feature = "docs")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use rquest::header::HeaderValue; @@ -181,6 +181,14 @@ pub struct ProxyExtractor(pub rquest::Proxy); impl FromPyObject<'_> for ProxyExtractor { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + if let Ok(proxy_str) = ob.extract::() { + let proxy = rquest::Proxy::all(proxy_str.as_ref() as &str) + .map(Self) + .map_err(Error::RquestError)?; + + return Ok(proxy); + } + let proxy = ob.downcast::()?; let proxy = proxy.borrow().0.clone(); Ok(Self(proxy)) @@ -204,21 +212,10 @@ impl FromPyObject<'_> for ProxyListExtractor { } } -impl<'py> IntoPyObject<'py> for ProxyListExtractor { - type Target = ProxyListExtractor; +define_into_pyobject_todo!(ProxyExtractor); - type Output = Bound<'py, Self::Target>; +define_into_pyobject_todo!(ProxyListExtractor); - type Error = PyErr; +define_py_stub_gen!(ProxyExtractor, "typing.Union[Proxy, str]", "typing"); - fn into_pyobject(self, _: Python<'py>) -> Result { - todo!("ProxyListExtractor::into_pyobject is not implemented yet"); - } -} - -#[cfg(feature = "docs")] -impl pyo3_stub_gen::PyStubType for ProxyListExtractor { - fn type_output() -> pyo3_stub_gen::TypeInfo { - pyo3_stub_gen::TypeInfo::with_module("typing.Optional[typing.List[Proxy]]", "typing".into()) - } -} +define_py_stub_gen!(ProxyListExtractor, "typing.List[Proxy]", "typing"); From 68596a5715be3052e2362bbf0c7cab3393daec9d Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 26 Apr 2025 23:36:19 +0800 Subject: [PATCH 3/5] feat(client): partially update and append client headers --- src/async_impl/client.rs | 41 +++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index a18d24c5..5e8b8284 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -14,7 +14,11 @@ use pyo3::{prelude::*, pybacked::PyBackedStr}; use pyo3_async_runtimes::tokio::future_into_py; #[cfg(feature = "docs")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; -use rquest::{CertStore, Url, redirect::Policy}; +use rquest::{ + CertStore, Url, + header::{Entry, OccupiedEntry}, + redirect::Policy, +}; use std::ops::Deref; use std::time::Duration; @@ -814,16 +818,35 @@ impl Client { let mut update = self.0.update(); // Impersonation options. - if let Some(impersonate) = params.impersonate.take() { - update = update.emulation(impersonate.0); - } + apply_option!(apply_if_some_inner, update, params.impersonate, emulation); // Updated headers options. - if let Some(mut updated_headers) = params.headers.take() { - update = update.headers(|default_headers| { - for (name, value) in updated_headers.0.drain() { - if let Some(name) = name { - default_headers.insert(name, value); + if let Some(src) = params.headers.take() { + update = update.headers(|dst| { + // IntoIter of HeaderMap yields (Option, HeaderValue). + // The first time a name is yielded, it will be Some(name), and if + // there are more values with the same name, the next yield will be + // None. + + let mut prev_entry: Option> = None; + for (key, value) in src.0 { + match key { + Some(key) => match dst.entry(key) { + Entry::Occupied(mut e) => { + e.insert(value); + prev_entry = Some(e); + } + Entry::Vacant(e) => { + let e = e.insert_entry(value); + prev_entry = Some(e); + } + }, + None => match prev_entry { + Some(ref mut entry) => { + entry.append(value); + } + None => unreachable!("HeaderMap::into_iter yielded None first"), + }, } } }); From be8a002c18b4405c1b871c30a4680006957ef5de Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 26 Apr 2025 23:37:07 +0800 Subject: [PATCH 4/5] feat(client): partially update and append client headers --- rnet.pyi | 641 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 541 insertions(+), 100 deletions(-) diff --git a/rnet.pyi b/rnet.pyi index 2fdfdf21..514948a7 100644 --- a/rnet.pyi +++ b/rnet.pyi @@ -13,8 +13,83 @@ class BlockingClient: """ user_agent: typing.Optional[builtins.str] + r""" + Returns the user agent of the client. + + # Returns + + An optional string containing the user agent of the client. + """ headers: HeaderMap - def __new__(cls, **kwds): ... + r""" + Returns the headers of the client. + + # Returns + + A `HeaderMap` object containing the headers of the client. + """ + def __new__(cls, **kwds) -> BlockingClient: + r""" + Creates a new BlockingClient instance. + + # Arguments + + * `**kwds` - Optional request parameters as a dictionary. + + impersonate: typing.Optional[typing.Union[Impersonate, ImpersonateOption]] + user_agent: typing.Optional[str] + default_headers: typing.Optional[typing.Dict[str, bytes]] + headers_order: typing.Optional[typing.List[str]] + referer: typing.Optional[builtins.bool] + allow_redirects: typing.Optional[builtins.bool] + max_redirects: typing.Optional[builtins.int] + cookie_store: typing.Optional[builtins.bool] + lookup_ip_strategy: typing.Optional[LookupIpStrategy] + timeout: typing.Optional[builtins.int] + connect_timeout: typing.Optional[builtins.int] + read_timeout: typing.Optional[builtins.int] + no_keepalive: typing.Optional[builtins.bool] + tcp_keepalive: typing.Optional[builtins.int] + pool_idle_timeout: typing.Optional[builtins.int] + pool_max_idle_per_host: typing.Optional[builtins.int] + pool_max_size: typing.Optional[builtins.int] + http1_only: typing.Optional[builtins.bool] + http2_only: typing.Optional[builtins.bool] + https_only: typing.Optional[builtins.bool] + tcp_nodelay: typing.Optional[builtins.bool] + http2_max_retry_count: typing.Optional[builtins.int] + verify: Optional[Union[bool, Path]] + tls_info: typing.Optional[builtins.bool] + min_tls_version: typing.Optional[TlsVersion] + max_tls_version: typing.Optional[TlsVersion] + no_proxy: typing.Optional[builtins.bool] + proxies: typing.Optional[builtins.list[Proxy]] + local_address: typing.Optional[typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]] + interface: typing.Optional[builtins.str] + gzip: typing.Optional[builtins.bool] + brotli: typing.Optional[builtins.bool] + deflate: typing.Optional[builtins.bool] + zstd: typing.Optional[builtins.bool] + + # Returns + + A new `BlockingClient` instance. + + # Examples + + ```python + import asyncio + import rnet + + client = rnet.BlockingClient( + user_agent="my-app/0.0.1", + timeout=10, + ) + response = client.get('https://httpbin.org/get') + print(response.text()) + ``` + """ + def get_cookies(self, url: str) -> typing.Optional[typing.Any]: r""" Returns the cookies for the given URL. @@ -27,7 +102,6 @@ class BlockingClient: A list of cookie strings. """ - ... def set_cookie(self, url: str, cookie: Cookie) -> None: r""" @@ -46,7 +120,6 @@ class BlockingClient: client.set_cookie("https://example.com", rnet.Cookie(name="foo", value="bar")) ``` """ - ... def remove_cookie(self, url: str, name: str) -> None: r""" @@ -64,13 +137,11 @@ class BlockingClient: client = rnet.Client(cookie_store=True) client.remove_cookie("https://example.com", "foo") """ - ... def clear_cookies(self) -> None: r""" Clears the cookies for the given URL. """ - ... def update( self, @@ -111,7 +182,6 @@ class BlockingClient: ) ``` """ - ... def request( self, @@ -166,7 +236,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def websocket(self, url: str, **kwds) -> BlockingWebSocket: r""" @@ -216,7 +285,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def trace(self, url: str, **kwds) -> BlockingResponse: r""" @@ -265,7 +333,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def options(self, url: str, **kwds) -> BlockingResponse: r""" @@ -314,7 +381,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def head(self, url: str, **kwds) -> BlockingResponse: r""" @@ -363,7 +429,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def delete(self, url: str, **kwds) -> BlockingResponse: r""" @@ -412,7 +477,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def patch(self, url: str, **kwds) -> BlockingResponse: r""" @@ -461,7 +525,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def put(self, url: str, **kwds) -> BlockingResponse: r""" @@ -510,7 +573,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def post(self, url: str, **kwds) -> BlockingResponse: r""" @@ -559,7 +621,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... def get(self, url: str, **kwds) -> BlockingResponse: r""" @@ -608,7 +669,6 @@ class BlockingClient: asyncio.run(main()) ``` """ - ... class BlockingResponse: r""" @@ -616,15 +676,85 @@ class BlockingResponse: """ url: builtins.str + r""" + Returns the URL of the response. + + # Returns + + A string representing the URL of the response. + """ ok: builtins.bool + r""" + Returns whether the response is successful. + + # Returns + + A boolean indicating whether the response is successful. + """ status: builtins.int + r""" + Returns the status code as integer of the response. + + # Returns + + An integer representing the HTTP status code. + """ status_code: StatusCode + r""" + Returns the status code of the response. + + # Returns + + A Python object representing the HTTP status code. + """ version: Version + r""" + Returns the HTTP version of the response. + + # Returns + + A `Version` object representing the HTTP version of the response. + """ headers: HeaderMap + r""" + Returns the headers of the response. + + # Returns + + A `HeaderMap` object representing the headers of the response. + """ cookies: builtins.list[Cookie] + r""" + Returns the cookies of the response. + + # Returns + + A Python cookies object representing the cookies of the response. + """ content_length: builtins.int + r""" + Returns the content length of the response. + + # Returns + + An integer representing the content length of the response. + """ remote_addr: typing.Optional[SocketAddr] + r""" + Returns the remote address of the response. + + # Returns + + An `IpAddr` object representing the remote address of the response. + """ encoding: builtins.str + r""" + Encoding to decode with when accessing text. + + # Returns + + A string representing the encoding to decode with when accessing text. + """ def __enter__(self) -> BlockingResponse: ... def __exit__( self, _exc_type: typing.Any, _exc_value: typing.Any, _traceback: typing.Any @@ -637,7 +767,6 @@ class BlockingResponse: A Python object representing the TLS peer certificate of the response. """ - ... def text(self) -> builtins.str: r""" @@ -647,7 +776,6 @@ class BlockingResponse: A Python object representing the text content of the response. """ - ... def text_with_charset(self, encoding: builtins.str) -> builtins.str: r""" @@ -661,7 +789,6 @@ class BlockingResponse: A Python object representing the text content of the response. """ - ... def json(self) -> typing.Dict[str, typing.Any]: r""" @@ -671,7 +798,6 @@ class BlockingResponse: A Python object representing the JSON content of the response. """ - ... def bytes(self) -> typing.Any: r""" @@ -681,7 +807,6 @@ class BlockingResponse: A Python object representing the bytes content of the response. """ - ... def stream(self) -> BlockingStreamer: r""" @@ -691,13 +816,11 @@ class BlockingResponse: A Python object representing the stream content of the response. """ - ... def close(self) -> None: r""" Closes the response connection. """ - ... class BlockingStreamer: r""" @@ -721,12 +844,61 @@ class BlockingWebSocket: """ ok: builtins.bool + r""" + Returns whether the response is successful. + + # Returns + + A boolean indicating whether the response is successful. + """ status: builtins.int + r""" + Returns the status code as integer of the response. + + # Returns + + An integer representing the HTTP status code. + """ status_code: StatusCode + r""" + Returns the status code of the response. + + # Returns + + A Python object representing the HTTP status code. + """ version: Version + r""" + Returns the HTTP version of the response. + + # Returns + + A `Version` object representing the HTTP version of the response. + """ headers: HeaderMap + r""" + Returns the headers of the response. + + # Returns + + A `HeaderMap` object representing the headers of the response. + """ cookies: builtins.list[Cookie] + r""" + Returns the cookies of the response. + + # Returns + + A Python cookies object representing the cookies of the response. + """ remote_addr: typing.Optional[SocketAddr] + r""" + Returns the remote address of the response. + + # Returns + + An `IpAddr` object representing the remote address of the response. + """ def __iter__(self) -> BlockingWebSocket: ... def __next__(self) -> Message: ... def __enter__(self) -> BlockingWebSocket: ... @@ -741,13 +913,11 @@ class BlockingWebSocket: An optional string representing the WebSocket protocol. """ - ... def recv(self) -> typing.Optional[Message]: r""" Receives a message from the WebSocket. """ - ... def send(self, message: Message) -> None: r""" @@ -757,7 +927,6 @@ class BlockingWebSocket: * `message` - The message to send. """ - ... def close( self, @@ -772,7 +941,6 @@ class BlockingWebSocket: * `code` - An optional close code. * `reason` - An optional reason for closing. """ - ... class Client: r""" @@ -780,8 +948,104 @@ class Client: """ user_agent: typing.Optional[builtins.str] + r""" + Returns the user agent of the client. + + # Returns + + An optional string containing the user agent of the client. + + # Examples + + ```python + import rnet + + client = rnet.Client() + user_agent = client.user_agent() + print(user_agent) + ``` + """ headers: HeaderMap - def __new__(cls, **kwds): ... + r""" + Returns the headers of the client. + + # Returns + + A `HeaderMap` object containing the headers of the client. + + # Examples + + ```python + import rnet + + client = rnet.Client() + headers = client.headers() + print(headers) + ``` + """ + def __new__(cls, **kwds) -> Client: + r""" + Creates a new Client instance. + + # Arguments + + * `**kwds` - Optional request parameters as a dictionary. + + impersonate: typing.Optional[typing.Union[Impersonate, ImpersonateOption]] + base_url: typing.Optional[str] + user_agent: typing.Optional[str] + default_headers: typing.Optional[typing.Dict[str, bytes]] + headers_order: typing.Optional[typing.List[str]] + referer: typing.Optional[builtins.bool] + allow_redirects: typing.Optional[builtins.bool] + max_redirects: typing.Optional[builtins.int] + cookie_store: typing.Optional[builtins.bool] + lookup_ip_strategy: typing.Optional[LookupIpStrategy] + timeout: typing.Optional[builtins.int] + connect_timeout: typing.Optional[builtins.int] + read_timeout: typing.Optional[builtins.int] + no_keepalive: typing.Optional[builtins.bool] + tcp_keepalive: typing.Optional[builtins.int] + pool_idle_timeout: typing.Optional[builtins.int] + pool_max_idle_per_host: typing.Optional[builtins.int] + pool_max_size: typing.Optional[builtins.int] + http1_only: typing.Optional[builtins.bool] + http2_only: typing.Optional[builtins.bool] + https_only: typing.Optional[builtins.bool] + tcp_nodelay: typing.Optional[builtins.bool] + http2_max_retry_count: typing.Optional[builtins.int] + verify: Optional[Union[bool, Path]] + tls_info: typing.Optional[builtins.bool] + min_tls_version: typing.Optional[TlsVersion] + max_tls_version: typing.Optional[TlsVersion] + no_proxy: typing.Optional[builtins.bool] + proxies: typing.Optional[builtins.list[Proxy]] + local_address: typing.Optional[typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]] + interface: typing.Optional[builtins.str] + gzip: typing.Optional[builtins.bool] + brotli: typing.Optional[builtins.bool] + deflate: typing.Optional[builtins.bool] + zstd: typing.Optional[builtins.bool] + + # 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) + ``` + """ + def get_cookies(self, url: str) -> typing.Optional[typing.Any]: r""" Returns the cookies for the given URL. @@ -804,7 +1068,6 @@ class Client: print(cookies) ``` """ - ... def set_cookie(self, url: str, cookie: Cookie) -> None: r""" @@ -823,7 +1086,6 @@ class Client: client.set_cookie("https://example.com", rnet.Cookie(name="foo", value="bar")) ``` """ - ... def remove_cookie(self, url: str, name: str) -> None: r""" @@ -841,13 +1103,11 @@ class Client: client = rnet.Client(cookie_store=True) client.remove_cookie("https://example.com", "foo") """ - ... def clear_cookies(self) -> None: r""" Clears the cookies for the given URL. """ - ... def update( self, @@ -866,14 +1126,13 @@ class Client: Updates the client with the given parameters. # Arguments - * `**kwds` - The parameters to update the client with. - impersonate: typing.Optional[typing.Union[Impersonate, ImpersonateOption]] - headers: typing.Optional[typing.Dict[str, bytes]] - headers_order: typing.Optional[typing.List[str]] - proxies: typing.Optional[builtins.list[Proxy]] - local_address: typing.Optional[typing.Optional[typing.Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]]] - interface: typing.Optional[builtins.str] + * `impersonate` - The impersonation settings for the request. + * `headers` - The headers to use for the request. + * `headers_order` - The order of the headers to use for the request. + * `proxies` - The proxy to use for the request. + * `local_address` - The local IP address to bind to. + * `interface` - The interface to bind to. # Examples @@ -888,7 +1147,6 @@ class Client: ) ``` """ - ... def request(self, method: Method, url: str, **kwds) -> typing.Any: r""" @@ -938,7 +1196,6 @@ class Client: asyncio.run(main()) ``` """ - ... def websocket(self, url: str, **kwds) -> typing.Any: r""" @@ -988,7 +1245,6 @@ class Client: asyncio.run(main()) ``` """ - ... def trace(self, url: str, **kwds) -> typing.Any: r""" @@ -1037,7 +1293,6 @@ class Client: asyncio.run(main()) ``` """ - ... def options(self, url: str, **kwds) -> typing.Any: r""" @@ -1086,7 +1341,6 @@ class Client: asyncio.run(main()) ``` """ - ... def patch(self, url: str, **kwds) -> typing.Any: r""" @@ -1135,7 +1389,6 @@ class Client: asyncio.run(main()) ``` """ - ... def delete(self, url: str, **kwds) -> typing.Any: r""" @@ -1184,7 +1437,6 @@ class Client: asyncio.run(main()) ``` """ - ... def put(self, url: str, **kwds) -> typing.Any: r""" @@ -1233,7 +1485,6 @@ class Client: asyncio.run(main()) ``` """ - ... def post(self, url: str, **kwds) -> typing.Any: r""" @@ -1282,7 +1533,6 @@ class Client: asyncio.run(main()) ``` """ - ... def head(self, url: str, **kwds) -> typing.Any: r""" @@ -1331,7 +1581,6 @@ class Client: asyncio.run(main()) ``` """ - ... def get(self, url: str, **kwds) -> typing.Any: r""" @@ -1380,7 +1629,6 @@ class Client: asyncio.run(main()) ``` """ - ... class Cookie: r""" @@ -1388,15 +1636,45 @@ class Cookie: """ name: builtins.str + r""" + The name of the cookie. + """ value: builtins.str + r""" + The value of the cookie. + """ http_only: builtins.bool + r""" + Returns true if the 'HttpOnly' directive is enabled. + """ secure: builtins.bool + r""" + Returns true if the 'Secure' directive is enabled. + """ same_site_lax: builtins.bool + r""" + Returns true if 'SameSite' directive is 'Lax'. + """ same_site_strict: builtins.bool + r""" + Returns true if 'SameSite' directive is 'Strict'. + """ path: typing.Optional[builtins.str] + r""" + Returns the path directive of the cookie, if set. + """ domain: typing.Optional[builtins.str] + r""" + Returns the domain directive of the cookie, if set. + """ max_age: typing.Optional[datetime.timedelta] + r""" + Get the Max-Age information. + """ expires: typing.Optional[datetime.datetime] + r""" + The cookie expiration time. + """ def __new__( cls, name: builtins.str, @@ -1408,7 +1686,11 @@ class Cookie: http_only: builtins.bool = False, secure: builtins.bool = False, same_site: typing.Optional[SameSite] = None, - ): ... + ) -> Cookie: + r""" + Create a new cookie. + """ + def __str__(self) -> builtins.str: ... def __repr__(self) -> builtins.str: ... @@ -1417,7 +1699,6 @@ class HeaderMap: A HTTP header map. """ - def __new__(cls, init: typing.Optional[dict]): ... def __getitem__(self, key: str) -> typing.Optional[typing.Any]: ... def __setitem__(self, key: str, value: str) -> None: ... def __delitem__(self, key: str) -> None: ... @@ -1426,17 +1707,16 @@ class HeaderMap: def __iter__(self) -> HeaderMapKeysIter: ... def __str__(self) -> builtins.str: ... def __repr__(self) -> builtins.str: ... + def __new__(cls, init: typing.Optional[dict]) -> HeaderMap: ... def get_all(self, key: str) -> HeaderMapValuesIter: r""" Returns multiple value sequences of key mapping """ - ... def items(self) -> HeaderMapItemsIter: r""" Returns key-value pairs in the order they were added. """ - ... class HeaderMapItemsIter: r""" @@ -1475,8 +1755,38 @@ class ImpersonateOption: impersonate_os: typing.Optional[ImpersonateOS] = None, skip_http2: typing.Optional[builtins.bool] = None, skip_headers: typing.Optional[builtins.bool] = None, - ): ... - ... + ) -> ImpersonateOption: + r""" + Create a new impersonation option instance. + + This class allows you to configure browser/client impersonation settings + including the browser type, operating system, and HTTP protocol options. + + Args: + impersonate (Impersonate): The browser/client type to impersonate + impersonate_os (Optional[ImpersonateOS]): The operating system to impersonate, defaults to None + skip_http2 (Optional[bool]): Whether to disable HTTP/2 support, defaults to False + skip_headers (Optional[bool]): Whether to skip default request headers, defaults to False + + Returns: + ImpersonateOption: A new impersonation option instance + + Examples: + ```python + from rnet import ImpersonateOption, Impersonate, ImpersonateOS + + # Basic Chrome 120 impersonation + option = ImpersonateOption(Impersonate.Chrome120) + + # Firefox 136 on Windows with custom options + option = ImpersonateOption( + impersonate=Impersonate.Firefox136, + impersonate_os=ImpersonateOS.Windows, + skip_http2=False, + skip_headers=True + ) + ``` + """ class Message: r""" @@ -1484,11 +1794,53 @@ class Message: """ data: typing.Optional[typing.Any] + r""" + Returns the data of the message as bytes. + + # Returns + + A byte slice representing the data of the message. + """ text: typing.Optional[builtins.str] + r""" + Returns the text content of the message if it is a text message. + + # Returns + + An optional string representing the text content of the message. + """ binary: typing.Optional[typing.Any] + r""" + Returns the binary data of the message if it is a binary message. + + # Returns + + An optional byte slice representing the binary data of the message. + """ ping: typing.Optional[typing.Any] + r""" + Returns the ping data of the message if it is a ping message. + + # Returns + + An optional byte slice representing the ping data of the message. + """ pong: typing.Optional[typing.Any] + r""" + Returns the pong data of the message if it is a pong message. + + # Returns + + An optional byte slice representing the pong data of the message. + """ close: typing.Optional[tuple[builtins.int, typing.Optional[builtins.str]]] + r""" + Returns the close code and reason of the message if it is a close message. + + # Returns + + An optional tuple containing the close code and reason. + """ def __str__(self) -> builtins.str: ... def __repr__(self) -> builtins.str: ... @staticmethod @@ -1503,7 +1855,6 @@ class Message: A new `Message` instance containing the message. """ - ... @staticmethod def binary_from_json(json: typing.Dict[str, typing.Any]) -> Message: @@ -1517,7 +1868,6 @@ class Message: A new `Message` instance containing the message. """ - ... @staticmethod def from_text(text: str) -> Message: @@ -1532,7 +1882,6 @@ class Message: A new `Message` instance containing the text message. """ - ... @staticmethod def from_binary(data: bytes) -> Message: @@ -1547,7 +1896,6 @@ class Message: A new `Message` instance containing the binary message. """ - ... @staticmethod def from_ping(data: bytes) -> Message: @@ -1562,7 +1910,6 @@ class Message: A new `Message` instance containing the ping message. """ - ... @staticmethod def from_pong(data: bytes) -> Message: @@ -1577,7 +1924,6 @@ class Message: A new `Message` instance containing the pong message. """ - ... @staticmethod def from_close(code: builtins.int, reason: typing.Optional[str] = None) -> Message: @@ -1593,21 +1939,21 @@ class Message: A new `Message` instance containing the close message. """ - ... def json(self) -> typing.Dict[str, typing.Any]: r""" Returns the JSON representation of the message. """ - ... class Multipart: r""" A multipart form for a request. """ - def __new__(cls, *parts): ... - ... + def __new__(cls, *parts) -> Multipart: + r""" + Creates a new multipart form. + """ class Part: r""" @@ -1620,8 +1966,16 @@ class Part: value: typing.Any, filename: typing.Optional[builtins.str] = None, mime: typing.Optional[builtins.str] = None, - ): ... - ... + ) -> Part: + r""" + Creates a new part. + + # Arguments + - `name` - The name of the part. + - `value` - The value of the part, either text, bytes, a file path, or a async or sync stream. + - `filename` - The filename of the part. + - `mime` - The MIME type of the part. + """ class Proxy: r""" @@ -1666,7 +2020,6 @@ class Proxy: proxy = rnet.Proxy.http("http://proxy.example.com") ``` """ - ... @staticmethod def https( @@ -1705,7 +2058,6 @@ class Proxy: proxy = rnet.Proxy.https("https://proxy.example.com") ``` """ - ... @staticmethod def all( @@ -1744,7 +2096,6 @@ class Proxy: proxy = rnet.Proxy.all("https://proxy.example.com") ``` """ - ... class Response: r""" @@ -1775,15 +2126,85 @@ class Response: """ url: builtins.str + r""" + Returns the URL of the response. + + # Returns + + A string representing the URL of the response. + """ ok: builtins.bool + r""" + Returns whether the response is successful. + + # Returns + + A boolean indicating whether the response is successful. + """ status: builtins.int + r""" + Returns the status code as integer of the response. + + # Returns + + An integer representing the HTTP status code. + """ status_code: StatusCode + r""" + Returns the status code of the response. + + # Returns + + A Python object representing the HTTP status code. + """ version: Version + r""" + Returns the HTTP version of the response. + + # Returns + + A `Version` object representing the HTTP version of the response. + """ headers: HeaderMap + r""" + Returns the headers of the response. + + # Returns + + A `HeaderMap` object representing the headers of the response. + """ cookies: builtins.list[Cookie] + r""" + Returns the cookies of the response. + + # Returns + + A Python cookies object representing the cookies of the response. + """ content_length: builtins.int + r""" + Returns the content length of the response. + + # Returns + + An integer representing the content length of the response. + """ remote_addr: typing.Optional[SocketAddr] + r""" + Returns the remote address of the response. + + # Returns + + An `IpAddr` object representing the remote address of the response. + """ encoding: builtins.str + r""" + Encoding to decode with when accessing text. + + # Returns + + A string representing the encoding to decode with when accessing text. + """ def __aenter__(self) -> typing.Any: ... def __aexit__( self, _exc_type: typing.Any, _exc_value: typing.Any, _traceback: typing.Any @@ -1796,7 +2217,6 @@ class Response: A Python object representing the TLS peer certificate of the response. """ - ... def text(self) -> typing.Any: r""" @@ -1806,7 +2226,6 @@ class Response: A Python object representing the text content of the response. """ - ... def text_with_charset(self, encoding: builtins.str) -> typing.Any: r""" @@ -1820,7 +2239,6 @@ class Response: A Python object representing the text content of the response. """ - ... def json(self) -> typing.Any: r""" @@ -1830,7 +2248,6 @@ class Response: A Python object representing the JSON content of the response. """ - ... def bytes(self) -> typing.Any: r""" @@ -1840,7 +2257,6 @@ class Response: A Python object representing the bytes content of the response. """ - ... def stream(self) -> Streamer: r""" @@ -1850,13 +2266,11 @@ class Response: A Python object representing the stream content of the response. """ - ... def close(self) -> None: r""" Closes the response connection. """ - ... class SocketAddr: r""" @@ -1868,13 +2282,11 @@ class SocketAddr: r""" Returns the IP address of the socket address. """ - ... def port(self) -> builtins.int: r""" Returns the port number of the socket address. """ - ... class StatusCode: r""" @@ -1887,37 +2299,31 @@ class StatusCode: r""" Return the status code as an integer. """ - ... def is_informational(self) -> builtins.bool: r""" Check if status is within 100-199. """ - ... def is_success(self) -> builtins.bool: r""" Check if status is within 200-299. """ - ... def is_redirection(self) -> builtins.bool: r""" Check if status is within 300-399. """ - ... def is_client_error(self) -> builtins.bool: r""" Check if status is within 400-499. """ - ... def is_server_error(self) -> builtins.bool: r""" Check if status is within 500-599. """ - ... class Streamer: r""" @@ -1967,12 +2373,61 @@ class WebSocket: """ ok: builtins.bool + r""" + Returns whether the response is successful. + + # Returns + + A boolean indicating whether the response is successful. + """ status: builtins.int + r""" + Returns the status code as integer of the response. + + # Returns + + An integer representing the HTTP status code. + """ status_code: StatusCode + r""" + Returns the status code of the response. + + # Returns + + A Python object representing the HTTP status code. + """ version: Version + r""" + Returns the HTTP version of the response. + + # Returns + + A `Version` object representing the HTTP version of the response. + """ headers: HeaderMap + r""" + Returns the headers of the response. + + # Returns + + A `HeaderMap` object representing the headers of the response. + """ cookies: builtins.list[Cookie] + r""" + Returns the cookies of the response. + + # Returns + + A Python cookies object representing the cookies of the response. + """ remote_addr: typing.Optional[SocketAddr] + r""" + Returns the remote address of the response. + + # Returns + + An `IpAddr` object representing the remote address of the response. + """ def __aiter__(self) -> WebSocket: ... def __anext__(self) -> typing.Any: ... def __aenter__(self) -> typing.Any: ... @@ -1987,13 +2442,11 @@ class WebSocket: An optional string representing the WebSocket protocol. """ - ... def recv(self) -> typing.Any: r""" Receives a message from the WebSocket. """ - ... def send(self, message: Message) -> typing.Any: r""" @@ -2003,7 +2456,6 @@ class WebSocket: * `message` - The message to send. """ - ... def close( self, @@ -2018,7 +2470,6 @@ class WebSocket: * `code` - An optional close code. * `reason` - An optional reason for closing. """ - ... class Impersonate(Enum): r""" @@ -2198,7 +2649,6 @@ def delete(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def get(url: str, **kwds) -> typing.Any: r""" @@ -2242,7 +2692,6 @@ def get(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def head(url: str, **kwds) -> typing.Any: r""" @@ -2285,7 +2734,6 @@ def head(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def options(url: str, **kwds) -> typing.Any: r""" @@ -2328,7 +2776,6 @@ def options(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def patch(url: str, **kwds) -> typing.Any: r""" @@ -2372,7 +2819,6 @@ def patch(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def post(url: str, **kwds) -> typing.Any: r""" @@ -2416,7 +2862,6 @@ def post(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def put(url: str, **kwds) -> typing.Any: r""" @@ -2460,7 +2905,6 @@ def put(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def request(method: Method, url: str, **kwds) -> typing.Any: r""" @@ -2506,7 +2950,6 @@ def request(method: Method, url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def trace(url: str, **kwds) -> typing.Any: r""" @@ -2549,7 +2992,6 @@ def trace(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... def websocket(url: str, **kwds) -> typing.Any: r""" @@ -2595,4 +3037,3 @@ def websocket(url: str, **kwds) -> typing.Any: asyncio.run(run()) ``` """ - ... From 4ec6c35c5afd550129e5414b94ec8613c6e5f738 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 26 Apr 2025 23:40:26 +0800 Subject: [PATCH 5/5] update deps --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed744574..a661a2a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,12 @@ name = "stubgen" path = "src/stubgen.rs" [features] -default = ["docs"] +default = [] docs = ["dep:pyo3-stub-gen"] [dependencies] tokio = { version = "1.44.1", features = ["sync"] } -pyo3 = { version = "0.24.1", features = [ +pyo3 = { version = "0.24.2", features = [ "indexmap", "multiple-pymethods", "generate-import-lib", @@ -33,7 +33,7 @@ pyo3-async-runtimes = { version = "0.24.0", features = [ "tokio-runtime", "unstable-streams", ] } -pyo3-stub-gen = { version = "0.8.0", optional = true } +pyo3-stub-gen = { version = "0.8.1", optional = true } serde = { version = "1.0.219", features = ["derive"] } mime = "0.3.17" indexmap = { version = "2.8.0", features = ["serde"] }