httpr 0.1.11__tar.gz → 0.1.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of httpr might be problematic. Click here for more details.
- {httpr-0.1.11 → httpr-0.1.13}/PKG-INFO +1 -1
- httpr-0.1.13/benchmark.jpg +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/httpr/__init__.py +1 -1
- {httpr-0.1.11 → httpr-0.1.13}/pyproject.toml +1 -1
- {httpr-0.1.11 → httpr-0.1.13}/src/lib.rs +2 -2
- httpr-0.1.13/src/response.rs +199 -0
- {httpr-0.1.11 → httpr-0.1.13}/src/traits.rs +3 -1
- {httpr-0.1.11 → httpr-0.1.13}/src/utils.rs +32 -2
- {httpr-0.1.11 → httpr-0.1.13}/tests/test_client.py +36 -0
- httpr-0.1.11/benchmark.jpg +0 -0
- httpr-0.1.11/src/response.rs +0 -100
- {httpr-0.1.11 → httpr-0.1.13}/.github/workflows/CI.yml +4 -4
- {httpr-0.1.11 → httpr-0.1.13}/.github/workflows/mkdocs.yml +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/.github/workflows/set_version.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/.gitignore +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/.pre-commit-config.yaml +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/Cargo.lock +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/Cargo.toml +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/LICENSE +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/README.md +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/README.md +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/benchmark.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/generate_image.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/pyproject.toml +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/requirements.txt +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/benchmark/server.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/docs/index.md +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/docs/writings/index.md +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/docs/writings/posts/2025-02-24-python-http-clients-suck.md +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/httpr/httpr.pyi +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/httpr/py.typed +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/httpr.code-workspace +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/mkdocs.yml +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/scratch.ipynb +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/tests/httpx_conns.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/tests/test_asyncclient.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/tests/test_defs.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/tests/test_ssl.py +0 -0
- {httpr-0.1.11 → httpr-0.1.13}/uv.lock +0 -0
|
Binary file
|
|
@@ -10,7 +10,7 @@ keywords = [ "python", "request",]
|
|
|
10
10
|
classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules",]
|
|
11
11
|
dynamic = [ "version",]
|
|
12
12
|
dependencies = []
|
|
13
|
-
version = "0.1.
|
|
13
|
+
version = "0.1.13"
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "thomasht86"
|
|
16
16
|
|
|
@@ -26,7 +26,7 @@ use tokio_util::codec::{BytesCodec, FramedRead};
|
|
|
26
26
|
use tracing;
|
|
27
27
|
|
|
28
28
|
mod response;
|
|
29
|
-
use response::Response;
|
|
29
|
+
use response::{CaseInsensitiveHeaderMap, Response};
|
|
30
30
|
|
|
31
31
|
mod traits;
|
|
32
32
|
use traits::{CookiesTraits, HeadersTraits};
|
|
@@ -428,7 +428,7 @@ impl RClient {
|
|
|
428
428
|
content: PyBytes::new(py, &f_buf).unbind(),
|
|
429
429
|
cookies: f_cookies,
|
|
430
430
|
encoding: String::new(),
|
|
431
|
-
headers: f_headers,
|
|
431
|
+
headers: CaseInsensitiveHeaderMap::from_indexmap(f_headers),
|
|
432
432
|
status_code: f_status_code,
|
|
433
433
|
url: f_url,
|
|
434
434
|
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
use crate::utils::{get_encoding_from_content, get_encoding_from_case_insensitive_headers};
|
|
2
|
+
use anyhow::{anyhow, Result};
|
|
3
|
+
use encoding_rs::Encoding;
|
|
4
|
+
use foldhash::fast::RandomState;
|
|
5
|
+
use html2text::{
|
|
6
|
+
from_read, from_read_with_decorator,
|
|
7
|
+
render::{RichDecorator, TrivialDecorator},
|
|
8
|
+
};
|
|
9
|
+
use indexmap::IndexMap;
|
|
10
|
+
use pyo3::{prelude::*, types::PyBytes, IntoPyObject};
|
|
11
|
+
use pythonize::pythonize;
|
|
12
|
+
use serde_json::from_slice;
|
|
13
|
+
|
|
14
|
+
/// A struct representing an HTTP response.
|
|
15
|
+
///
|
|
16
|
+
/// This struct provides methods to access various parts of an HTTP response, such as headers, cookies, status code, and the response body.
|
|
17
|
+
/// It also supports decoding the response body as text or JSON, with the ability to specify the character encoding.
|
|
18
|
+
#[pyclass]
|
|
19
|
+
#[derive(Clone)]
|
|
20
|
+
pub struct CaseInsensitiveHeaderMap {
|
|
21
|
+
headers: IndexMap<String, String, RandomState>,
|
|
22
|
+
lowercase_map: IndexMap<String, String, RandomState>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[pymethods]
|
|
26
|
+
impl CaseInsensitiveHeaderMap {
|
|
27
|
+
#[new]
|
|
28
|
+
fn new() -> Self {
|
|
29
|
+
CaseInsensitiveHeaderMap {
|
|
30
|
+
headers: IndexMap::with_hasher(RandomState::default()),
|
|
31
|
+
lowercase_map: IndexMap::with_hasher(RandomState::default()),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn __getitem__(&self, key: String) -> PyResult<String> {
|
|
36
|
+
let lower_key = key.to_lowercase();
|
|
37
|
+
if let Some(original_key) = self.lowercase_map.get(&lower_key) {
|
|
38
|
+
if let Some(value) = self.headers.get(original_key) {
|
|
39
|
+
return Ok(value.clone());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
Err(pyo3::exceptions::PyKeyError::new_err(format!("Header key '{}' not found", key)))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn __contains__(&self, key: String) -> bool {
|
|
46
|
+
self.lowercase_map.contains_key(&key.to_lowercase())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn __iter__(slf: PyRef<'_, Self>) -> PyResult<Py<PyAny>> {
|
|
50
|
+
let iter = slf.headers.keys().cloned().collect::<Vec<_>>();
|
|
51
|
+
Python::with_gil(|py| {
|
|
52
|
+
let iter_obj = iter.into_pyobject(py)?;
|
|
53
|
+
let iter_method = iter_obj.getattr("__iter__")?;
|
|
54
|
+
let py_iter = iter_method.call0()?;
|
|
55
|
+
Ok(py_iter.into())
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn items(&self) -> Vec<(String, String)> {
|
|
60
|
+
self.headers.clone().into_iter().collect()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn keys(&self) -> Vec<String> {
|
|
64
|
+
self.headers.keys().cloned().collect()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn values(&self) -> Vec<String> {
|
|
68
|
+
self.headers.values().cloned().collect()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[pyo3(signature = (key, default=None))]
|
|
72
|
+
fn get(&self, key: String, default: Option<String>) -> String {
|
|
73
|
+
let lower_key = key.to_lowercase();
|
|
74
|
+
if let Some(original_key) = self.lowercase_map.get(&lower_key) {
|
|
75
|
+
if let Some(value) = self.headers.get(original_key) {
|
|
76
|
+
return value.clone();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
default.unwrap_or_default()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl CaseInsensitiveHeaderMap {
|
|
84
|
+
// Helper method to insert a header
|
|
85
|
+
pub fn insert(&mut self, key: String, value: String) {
|
|
86
|
+
let lower_key = key.to_lowercase();
|
|
87
|
+
self.lowercase_map.insert(lower_key, key.clone());
|
|
88
|
+
self.headers.insert(key, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Helper method to build from an IndexMap
|
|
92
|
+
pub fn from_indexmap(map: IndexMap<String, String, RandomState>) -> Self {
|
|
93
|
+
let mut headers_map = CaseInsensitiveHeaderMap::new();
|
|
94
|
+
for (key, value) in map {
|
|
95
|
+
headers_map.insert(key, value);
|
|
96
|
+
}
|
|
97
|
+
headers_map
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Public method to check if a header exists
|
|
101
|
+
pub fn contains_key(&self, key: &str) -> bool {
|
|
102
|
+
self.lowercase_map.contains_key(&key.to_lowercase())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Public method to get a header value
|
|
106
|
+
pub fn get_value(&self, key: &str) -> Option<String> {
|
|
107
|
+
let lower_key = key.to_lowercase();
|
|
108
|
+
if let Some(original_key) = self.lowercase_map.get(&lower_key) {
|
|
109
|
+
if let Some(value) = self.headers.get(original_key) {
|
|
110
|
+
return Some(value.clone());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
None
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[pyclass]
|
|
118
|
+
pub struct Response {
|
|
119
|
+
#[pyo3(get)]
|
|
120
|
+
pub content: Py<PyBytes>,
|
|
121
|
+
#[pyo3(get)]
|
|
122
|
+
pub cookies: IndexMap<String, String, RandomState>,
|
|
123
|
+
#[pyo3(get, set)]
|
|
124
|
+
pub encoding: String,
|
|
125
|
+
#[pyo3(get)]
|
|
126
|
+
pub headers: CaseInsensitiveHeaderMap,
|
|
127
|
+
#[pyo3(get)]
|
|
128
|
+
pub status_code: u16,
|
|
129
|
+
#[pyo3(get)]
|
|
130
|
+
pub url: String,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[pymethods]
|
|
134
|
+
impl Response {
|
|
135
|
+
#[getter]
|
|
136
|
+
fn get_encoding(&mut self, py: Python) -> Result<&String> {
|
|
137
|
+
if !self.encoding.is_empty() {
|
|
138
|
+
return Ok(&self.encoding);
|
|
139
|
+
}
|
|
140
|
+
self.encoding = get_encoding_from_case_insensitive_headers(&self.headers)
|
|
141
|
+
.or_else(|| get_encoding_from_content(self.content.as_bytes(py)))
|
|
142
|
+
.unwrap_or_else(|| "utf-8".to_string());
|
|
143
|
+
Ok(&self.encoding)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[getter]
|
|
147
|
+
fn text(&mut self, py: Python) -> Result<String> {
|
|
148
|
+
// If self.encoding is empty, call get_encoding to populate self.encoding
|
|
149
|
+
if self.encoding.is_empty() {
|
|
150
|
+
self.get_encoding(py)?;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Convert Py<PyBytes> to &[u8]
|
|
154
|
+
let raw_bytes = self.content.as_bytes(py);
|
|
155
|
+
|
|
156
|
+
// Release the GIL here because decoding can be CPU-intensive
|
|
157
|
+
py.allow_threads(|| {
|
|
158
|
+
let encoding = Encoding::for_label(self.encoding.as_bytes())
|
|
159
|
+
.ok_or_else(|| anyhow!("Unsupported charset: {}", self.encoding))?;
|
|
160
|
+
let (decoded_str, detected_encoding, _) = encoding.decode(raw_bytes);
|
|
161
|
+
|
|
162
|
+
// Update self.encoding based on the detected encoding
|
|
163
|
+
if self.encoding != detected_encoding.name() {
|
|
164
|
+
self.encoding = detected_encoding.name().to_string();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Ok(decoded_str.to_string())
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn json(&mut self, py: Python) -> Result<PyObject> {
|
|
172
|
+
let json_value: serde_json::Value = from_slice(self.content.as_bytes(py))?;
|
|
173
|
+
let result = pythonize(py, &json_value).unwrap().unbind();
|
|
174
|
+
Ok(result)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[getter]
|
|
178
|
+
fn text_markdown(&mut self, py: Python) -> Result<String> {
|
|
179
|
+
let raw_bytes = self.content.bind(py).as_bytes();
|
|
180
|
+
let text = py.allow_threads(|| from_read(raw_bytes, 100))?;
|
|
181
|
+
Ok(text)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[getter]
|
|
185
|
+
fn text_plain(&mut self, py: Python) -> Result<String> {
|
|
186
|
+
let raw_bytes = self.content.bind(py).as_bytes();
|
|
187
|
+
let text =
|
|
188
|
+
py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()))?;
|
|
189
|
+
Ok(text)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[getter]
|
|
193
|
+
fn text_rich(&mut self, py: Python) -> Result<String> {
|
|
194
|
+
let raw_bytes = self.content.bind(py).as_bytes();
|
|
195
|
+
let text =
|
|
196
|
+
py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, RichDecorator::new()))?;
|
|
197
|
+
Ok(text)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -40,8 +40,10 @@ impl HeadersTraits for HeaderMap {
|
|
|
40
40
|
let mut index_map =
|
|
41
41
|
IndexMapSSR::with_capacity_and_hasher(self.len(), RandomState::default());
|
|
42
42
|
for (key, value) in self {
|
|
43
|
+
// Store the original header name (preserving case)
|
|
44
|
+
let header_name = key.as_str().to_string();
|
|
43
45
|
index_map.insert(
|
|
44
|
-
|
|
46
|
+
header_name,
|
|
45
47
|
value
|
|
46
48
|
.to_str()
|
|
47
49
|
.unwrap_or_else(|v| panic!("Invalid header value: {v:?}"))
|
|
@@ -46,8 +46,13 @@ fn read_pem_certificates(path: &str) -> Result<Vec<Certificate>> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/// Get encoding from the "Content-Type" header
|
|
49
|
-
|
|
50
|
-
///
|
|
49
|
+
///
|
|
50
|
+
/// This function is deprecated; use get_encoding_from_case_insensitive_headers instead.
|
|
51
|
+
#[deprecated(
|
|
52
|
+
since = "0.1.0",
|
|
53
|
+
note = "use get_encoding_from_case_insensitive_headers instead"
|
|
54
|
+
)]
|
|
55
|
+
#[allow(dead_code)]
|
|
51
56
|
pub fn get_encoding_from_headers(
|
|
52
57
|
headers: &IndexMap<String, String, RandomState>,
|
|
53
58
|
) -> Option<String> {
|
|
@@ -72,6 +77,31 @@ pub fn get_encoding_from_headers(
|
|
|
72
77
|
})
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
/// Get encoding from the "Content-Type" header using CaseInsensitiveHeaderMap
|
|
81
|
+
pub fn get_encoding_from_case_insensitive_headers(
|
|
82
|
+
headers: &crate::response::CaseInsensitiveHeaderMap
|
|
83
|
+
) -> Option<String> {
|
|
84
|
+
if headers.contains_key("content-type") {
|
|
85
|
+
let content_type = headers.get_value("content-type")?;
|
|
86
|
+
|
|
87
|
+
// Parse the Content-Type header to separate the media type and parameters
|
|
88
|
+
let mut parts = content_type.split(';');
|
|
89
|
+
let media_type = parts.next().unwrap_or("").trim();
|
|
90
|
+
let params = parts.next().unwrap_or("").trim();
|
|
91
|
+
|
|
92
|
+
// Check for specific conditions and return the appropriate encoding
|
|
93
|
+
if let Some(param) = params.to_ascii_lowercase().strip_prefix("charset=") {
|
|
94
|
+
Some(param.trim_matches('"').to_ascii_lowercase())
|
|
95
|
+
} else if media_type == "application/json" {
|
|
96
|
+
Some("utf-8".to_string())
|
|
97
|
+
} else {
|
|
98
|
+
None
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
None
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
/// Get encoding from the `<meta charset="...">` tag within the first 2048 bytes of HTML content.
|
|
76
106
|
pub fn get_encoding_from_content(raw_bytes: &[u8]) -> Option<String> {
|
|
77
107
|
let start_sequence: &[u8] = b"charset=";
|
|
@@ -238,6 +238,42 @@ def test_client_post_json():
|
|
|
238
238
|
assert json_data["args"] == {"x": "aaa", "y": "bbb", "z": "3"}
|
|
239
239
|
assert json_data["json"] == data
|
|
240
240
|
|
|
241
|
+
@retry()
|
|
242
|
+
def test_client_number_params():
|
|
243
|
+
client = httpr.Client()
|
|
244
|
+
params = {
|
|
245
|
+
"int": 42,
|
|
246
|
+
"float": 3.14159,
|
|
247
|
+
"sci_notation": 1.23e-4,
|
|
248
|
+
"large_float": 1.7976931348623157e+308,
|
|
249
|
+
"small_float": 0.026305610314011577,
|
|
250
|
+
}
|
|
251
|
+
response = client.get("https://httpbin.org/anything", params=params)
|
|
252
|
+
assert response.status_code == 200
|
|
253
|
+
json_data = response.json()
|
|
254
|
+
|
|
255
|
+
# Verify all numeric values are converted to strings
|
|
256
|
+
assert json_data["args"]["int"] == "42"
|
|
257
|
+
assert json_data["args"]["float"] == "3.14159"
|
|
258
|
+
assert json_data["args"]["sci_notation"] == "0.000123" or json_data["args"]["sci_notation"] == "1.23e-4"
|
|
259
|
+
# Large values might have different string representations
|
|
260
|
+
assert float(json_data["args"]["large_float"]) == 1.7976931348623157e+308
|
|
261
|
+
assert float(json_data["args"]["small_float"]) == 0.026305610314011577
|
|
262
|
+
|
|
263
|
+
@retry()
|
|
264
|
+
def test_header_case_preservation():
|
|
265
|
+
client = httpr.Client()
|
|
266
|
+
|
|
267
|
+
# Send a request to a server that will return case-sensitive headers
|
|
268
|
+
response = client.get("https://httpbin.org/response-headers?X-Custom-Header=TestValue")
|
|
269
|
+
|
|
270
|
+
# Verify the header case is preserved
|
|
271
|
+
assert "X-Custom-Header" in response.headers
|
|
272
|
+
assert response.headers["X-Custom-Header"] == "TestValue"
|
|
273
|
+
|
|
274
|
+
# Also verify headers can be accessed with different cases
|
|
275
|
+
assert "x-custom-header" in response.headers
|
|
276
|
+
assert response.headers["x-custom-header"] == "TestValue"
|
|
241
277
|
|
|
242
278
|
@pytest.fixture(scope="session")
|
|
243
279
|
def test_files(tmp_path_factory):
|
httpr-0.1.11/benchmark.jpg
DELETED
|
Binary file
|
httpr-0.1.11/src/response.rs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
use crate::utils::{get_encoding_from_content, get_encoding_from_headers};
|
|
2
|
-
use anyhow::{anyhow, Result};
|
|
3
|
-
use encoding_rs::Encoding;
|
|
4
|
-
use foldhash::fast::RandomState;
|
|
5
|
-
use html2text::{
|
|
6
|
-
from_read, from_read_with_decorator,
|
|
7
|
-
render::{RichDecorator, TrivialDecorator},
|
|
8
|
-
};
|
|
9
|
-
use indexmap::IndexMap;
|
|
10
|
-
use pyo3::{prelude::*, types::PyBytes};
|
|
11
|
-
use pythonize::pythonize;
|
|
12
|
-
use serde_json::from_slice;
|
|
13
|
-
|
|
14
|
-
/// A struct representing an HTTP response.
|
|
15
|
-
///
|
|
16
|
-
/// This struct provides methods to access various parts of an HTTP response, such as headers, cookies, status code, and the response body.
|
|
17
|
-
/// It also supports decoding the response body as text or JSON, with the ability to specify the character encoding.
|
|
18
|
-
#[pyclass]
|
|
19
|
-
pub struct Response {
|
|
20
|
-
#[pyo3(get)]
|
|
21
|
-
pub content: Py<PyBytes>,
|
|
22
|
-
#[pyo3(get)]
|
|
23
|
-
pub cookies: IndexMap<String, String, RandomState>,
|
|
24
|
-
#[pyo3(get, set)]
|
|
25
|
-
pub encoding: String,
|
|
26
|
-
#[pyo3(get)]
|
|
27
|
-
pub headers: IndexMap<String, String, RandomState>,
|
|
28
|
-
#[pyo3(get)]
|
|
29
|
-
pub status_code: u16,
|
|
30
|
-
#[pyo3(get)]
|
|
31
|
-
pub url: String,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#[pymethods]
|
|
35
|
-
impl Response {
|
|
36
|
-
#[getter]
|
|
37
|
-
fn get_encoding(&mut self, py: Python) -> Result<&String> {
|
|
38
|
-
if !self.encoding.is_empty() {
|
|
39
|
-
return Ok(&self.encoding);
|
|
40
|
-
}
|
|
41
|
-
self.encoding = get_encoding_from_headers(&self.headers)
|
|
42
|
-
.or_else(|| get_encoding_from_content(self.content.as_bytes(py)))
|
|
43
|
-
.unwrap_or_else(|| "utf-8".to_string());
|
|
44
|
-
Ok(&self.encoding)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
#[getter]
|
|
48
|
-
fn text(&mut self, py: Python) -> Result<String> {
|
|
49
|
-
// If self.encoding is empty, call get_encoding to populate self.encoding
|
|
50
|
-
if self.encoding.is_empty() {
|
|
51
|
-
self.get_encoding(py)?;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Convert Py<PyBytes> to &[u8]
|
|
55
|
-
let raw_bytes = self.content.as_bytes(py);
|
|
56
|
-
|
|
57
|
-
// Release the GIL here because decoding can be CPU-intensive
|
|
58
|
-
py.allow_threads(|| {
|
|
59
|
-
let encoding = Encoding::for_label(self.encoding.as_bytes())
|
|
60
|
-
.ok_or_else(|| anyhow!("Unsupported charset: {}", self.encoding))?;
|
|
61
|
-
let (decoded_str, detected_encoding, _) = encoding.decode(raw_bytes);
|
|
62
|
-
|
|
63
|
-
// Update self.encoding based on the detected encoding
|
|
64
|
-
if self.encoding != detected_encoding.name() {
|
|
65
|
-
self.encoding = detected_encoding.name().to_string();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
Ok(decoded_str.to_string())
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
fn json(&mut self, py: Python) -> Result<PyObject> {
|
|
73
|
-
let json_value: serde_json::Value = from_slice(self.content.as_bytes(py))?;
|
|
74
|
-
let result = pythonize(py, &json_value).unwrap().unbind();
|
|
75
|
-
Ok(result)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
#[getter]
|
|
79
|
-
fn text_markdown(&mut self, py: Python) -> Result<String> {
|
|
80
|
-
let raw_bytes = self.content.bind(py).as_bytes();
|
|
81
|
-
let text = py.allow_threads(|| from_read(raw_bytes, 100))?;
|
|
82
|
-
Ok(text)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
#[getter]
|
|
86
|
-
fn text_plain(&mut self, py: Python) -> Result<String> {
|
|
87
|
-
let raw_bytes = self.content.bind(py).as_bytes();
|
|
88
|
-
let text =
|
|
89
|
-
py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()))?;
|
|
90
|
-
Ok(text)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
#[getter]
|
|
94
|
-
fn text_rich(&mut self, py: Python) -> Result<String> {
|
|
95
|
-
let raw_bytes = self.content.bind(py).as_bytes();
|
|
96
|
-
let text =
|
|
97
|
-
py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, RichDecorator::new()))?;
|
|
98
|
-
Ok(text)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -344,6 +344,10 @@ jobs:
|
|
|
344
344
|
runs-on: ubuntu-latest
|
|
345
345
|
needs: [linux]
|
|
346
346
|
steps:
|
|
347
|
+
- name: Checkout main branch
|
|
348
|
+
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
349
|
+
run: |
|
|
350
|
+
git checkout main
|
|
347
351
|
- uses: actions/checkout@v4
|
|
348
352
|
with:
|
|
349
353
|
fetch-depth: 0
|
|
@@ -367,10 +371,6 @@ jobs:
|
|
|
367
371
|
run: python benchmark/benchmark.py
|
|
368
372
|
- name: Generate image
|
|
369
373
|
run: python benchmark/generate_image.py
|
|
370
|
-
- name: Checkout main branch
|
|
371
|
-
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
372
|
-
run: |
|
|
373
|
-
git checkout main
|
|
374
374
|
- name: Commit generated image if changed
|
|
375
375
|
uses: EndBug/add-and-commit@v9
|
|
376
376
|
with:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|